Giter Club home page Giter Club logo

obeythetestinggoat's Introduction

Obeying The Testing Goat in 2023... with the latest versions of Python (3.11), Django (4.2), and Selenium (4.9)

Live previews

About this project

The goal of this project is to follow along with the tutorials in the book: "Test-Driven Development with Python, 2nd Edition" by Harry Percival, which is available for free online at www.obeythetestinggoat.com, however the example code uses old versions of python, django and selenium, and the book discourages trying to use newer versions. But because I don't do what I'm told, I've decided to document the changes required to bring run the code with the latest versions as of June 2023. This readme file will describe just the amendments, and the code will be available in full in the repository, with branches for each Chapter.

  • Install Firefox (as per book)
  • Install Git (as per book)
  • Install Python 3.11 from Python.org
  • Install pipenv (pip3 install pipenv) (instead of using virtualenv)
  • pipenv install django selenium (latest versions which are django 4.2.1, selenium 4.9.1)
  • Download Geckodriver and move to /usr/local/bin/

functional_tests.py
assert 'Django' in browser.title
assert 'The install worked successfully' in browser.title

$django-admin.py startproject superlists .
$django-admin startproject superlists .

No code changes required.

The way the URL dispatcher works in the book examples compared to Django 4.2 has changed, see the Django 4.2 URL dispatcher docs

superlists/urls.py:
url(r'^$', views.home_page, name='home'),
path('', views.home_page, name='home')

The .find_element_by_id() and .find_element_by_tag_name() methods need to be replaced with:
.find_element(by=['id'|'tag name'], value='id|name') as follows.

/lists/tests.py:
header_text = self.browser.find_element_by_tag_name('h1').text
header_text = self.browser.find_element(by='tag name', value='h1').text

inputbox = self.browser.find_element_by_id('id_new_item')
input_box = self.browser.find_element(by='id', value='id_new_item')

table = self.browser.find_element_by_id('id_list_table')
table = self.browser.find_element(by='id', value='id_list_table')

rows = table.find_elements_by_tag_name('tr')
rows = table.find_elements(by='tag name', value='tr')

Note, I named the inputbox variable with an underscore input_box to keep it consistent with the previous header_text variable.
Also, there's a .find_elements() method, not used here, which can be used in place of .find_elements_by_id() and .find_elements_by_tag_name()

No code changes required, except for .find_elements_by_id() as per chapter 04 above

No code changes required.

A New URL

/superlists/urls.py:
url(r'^lists/the-only-list-in-the-world/$', views.view_list, name='view_list'),
path('lists/the-only-list-in-the-world/', views.view_list, name='view_list'),

A Foreign Key Relationship

on_delete is now a required named parameter to models.ForeignKey()

/lists/models.py:
list = models.ForeignKey(List, default=None)
list = models.ForeignKey(List, default=None, on_delete=models.CASCADE)

I've used models.CASCADE which will delete the item when it's list is deleted.
See the other available options in the Django 4.2 docs: ForeignKey.on_delete

Capturing Parameters from URLs

The url dispatcher code has changed since the book example was written (as noted in Chapter 03)

/superlists/urls.py:
url(r'^lists/(.+)/$', views.view_list, name='view_list'),
path("lists/<int:id>/", views.view_list, name='view_list'),

Beware of Greedy Regular Expressions!

Because the URL dispatcher is different now, the last change to /superlists/urls.py doesn't use the same regular expressions, and is not susceptible to this problem, so we can skip this section as we receive the error 404 already.

The Last New URL

/superlists/urls.py:
url(r'^lists/(\d+)/add_item$', views.add_item, name='add_item'),
path("lists/<int:list_id>/add_item", views.add_item, name='add_item'),

No code changes required.

Chapters 09, 10, 11: Staging site, Production-Ready Deployment, Automating Deployment with Fabric

I skimmed through these chapters, and ended up deploying my app on PythonAnywhere, which was probably a lot simpler - I just started with PythonAnywhere's Django template and replaced the code with the code from this repo. There was a slightly different process for getting the environment variables set up, but I followed the instructions in PA's help, and got it working.

Set of supported locator strategies (for find_element() and find_elements()):

  • "id"
  • "xpath"
  • "link text"
  • "partial link text"
  • "name"
  • "tag name"
  • "class name"
  • "css selector"

functional_tests/test_list_item_validation.py:
self.browser.find_element_by_css_selector('.has-error').text,
self.browser.find_element(by='css selector', value='.has-error'),

No code changes required.

No code changes required.

No code changes required.

To download the QUnit and jQuery files:
curl -o qunit-2.19.4.css 'https://code.jquery.com/qunit/qunit-2.19.4.css'
curl -o qunit-2.19.4.js 'https://code.jquery.com/qunit/qunit-2.19.4.js'
curl -o jquery-3.7.0.min.js 'https://code.jquery.com/jquery-3.7.0.min.js'

Additional parameter required in authenticate()

/accounts/authenticate.py:
def authenticate(self, uid):
def authenticate(self, request, uid):

And edit the 3x AuthenticateTest(TestCase)'s in /accounts/tests/test_authentication.py:
PasswordlessAuthenticationBackend().authenticate('no-such-token')
PasswordlessAuthenticationBackend().authenticate(request=None, uid='no-such-token')

accounts/urls.py:
from django.contrib.auth.views import logout
from django.contrib.auth.views import LogoutView

url(r'^logout$', logout, {'next_page': '/'}, name='logout'),
path('logout', LogoutView.as_view(next_page='/'), name='logout'),

Pretty much from the start of this chapter I got an error, due to a change in Django since version 1:
TypeError: quote_from_bytes() expected bytes

I found this stackoverflow question/answer: Mock() function gives TypeError in django2

Which gave the solution of adding the following 2 lines into the tests

returned_object = mock_form.save.return_value
returned_object.get_absolute_url.return_value = 'fakeurl'

Instead of repeating that code multiple times in the tests, I extracted it into a static method:

@staticmethod
def mock_form_save_get_absolute_url(mock_form):
    returned_object = mock_form.save.return_value
    returned_object.get_absolute_url.return_value = "fakeurl"

and called it like this:
self.mock_form_save_get_absolute_url(mock_form)

obeythetestinggoat's People

Contributors

davidlpoole avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.