[PYTHON] Test Driven Development with Django Part 4

Test Driven Development with Django Part 4

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"

Chapter 4 Summary

Implemented functional tests, unit tests, unit test and coding cycles, and refactoring flows. The tip is long. ..

Recommended Posts

Test Driven Development with Django Part 3
Test Driven Development with Django Part 4
Test Driven Development with Django Part 6
Test Driven Development with Django Part 2
Test Driven Development with Django Part 1
Test Driven Development with Django Part 5
Test Driven Development Startup with PySide & Pytest
Django test
[Test Driven Development (TDD)] Chapter 21 Summary
How to authenticate with Django Part 2
How to authenticate with Django Part 3
Application development using SQLite with Django (PTVS)
Django begins part 1
Experience Part I "Multinational Currencies" in the book "Test Driven Development" in Python
Internationalization with django
Django begins part 4
CRUD with Django
First Django development
Create test data like that with Python (Part 1)
Build Django + NGINX + PostgreSQL development environment with Docker
Build the fastest Django development environment with docker-compose
[Python] Build a Django development environment with Docker
Django Getting Started Part 2 with eclipse Plugin (PyDev)
Build a Django development environment with Doker Toolbox
Authenticate Google with Django
Django 1.11 started with Python3.6
Primality test with Python
Upload files with Django
Strengthen with code test ⑦
Output PDF with Django
Strengthen with code test ⑨
Strengthen with code test ③
Markdown output with Django
Use Gentelella with django
Twitter OAuth with Django
Strengthen with code test ⑤
Strengthen with code test ④
Getting Started with Django 1
Primality test with python
Send email with Django
sandbox with neo4j part 10
Web application made with Python3.4 + Django (Part.1 Environment construction)
File upload with django
Strengthen with code test ②
Use LESS with Django
Pooling mechanize with Django
Use MySQL with Django
Strengthen with code test ①
Build a development environment with Poetry Django Docker Pycharm
[Memo] Django development environment
Articles that enable system development with Django (Python) _Introduction
Start today with Django
Getting Started with Django 2
Strengthen with code test ⑧
Strengthen with code test ⑨
Tutorial for doing Test Driven Development (TDD) in Flask-2 Decorators
Build a Django development environment with Docker! (Docker-compose / Django / postgreSQL / nginx)
[Memo] Build a development environment for Django + Nuxt.js with Docker
[Django] Build a Django container (Docker) development environment quickly with PyCharm
Build a bulletin board app from scratch with Django. (Part 2)
Build a bulletin board app from scratch with Django. (Part 3)