This is a learning note to help you understand Test Driven Development (TDD) in Django.
References are [** Test-Driven Development with Python: Obey the Testing Goat: Using Django, Selenium, and JavaScript (English Edition) 2nd Edition **](https://www.amazon.co.jp/dp/B074HXXXLS We will proceed with learning based on / ref = dp-kindle-redirect? _ Encoding = UTF8 & btkr = 1).
In this book, we are conducting functional tests using Django 1.1 series and FireFox, but this time we will carry out functional tests on Djagno 3 series and Google Chrome. I've also made some personal modifications (such as changing the Project name to Config), but there are no major changes.
⇒⇒ Click here for Part 1 --Chapter 1 ⇒⇒ Click here for Part 2-Chapter 2 ⇒⇒ Click here for Part 3-Chapter 3
Part1. The Basics of TDD and Django
Chapter4. What Are We Doing with All These Tests? (And, Refactoring)
I suppressed the flow of TDD until Chapter 3, but it was a little too detailed and boring (especially home_page = None
)
To be honest, is it necessary to write the code while looking at the unit test results in such detail?
Programming is Like Pulling a Bucket of Water Up from a Well
TDD is a bit boring and annoying, but it's also a stop to protect programmer development. Developing with TDD is a very tiring task, but it's a development method that is appreciated in the long run. The trick is to proceed with development based on as small a test as possible.
Using Selenium to Test User Interactions
Last time, we created a home_page view from unit tests, so let's extend the functional tests this time.
# django-tdd/functional_tests.py
from selenium import webdriver
from selenium.webdriver.common.keys import Keys #add to
import time #add to
import unittest
class NewVisitorTest(unittest.TestCase):
def setUp(self):
self.browser = webdriver.Chrome()
def tearDown(self):
self.browser.quit()
def test_can_start_a_list_and_retrieve_it_later(self):
#Nobita is a new to-I heard that there is a do app and accessed the homepage.
self.browser.get('http://localhost:8000')
#Nobita has the page title and header to-I confirmed that it suggests that it is a do app.
self.assertIn('To-Do', self.browser.title)
header_text = self.browser.find_element_by_tag_name('h1').text
self.assertIn('To-Do', header_text)
#Nobita is to-Prompted to fill in the do item,
inputbox = self.browser.find_element_by_id('id_new_item')
self.assertEqual(
inputbox.get_attribute('placeholder'),
'Enter a to-do item'
)
#Nobita wrote in the text box "Buy Dorayaki"(His best friend loves dorayaki)
inputbox.send_keys('Buy dorayaki')
#When Nobita presses enter, the page is refreshed
# "1:Buying dorayaki"Is to-Found to be added as an item to the do list
inputbox.send_keys(Keys.ENTER)
time.sleep(1) #Wait for page refresh.
table = self.browser.find_element_by_id('id_list_table')
rows = table.find_elements_by_tag_name('tr')
self.assertTrue(
any(row.text == "1: Buy dorayaki" for row in rows)
)
#The text box allows you to continue to fill in items, so
#Filled in "Billing Dorayaki Money"(He is tight on money)
self.fail("Finish the test!")
#The page was refreshed again and I was able to see that new items were added
#Nobita is this to-I was wondering if the do app was recording my items properly,
#When I checked the URL, I found that the URL seems to be a specific URL for Nobita
#When Nobita tried to access a specific URL that he had confirmed once,
#The item was saved so I was happy to fall asleep.
if __name__ == '__main__':
unittest.main(warnings='ignore')
Enhanced functional testing. Let's actually test it.
#Launch a development server
$ python manage.py runserver
#Launch another cmd to run a functional test
$ python functional_tests.py
DevTools listening on ws://127.0.0.1:51636/devtools/browser/9aa225f9-c6e8-4119-ac2a-360d76473962
E
======================================================================
ERROR: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "functional_tests.py", line 24, in test_can_start_a_list_and_retrieve_it_later
header_text = self.browser.find_element_by_tag_name('h1').text
File "C:\--your_path--\django-TDD\venv-tdd\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 530, in find_element_by_tag_name
return self.find_element(by=By.TAG_NAME, value=name)
File "C:\--your_path--\django-TDD\venv-tdd\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 978, in find_element
'value': value})['value']
File "C:\--your_path--\django-TDD\venv-tdd\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 321, in execute
self.error_handler.check_response(response)
File "C:\--your_path--\django-TDD\venv-tdd\lib\site-packages\selenium\webdriver\remote\errorhandler.py", line 242, in check_response
raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"h1"}
(Session info: chrome=79.0.3945.130)
----------------------------------------------------------------------
Ran 1 test in 7.004s
FAILED (errors=1)
The test result was that the <h1>
element was not found. What can be done to solve this?
First of all, we have extended the functional test, so let's commit it.
$ git add .
$ git commit -m "Functional test now checks we can input a to-do item"
The "Don't Test Constants" Rule, and Templates to the Rescue
Let's check the current lists / tests.py.
# lists/tests.py
from django.urls import resolve
from django.test import TestCase
from django.http import HttpRequest
from lists.views import home_page
class HomePageTest(TestCase):
def test_root_url_resolve_to_home_page_view(self):
found = resolve('/')
self.assertEqual(found.func, home_page)
def test_home_page_returns_current_html(self):
request = HttpRequest()
response = home_page(request)
html = response.content.decode('utf8')
self.assertTrue(html.startswith('<html>'))
self.assertIn('<title>To-Do lists</title>', html)
self.assertTrue(html.endswith('</html>'))
Looking at this, I'm checking if it contains a specific HTML string, but this is not an effective method. In general, unit tests should avoid testing constants. HTML in particular is like a collection of constants (text).
HTML should be created using a template and functional tests should proceed assuming that.
Refactoring to Use a Template
Refactor lists / views.py to return a specific HTML file. The feeling of refactoring in TDD is to * improve existing functionality without changing it *. Refactoring cannot proceed without testing. Let's unit test first.
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.009s
OK
Destroying test database for alias 'default'...
If it is a continuation from the last time, the test should pass without any problem. Now let's create a template.
$ mkdir templates
$ cd templates
$ mkdir lists
$ type nul > lists\home.html
$ cd ../ # manage.Return to the directory where py is
<!-- templates/lists/home.html -->
<html>
<title>To-Do lists</title>
</html>
Modify lists / views.py to return this.
# lists/views.py
from django.shortcuts import render
def home_page(request):
return render(request, 'lists/home.html')
Let's unit test.
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
E.
======================================================================
ERROR: test_home_page_returns_current_html (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\--your_path--\django-TDD\lists\tests.py", line 18, in test_home_page_returns_current_html
response = home_page(request)
File "C:\--your_path--\django-TDD\lists\views.py", line 7, in home_page
return render(request, 'home.html')
File "C:\--your_path--\django-TDD\venv-tdd\lib\site-packages\django\shortcuts.py", line 19, in render
content = loader.render_to_string(template_name, context, request, using=using)
File "C:\--your_path--\django-TDD\venv-tdd\lib\site-packages\django\template\loader.py", line 61, in render_to_string
template = get_template(template_name, using=using)
File "C:\--your_path--\django-TDD\venv-tdd\lib\site-packages\django\template\loader.py", line 19, in get_template
raise TemplateDoesNotExist(template_name, chain=chain)
django.template.exceptions.TemplateDoesNotExist: home.html
----------------------------------------------------------------------
Ran 2 tests in 0.019s
FAILED (errors=1)
Destroying test database for alias 'default'...
You'll see the message django.template.exceptions.TemplateDoesNotExist: home.html
, even though you should have created the template.
You can also see that the return render (request,'home.html')
in lists / views.py isn't working.
This is because you didn't register with Django when you created the application. Add it to ʻINSTALLED_APPS` in config / settings.py.
# config/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'lists.apps.ListsConfig', #add to
]
Let's test it.
$ python manage.py test
======================================================================
FAIL: test_home_page_returns_current_html (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\--your_path--\django-TDD\lists\tests.py", line 22, in test_home_page_returns_current_html
self.assertTrue(html.endswith('</html>'))
AssertionError: False is not true
----------------------------------------------------------------------
If you check this, you can see that it is stumbling with self.assertTrue (html.endswith ('</ html>'))
, but lists / home.html certainly ends with </ html>
. I will.
You can check it by adding print (repr (html))
to a part of html of lists / tests.py and executing it, but at the end of the sentence of lists / home.html, the line feed code \ n
Has been added.
Some tests need to be modified to pass this.
# lists/tests.py
#~~abridgement~~
self.assertTrue(html.strip().endswith('</html>')) #Change
Let's run it now.
$ python manage.py test
----------------------------------------------------------------------
Ran 2 tests in 0.032s
OK
The unit test passed. You can now modify lists / views.py to return a template. Then try refactoring lists / tests.py to determine if the correct template is being rendered.
The Django Test Client
Django's .assertTemplteUsed
is an effective way to test if the correct template is returned.
Let's add it as part of the test.
# lists/tests.py
# ~~abridgement~~
def test_home_page_returns_current_html(self):
response = self.client.get('/') #Change
html = response.content.decode('utf8')
# print(repr(html))
self.assertTrue(html.startswith('<html>'))
self.assertIn('<title>To-Do lists</title>', html)
self.assertTrue(html.strip().endswith('</html>')) #Change
self.assertTemplateUsed(response, 'lists/home.html') #add to
Changed to a request using Djagno test Client instead of a manual request using HttpRequest () to use .assertTemplateUsed
.
$ python manage.py test
----------------------------------------------------------------------
Ran 2 tests in 0.040s
OK
You can use this Django test Client and .assertTemplateUsed
together to see if the URL is mapped and if the specified template is returned. Therefore, lists / tests.py could be rewritten more neatly.
# lists/tests.py
from django.test import TestCase
class HomePageTest(TestCase):
def test_users_home_template(self):
response = self.client.get('/') #URL resolution
self.assertTemplateUsed(response, 'lists/home.html')
Now that we have refactored the unit test, lists / view.py, let's commit.
$ git add .
$ git commit -m "Refactor home page view to user a template"
A Little More of Our Front Page
The unit tests passed, but the functional tests still failed. The contents of the template are not evaluated in unit tests, so functional tests are used to determine if the template is correct.
<!-- lists/home.html -->
<html>
<head>
<title>To-Do lists</title>
</head>
<body>
<h1>Your To-Do list</h1>
</body>
</html>
$ python functional_tests.py
[...]
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"[id="id_new_item"]"}
(Session info: chrome=79.0.3945.130)
Add a place to enter a new item.
<!-- lists/home.html -->
<html>
<head>
<title>To-Do lists</title>
</head>
<body>
<h1>Your To-Do list</h1>
<input id="id_new_item">
</body>
</html>
$ python functional_tests.py
[...]
AssertionError: '' != 'Enter a to-do item'
+ Enter a to-do item
Let's add a placeholder.
<!-- lists/home.html -->
<html>
<head>
<title>To-Do lists</title>
</head>
<body>
<h1>Your To-Do list</h1>
<input id="id_new_item" placeholder="Enter a to-do item">
</body>
</html>
$ python functional_tests.py
[...]
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"[id="id_list_table"]"}
Add a table tag.
<!-- lists/home.html -->
<html>
<head>
<title>To-Do lists</title>
</head>
<body>
<h1>Your To-Do list</h1>
<input id="id_new_item" placeholder="Enter a to-do item">
<table id="id_list_table">
</table>
</body>
</html>
$ python functional_tests.py
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "functional_tests.py", line 45, in test_can_start_a_list_and_retrieve_it_later
any(row.text == "1: Buy dorayaki" for row in rows)
AssertionError: False is not true
This is an error with .assertTrue (any (~~))
in functional_tests.py. any (iterator) returns True if the argument is inside an iterator.
The function to return the entered value as "1: Buy dorayaki" will be implemented later.
For now, let's add a custom error message as " New to-do item did not appear in table "
.
# functional_tests.py
# ~~abridgement~~
table = self.browser.find_element_by_id('id_list_table')
rows = table.find_elements_by_tag_name('tr')
self.assertTrue(
any(row.text == "1: Buy dorayaki" for row in rows),
"New to-do item did not appear in table" #add to
)
$ python functional_tests.py
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "functional_tests.py", line 46, in test_can_start_a_list_and_retrieve_it_later
"New to-do item did not appear in table"
AssertionError: False is not true : New to-do item did not appear in table
----------------------------------------------------------------------
Let's commit.
$ git add .
$ git commit -m "Front page HTML now generated from template"
Implemented functional tests, unit tests, unit test and coding cycles, and refactoring flows. The tip is long. ..
Recommended Posts