Python Django Tutorial (7)

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/

Other tutorials

Let's run the test

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'...

Make it a test-like test

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)

Value comparison

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.

model test

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.

Add test

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)

Kobito.1aoQFZ.png

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.

view test

Honke Tutorial

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.

Check the status code

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, use self.client.post because it is a POST method. You need to pass the url as the first argument.

Confirmation of contents

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 exampleself.assertContains(response, "No polls are available.") If you write like, make sure thatNo 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

python official

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.

Kobito.eoQ3cl.png

Let's replace the function that performs api communication with the function for dummy. Use ʻunittest.mock.patchfor replacement. You can enclose the call inwith, 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

Other tutorials

Recommended Posts

Python Django Tutorial (5)
Python Django Tutorial (2)
Python Django Tutorial (7)
Python Django Tutorial (1)
Python Django tutorial tutorial
Python Django Tutorial (3)
Python Django Tutorial (4)
Python Django tutorial summary
Python tutorial
Python Django Tutorial Cheat Sheet
Python tutorial summary
django tutorial memo
Start Django Tutorial 1
Django 1.11 started with Python3.6
[Docker] Tutorial (Python + php)
Django python web framework
Django Polymorphic Associations Tutorial
django oscar simple tutorial
Try Debian + Python 3.4 + django1.7 ...
[Personal notes] Python, Django
Python OpenCV tutorial memo
[Python tutorial] Data structure
Django Girls Tutorial Note
Cloud Run tutorial (python)
Python Django CSS reflected
Do Django with CodeStar (Python3.6.8, Django2.2.9)
Get started with Django! ~ Tutorial ⑤ ~
Introduction to Python Django (2) Win
[Python tutorial] Control structure tool
Python
Do Django with CodeStar (Python3.8, Django2.1.15)
Python3 + Django ~ Mac ~ with Apache
Create ToDo List [Python Django]
Getting Started with Python Django (1)
Django
Getting Started with Python Django (4)
Getting Started with Python Django (3)
Get started with Django! ~ Tutorial ⑥ ~
Install Python 3.7 and Django 3.0 (CentOS)
[Python] Decision Tree Personal Tutorial
Getting Started with Python Django (6)
Getting Started with Python Django (5)
Until Python [Django] de Web service is released [Tutorial, Part 1]
8 Frequently Used Commands in Python Django
Create new application use python, django
python + django + scikit-learn + mecab (1) on heroku
python + django + scikit-learn + mecab (2) on heroku
Django Girls Tutorial Summary First Half
Stumble when doing the django 1.7 tutorial
Deploy the Django tutorial to IIS ①
Install Python framework django using pip
Introduction to Python Django (2) Mac Edition
[Python Tutorial] An Easy Introduction to Python
Learning history for participating in team app development in Python ~ Django Tutorial 5 ~
Learning history for participating in team app development in Python ~ Django Tutorial 4 ~
Learning history for participating in team app development in Python ~ Django Tutorial 1, 2, 3 ~
Learning history for participating in team app development in Python ~ Django Tutorial 6 ~
Learning history for participating in team app development in Python ~ Django Tutorial 7 ~
Django Crispy Tutorial (Environment Building on Mac)
kafka python
django update