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 ⇒⇒ Click here for Part 4-Chapter 4 ⇒⇒ Click here for Part 5 --Chapter 5
Part1. The Basics of TDD and Django
Chapter6. Improving Functional Testss: Ensuring Isolation and Removing Voodoo Sleeps
In Chapter 5, I checked whether the POSTed data was saved and whether it could be returned to the response without any problem.
Since the unit test creates a Django test DB, the test data is deleted when the test is executed, but the functional test uses the production DB (db.sqlite3) (in the current setting). There was a problem that the data at the time of testing was also saved.
This time we will practice best practice
for these problems.
Ensuring Test Isolation in Functional Tests
If the test data remains, it cannot be separated between the tests, so troubles such as "the test that should succeed because the test data is saved fails" occur. It is important to be aware of the separation between tests to avoid this.
In Djagno, you can implement a mechanism that automatically creates a database for testing like a unit test and deletes it after the test is completed by using the LiveServerTestCase
class.
LiveServerTestCase
is intended for testing with Django's test runner.
When Django's test runner runs, it runs files starting with test
in all folders.
So let's create a folder for functional testing like a Django application.
#Create folder
$ mkdir functional_tests
#To make Django recognize it as a Python package
$ type nul > functional_tests/__init__.py
#Rename and move existing functional tests
$ git mv functional_tests.py functional_tests/tests.py
#Verification
$ git status
Now I was running a functional test with python manage.py functional_tests.py
You can now run it with python manage.py test functional_tests
.
Now let's rewrite the functional test.
# django-tdd/functional_tests/tests.py
from django.test import LiveServerTestCase #add to
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import time
class NewVisitorTest(LiveServerTestCase): #Change
def setUp(self):
self.browser = webdriver.Chrome()
def tearDown(self):
self.browser.quit()
def check_for_row_in_list_table(self, row_text):
table = self.browser.find_element_by_id('id_list_table')
rows = table.find_elements_by_tag_name('tr')
self.assertIn(row_text, [row.text for row in rows])
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(self.live_server_url) #Change
#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(3) #Wait for page refresh.
self.check_for_row_in_list_table('1: Buy dorayaki')
#The text box allows you to continue to fill in items, so
#Filled in "Billing Dorayaki Money"(He is tight on money)
inputbox = self.browser.find_element_by_id('id_new_item')
inputbox.send_keys("Demand payment for the dorayaki")
inputbox.send_keys(Keys.ENTER)
time.sleep(3)
#The page was refreshed again and I was able to see that new items were added
self.check_for_row_in_list_table('2: Demand payment for the dorayaki')
#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
self.fail("Finish the test!")
#When Nobita tried to access a specific URL that he had confirmed once,
#The item was saved so I was happy to fall asleep.
Changed the functional test to inherit LiveServerTestCase
from the ʻunittest module. Now that you can run functional tests using Django's test runners, I've removed the following: ʻif __name =='__main__'
Now let's actually run the functional test.
$ python manage.py test functional_tests
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (functional_tests.tests.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:--your_path--\django-TDD\functional_tests\tests.py", line 59, in test_can_start_a_list_and_retrieve_it_later
self.fail("Finish the test!")
AssertionError: Finish the test!
----------------------------------------------------------------------
Ran 1 test in 28.702s
FAILED (failures=1)
Destroying test database for alias 'default'...
The functional test ended with self.fail
and gave the same results as before applying the LiveServerTestCase
.
We also confirmed that a database for functional testing was created and deleted as soon as the test was completed.
Let's commit here.
$ git status
$ git add functional_tests
$ git commit -m "make functional_tests an app, use LiveSeverTestCase"
Running Just the Unit Tests
The python manage.py test
command allows Django to run unit and functional tests together.
If you want to test only unit tests, specify the application like python manage.py test lists
.
On Implicit and Explicit Waits, and Voodoo time.sleeps
I added time.sleep (3)
to check the result of running the functional test.
Whether this time.sleep (3)
is set to 3 seconds, 1 second, or 0.5 seconds depends on the response, but I do not know what is the correct answer.
Let's rewrite the functional test so that only the necessary buffers are prepared.
Change check_for_row_in_list_table
to wait_for_row_in_list_table
and add polling / retry logic.
# django-tdd/functional_tests/tests.py
from django.test import LiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import WebDriverException #add to
import time
MAX_WAIT = 10 #add to
class NewVisitorTest(LiveServerTestCase):
def setUp(self):
self.browser = webdriver.Chrome()
def tearDown(self):
self.browser.quit()
def wait_for_row_in_list_table(self, row_text):
start_time = time.time()
while True:
try:
table = self.browser.find_element_by_id('id_list_table')
rows = table.find_elements_by_tag_name('tr')
self.assertIn(row_text, [row.text for row in rows])
return
except (AssertionError, WebDriverException) as e:
if time.time() - start_time > MAX_WAIT:
raise e
time.sleep(0.5)
[...]
This allows us to stop processing only the buffers we need for the response (we'll refactor it later). I try to wait up to 10 seconds.
Let's change the part that was executing check_for_row_in_list_table
to wait_for_row_in_list_table
and delete time.sleep (3)
.
As a result, the current functional test looks like this:
# django-tdd/functional_tests/tests.py
from django.test import LiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import WebDriverException #add to
import time
MAX_WAIT = 10 #add to
class NewVisitorTest(LiveServerTestCase): #Change
def setUp(self):
self.browser = webdriver.Chrome()
def tearDown(self):
self.browser.quit()
def wait_for_row_in_list_table(self, row_text):
start_time = time.time()
while True:
try:
table = self.browser.find_element_by_id('id_list_table')
rows = table.find_elements_by_tag_name('tr')
self.assertIn(row_text, [row.text for row in rows])
return
except (AssertionError, WebDriverException) as e:
if time.time() - start_time > MAX_WAIT:
raise e
time.sleep(0.5)
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(self.live_server_url) #Change
#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)
self.wait_for_row_in_list_table('1: Buy dorayaki')
#The text box allows you to continue to fill in items, so
#Filled in "Billing Dorayaki Money"(He is tight on money)
inputbox = self.browser.find_element_by_id('id_new_item')
inputbox.send_keys("Demand payment for the dorayaki")
inputbox.send_keys(Keys.ENTER)
#The page was refreshed again and I was able to see that new items were added
self.wait_for_row_in_list_table('2: Demand payment for the dorayaki')
#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
self.fail("Finish the test!")
#When Nobita tried to access a specific URL that he had confirmed once,
#The item was saved so I was happy to fall asleep.
When I ran the functional test, it ended with self.fail ("Finish the test!") AssertionError: Finish the test!
.
Testing "Best practives" applied in this chapter
Here is a summary of best practices in Chapter 6.
--A test must not affect other tests. Django's test runner creates and deletes a database for testing, so we'll use it for functional testing as well.
--Avoid abuse of time.sleep () It is easy to put time.sleep () and have a buffer for loading, but depending on the processing and buffer, a meaningless error may occur. avoid.
--Do not use Selenium's waits function It seems that selenium has a function to have a buffer automatically, but "Explicit is better than implict" and Zen of Python have it. Explicit implementation is preferred.
Recommended Posts