This is a material for study sessions. This time, I will explain various things about Test.
Original tutorial (explained as tutorial 5 in the original) https://docs.djangoproject.com/ja/1.9/intro/tutorial05/
Source → 0b1bcccc1da95d274cd92253fa44af7a84c51404
It may not be very relevant for a program that you write a little as a hobby, but For programs that are being refurbished little by little over a long period of time, writing a good test will make you happy.
Talk about the purpose of the test, why it is necessary, and how useful it is in the original tutorial and [Qiita article](http://qiita.com/search?q=%E3%83%86%E3%82%B9% Please check with E3% 83% 88).
In python, unittest is prepared as a standard library. django provides an extended TestCase class and execution commands.
The code for test is written in tests.py
created when the application was created.
In the tutorial, I created a polls app with the command $ python manage.py startapp polls
.
At that time, a file called tests.py
should be created together with views.py
and models.py
.
./manage.py
├ tutorial/ #Location of configuration files, etc.
│ ├ ...
│ └ settings.py
└ polls/ # ← "manage.py startapp polls"Directory for apps created in
├ models.py
├ views.py
├ ...
└ tests.py #← This should also be created automatically. I will write a test here
When you open tests.py, you should see the following state by default.
tests.py
from django.test import TestCase
# Create your tests here.
If you add a method that starts with test
to a class that inherits TestCase, django will automatically collect and execute the method.
Please note that it will not be executed unless the following three conditions are met.
--Create a file under the application that starts with test
--Create a class that inherits django.test.TestCase
--Start method name with test
tests.py
from django.test import TestCase
class PollsTest(TestCase):
def test_hoge(self):
print('test!')
$ ./manage.py test
Creating test database for alias 'default'...
test!
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
Testing is usually the task of checking if the return value of a function or method produces the expected results. python's TestCase provides some assert methods for comparison, and if the result is not as expected, Test will fail. The table shows four typical ones.
Method | Checklist |
---|---|
assertEqual(a, b) | a == b |
assertNotEqual(a, b) | a != b |
assertTrue(x) | bool(x) is True |
assertFalse(x) | bool(x) is False |
If you remember the above four, you can cover almost all cases, but there are some assert methods that are useful as shortcuts. You may want to read the official website once. http://docs.python.jp/3/library/unittest.html#assert-methods
For example, code that tests for an exception when executing a function is easier (and easier to understand) to write using ʻassertRaises than using ʻassertTrue
.
class PollsTest(TestCase):
def test_exception(self):
try:
func()
except Exception:
pass
else:
self.assertTrue(False) #Always fail if no exception occurs
def test_exception2(self):
self.assertRaises(Exception, func)
Source → 4301169d6eb1a06e01d93282fbaab0f1fc2c367e
Let's write a test using ʻassertEqual and ʻassertNotEqual
.
As written in the table, the above two are comparisons with ==
and ! =
, So 1 and True and 0 and False are the same.
If you want to compare the types clearly, use ʻassertIs or ʻassertIsNone
.
It is necessary to devise something like ʻassertTrue (bool (1)). (It seems that ʻassertIs
and ʻassertIsNone` are not in the python2 series)
polls/tests.py
from django.test import TestCase
class PollsTest(TestCase):
def test_success(self):
self.assertEqual(1, 1)
self.assertEqual(1, True)
def test_failed(self):
self.assertNotEqual(0, False)
$ ./manage.py test
Creating test database for alias 'default'...
F.
======================================================================
FAIL: test_failed (polls.tests.PollsTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/shimomura/pbox/work/tutorial/tutorial/polls/tests.py", line 10, in test_failed
self.assertNotEqual(0, False)
AssertionError: 0 == False
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...
If the test succeeds, .
is displayed, and if it fails, F
is displayed together with where it failed.
Since the number of tests = the number of methods, no matter how many times you call assert within the same method, the number of tests will be counted as one.
It is easier to understand which test failed if you write the test in detail, but the test time will increase due to initialization etc.
Source → c2c65afcb0d26913f683cb7b64f388925d7896eb
Now that you know how to compare the values, let's actually import the model and test if the method works correctly. That said, there is only one method-like method in the Question model so far ...
Since the test target is a method of Question model, it is necessary to create an instance of Question model in the test code.
polls/models.py (method to be tested)
class Question(models.Model):
...
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
polls/tests.py (test code)
from django.test import TestCase
from django.utils import timezone
from .models import Question
class PollsTest(TestCase):
def test_was_published_recently(self):
obj = Question(pub_date=timezone.now())
self.assertTrue(obj.was_published_recently())
$ ./manage.py test
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
It is like this.
Source → 1062f1b4362ef2bf4e9ea483b5e178e7ca82c0c6
It is lonely that the number of test executions for one method is one, so let's increase the pattern a little more.
By the way, the test is to check whether the method is working as intended in the first place.
The intended behavior of was_published_recently
is to determine if a ** question ** has been published recently.
** Recently ** is here within one day from the present.
There are several testing methods, but in the case of such a method, it is common to give values before and after the condition and check how it is judged.
(Boundary value analysis, boundary value test, etc. are called like that)
It looks like this in the figure.
So let's add a test to check this condition.
In Honke Tutorial, a test is added as a separate method, but this time, test_was_published_recently
is extended for the time being. To do.
① and ②, ③ and ④ should be as close as possible, but this time it is time, so I will loosen it a little.
polls/tests.py
from datetime import timedelta
from django.test import TestCase
from django.utils import timezone
from .models import Question
class PollsTest(TestCase):
def test_was_published_recently(self):
#A little older than a day
obj = Question(pub_date=timezone.now() - timedelta(days=1, minutes=1))
self.assertFalse(obj.was_published_recently())
#A little newer than a day
obj = Question(pub_date=timezone.now() - timedelta(days=1) + timedelta(minutes=1))
self.assertTrue(obj.was_published_recently())
#Just recently released
obj = Question(pub_date=timezone.now() - timedelta(minutes=1))
self.assertTrue(obj.was_published_recently())
#Published soon
obj = Question(pub_date=timezone.now() + timedelta(minutes=1))
self.assertFalse(obj.was_published_recently())
$ ./manage.py test
Creating test database for alias 'default'...
F
======================================================================
FAIL: test_was_published_recently (polls.tests.PollsTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/shimomura/pbox/work/tutorial/tutorial/polls/tests.py", line 25, in test_was_published_recently
self.assertFalse(obj.was_published_recently())
AssertionError: True is not false
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...
At the moment, the was_published_recently
method has no future judgment, so the test in (4) fails.
In this way, even if you know that the test failed (= FAIL: test_was_published_recently (polls.tests.PollsTest)
) if you wrote the methods without dividing them.
The reason for failure (1), (2), (3), or (4) can only be determined from the line number.
However, since you can attach a message to ʻassert`, it will be a little easier to understand if you set a message.
polls/tests.py (add message to assert)
...
def test_was_published_recently(self):
#A little older than a day
obj = Question(pub_date=timezone.now() - timedelta(days=1, minutes=1))
self.assertFalse(obj.was_published_recently(), 'Published 1 day and 1 minute ago')
#A little newer than a day
obj = Question(pub_date=timezone.now() - timedelta(days=1) + timedelta(minutes=1))
self.assertTrue(obj.was_published_recently(), 'Published in 1 day and 1 minute')
#Just recently released
obj = Question(pub_date=timezone.now() - timedelta(minutes=1))
self.assertTrue(obj.was_published_recently(), 'Published 1 minute ago')
#Published soon
obj = Question(pub_date=timezone.now() + timedelta(minutes=1))
self.assertFalse(obj.was_published_recently(), 'Published in 1 minute')
$ ./manage.py test
Creating test database for alias 'default'...
F
======================================================================
FAIL: test_was_published_recently (polls.tests.PollsTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/shimomura/pbox/work/tutorial/tutorial/polls/tests.py", line 25, in test_was_published_recently
self.assertFalse(obj.was_published_recently(), 'Published in 1 minute')
AssertionError: True is not false :Published in 1 minute
----------------------------------------------------------------------
Adding a message made it a little easier to understand. This method is also not perfect. For example, if the judgment of ① fails, the test ends there, so it is not possible to confirm whether ② to ④ work normally. However, it can be said that it is not necessary to check (2) to (4) because it is necessary to correct it no matter how the other works when even one test fails ...
In any case, the test revealed that there was no future judgment, so let's fix it immediately.
polls/models.py
...
class Question(models.Model):
...
def was_published_recently(self):
return timezone.now() >= self.pub_date >= timezone.now() - datetime.timedelta(days=1)
$ ./manage.py test
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
You have now modified the method to work as intended.
In the model test, we checked the operation of the defined method logic. In the view test, when the user actually displays the page,
− Can the page be displayed normally? -Is the displayed item correct?
Will be checked.
django provides a test Client
class that simulates browser access.
First, let's write a test for the polls index screen.
In TestCase
, you can access the Client
class with self.client
.
Source → 852b862423f9908480e60c5657ee028782530933
Can the page be displayed normally? To check
, check status_code.
I think the status code that you often see when you touch django is as follows.
→ [Status Code (wikipedia)](https://ja.wikipedia.org/wiki/HTTP%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82 % B9% E3% 82% B3% E3% 83% BC% E3% 83% 89)
code | meaning |
---|---|
200 | Successful completion |
400 | Parameter error. For example, if the value passed to the Form is incorrect |
401 | Accessed without logging in to a page that requires authentication |
404 | No page |
500 | Internal error. This is if the python code is strange |
If it is in the 200s, it operates normally, if it is in the 400s, it is an error due to an operation error on the client side, Remember that the 500s are errors due to programs or servers. This time it is confirmed that the page can be displayed normally, so confirm that response.status_code is 200.
polls/tests.py
...
from django.shortcuts import resolve_url
...
class ViewTest(TestCase):
def test_index(self):
response = self.client.get(resolve_url('polls:index'))
self.assertEqual(200, response.status_code)
This time, I used
self.client.get
because it is a page display (access by GET method), but When testing Submit such as Form, useself.client.post
because it is a POST method. You need to pass the url as the first argument.
Source → d1ad02228ca2cb2370b323fd98cd27a0cf02d7a9
If the status_code is ** 200 **, it means that the page is displayed when viewed with a browser for the time being. Next, let's check if the displayed contents are as intended.
In the original tutorial, ʻassertContainsis used to check the contents of the html that is actually displayed. For example
self.assertContains(response, "No polls are available.") If you write like, make sure that
No polls are available.` is included in the displayed html source code.
Since response
, which is the return value of self.client.get
, has the context used for rendering html,
By checking the contents of this context, you can check whether the dynamically generated part has the intended value.
polls/views.py
...
def index(request):
return render(request, 'polls/index.html', {
'questions': Question.objects.all(),
})
...
The above is the view function to be tested, but the dictionary of the third argument of the render function ({'questions': Question.objects.all ()}
)
Is the context passed to the template.
When executing the test, a DB for the test is generated, and the DB is recreated for each test method. Therefore, even if you write a test that operates the DB, it will not be affected by the production DB or other tests. The index function in views.py gets all the contents of the Question object, As mentioned above, the contents of the DB should be empty, so let's write a test to check if it is actually empty.
The content of context ['questions'] is a QuerySet for the Question model. The number of Question table is acquired by calling
count ()
from QuerySet.
polls/tests.py
...
class ViewTest(TestCase):
def test_index(self):
response = self.client.get(resolve_url('polls:index'))
self.assertEqual(200, response.status_code)
self.assertEqual(0, response.context['questions'].count())
If the test passes successfully, let's register the data and check the change in the number of cases and the contents.
polls/tests.py
class ViewTest(TestCase):
def test_index(self):
response = self.client.get(resolve_url('polls:index'))
self.assertEqual(200, response.status_code)
self.assertEqual(0, response.context['questions'].count())
Question.objects.create(
question_text='aaa',
pub_date=timezone.now(),
)
response = self.client.get(resolve_url('polls:index'))
self.assertEqual(1, response.context['questions'].count())
self.assertEqual('aaa', response.context['questions'].first().question_text)
mock
There is no such program at the moment, but depending on the application, there will naturally be programs that link with the outside using APIs. To test programs that require communication with the outside world, python provides a mock library. By replacing the part where communication with the outside occurs with mock, you can replace the return value of API with your favorite value.
Let's replace the function that performs api communication with the function for dummy.
Use ʻunittest.mock.patchfor replacement. You can enclose the call in
with, or you can set
decorator` in the test method itself.
from unittest import mock
def dummy_api_func():
return 'dummy api response'
def api_func():
return 'api response'
class PollsTest(TestCase):
def test_mocked_api(self):
ret = api_func()
print('ret:', ret)
with mock.patch('polls.tests.api_func', dummy_api_func):
ret = api_func() #←←←←←←←← This call becomes dummy
print('mocked_ret:', ret)
@mock.patch('polls.tests.api_func', dummy_api_func)
def test_mocked_api_with_decorator(self):
ret = api_func() #If you use decorator, this will also be dummy
print('decorator:', ret)
$ ./manage.py testCreating test database for alias 'default'...
ret: api response
mocked_ret: dummy api response #← Apply patch with with
.decorator: dummy api response #← Apply patch with decorator
In the next tutorial, we will explain the supplementary explanation about Model and the operation from shell. To the next tutorial
Recommended Posts