[PYTHON] Django Tutorial (Blog App Creation) ④ --Unit Test

Last time, in Django Tutorial (Blog App Creation) ③ --Article List Display, a class-based general-purpose view was used to display a list of articles created from the management site. I used.

I'd like to add CRUD processing such as article creation, details, editing, and deletion in the app as it is, but let's hold back and include ** unit test **.

About Django testing

It's fun to add more features, but do you usually write tests?

Even those who have become able to create simple Django apps through various tutorials, etc. I think that it may cause an error when you play with it a little. Also, even if no error is output when Django is started with runserver etc. You may notice an error when you actually move the screen via a browser.

Of course, you can manually test a few operations, but it's wasteful to do that every time.

Therefore, we recommend that you use Django's features to perform unit tests. Django allows you to automate tests using the UnitTest class, so You don't have to do the same thing over and over again if you write only the test code first.

Thinking about testing is as important as thinking about development code, There is even a development method of creating a test and then writing the code for the operation of the application.

Now that you can test, save your testing time and spend your time improving the app itself.

About folder structure

At this point, the folder structure should be as follows.

.
├── blog
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py #Attention
│   ├── urls.py
│   └── views.py
├── db.sqlite3
├── manage.py
├── mysite
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── templates
    └── blog
        ├── index.html
        └── post_list.html

As you may have noticed, a file called ** tests.py ** is automatically created under the blog directory.

You can create test cases directly in this tests.py, It is easier to manage if the files are separated for each model test, view test and test. Create a tests directory as shown below, and create an empty file in each. The point is to create a ** __ init__.py ** file from the contents so that the files in the tests directory are also executed.

.
├── blog
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   ├── models.py
│   ├── tests #add to
│   │   ├── __init__.py
│   │   ├── test_models.py
│   │   ├── test_urls.py
│   │   └── test_views.py
......

Please note that Django will not recognize the module name unless you start it with "test".

How to write a test

Django extends the Python standard TestCase class (unittest.TestCase), Use Django's own TestCase class (django.test.TestCase). In this class, you can use a method called assertion, which has a function to check whether the return value is the expected value.

Also, as mentioned above, the test module must start with the string "test". The test method must also start with the string "test" (more on this later).

By following this rule, Django will find the test method in your project and execute it automatically.

test_models.py Let's start by testing the model. As a reminder, the Post model described in blog / models.py looks like this.

models.py


...

class Post(models.Model):
    title = models.CharField('title', max_length=200)
    text = models.TextField('Text')
    date = models.DateTimeField('date', default=timezone.now)

    def __str__(self): #Defines the value to return when the Post model is called directly
        return self.title #Returns the article title

Let's test this model with the following three cases this time.

  1. Nothing is registered in the initial state
  2. If you create one record properly, only one record will be counted.
  3. When you save the data by specifying the contents and immediately retrieve it, the same value as when you saved it is returned.

Let's start with the first one.

Open test_models.py and declare the required modules.

test_models.py


from django.test import TestCase
from blog.models import Post

Then, we will create a test class, but be sure to make it a class that inherits TestCase.

test_models.py


from django.test import TestCase
from blog.models import Post

class PostModelTests(TestCase):

Now, let's write a test method in this PostModelTest class. By starting with "test" in a class that inherits TestCase, Django will automatically recognize that it's a test method. Therefore, be sure to name the method starting with test after def.

test_models.py


from django.test import TestCase
from blog.models import Post

class PostModelTests(TestCase):

  def test_is_empty(self):
      """Check that nothing is registered in the initial state"""  
      saved_posts = Post.objects.all()
      self.assertEqual(saved_posts.count(), 0)

Store the current Post model in saved_posts I have confirmed that the count number (number of articles) is "0" with assertEqual.

Now you are ready to do one test. Let's run it once with this.

To run the test, execute the following command in the directory (in mysite) where manage.py is located. When you run it, Django will find and execute a test method that follows the naming convention.

(blog) bash-3.2$ python3 manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 1 tests in 0.009s

OK

It means that you ran one test and completed it without any errors.

By the way, I confirmed that the data is empty (= 0) in Post earlier, but let's expect that there is one data.

test_models.py(temporary)


from django.test import TestCase
from blog.models import Post

class PostModelTests(TestCase):

  def test_is_empty(self):
      """Initial state but one is to check if the data exists(error is expected)"""  
      saved_posts = Post.objects.all()
      self.assertEqual(saved_posts.count(), 1)

The test execution result at this time is as follows.

(blog) bash-3.2$ python3 manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_is_empty (blog.tests.test_models.PostModelTests)
Initial state but one is to check if the data exists(error is expected)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/masuyama/workspace/MyPython/MyDjango/blog/mysite/blog/tests/test_models.py", line 9, in test_is_empty
    self.assertEqual(saved_posts.count(), 1)
AssertionError: 0 != 1

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (failures=1)

You're getting an AssertionError and the test is failing because it's not what you expected (experimentally successful).

In Django's tests, you can also temporarily register data in the database from the create method, so You can also run the rest of the tests that you wouldn't be able to see without registering the data. Below is how to write a model test, so please refer to it.

test_models.py(Full text)


from django.test import TestCase
from blog.models import Post

class PostModelTests(TestCase):

  def test_is_empty(self):
    """Check that nothing is registered in the initial state"""  
    saved_posts = Post.objects.all()
    self.assertEqual(saved_posts.count(), 0)
  
  def test_is_count_one(self):
    """Test that if you create one record properly, only one record will be counted"""
    post = Post(title='test_title', text='test_text')
    post.save()
    saved_posts = Post.objects.all()
    self.assertEqual(saved_posts.count(), 1)

  def test_saving_and_retrieving_post(self):
    """Save the data with the specified content, and test that when you retrieve it immediately, the same value as when you saved it is returned."""
    post = Post()
    title = 'test_title_to_retrieve'
    text = 'test_text_to_retrieve'
    post.title = title
    post.text = text
    post.save()

    saved_posts = Post.objects.all()
    actual_post = saved_posts[0]

    self.assertEqual(actual_post.title, title)
    self.assertEqual(actual_post.text, text)

test_urls.py Besides model, you can also check if the routing written in urls.py is working. Looking back, blog / urls.py looks like this.

blog/urls.py


from django.urls import path
from . import views

app_name = 'blog'

urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('list', views.PostListView.as_view(), name='list'),
]

In the above routing, the routing is set according to the address entered under / blog /, so / blog / Test when the following are'' (blank) and'list'. Use assertEqual to compare and check if the result redirected to each page via view is what you expect.

test_urls.py


from django.test import TestCase
from django.urls import reverse, resolve
from ..views import IndexView, PostListView

class TestUrls(TestCase):

  """Test redirect when accessing by URL to index page"""
  def test_post_index_url(self):
    view = resolve('/blog/')
    self.assertEqual(view.func.view_class, IndexView)

  """Test redirect to Post list page"""
  def test_post_list_url(self):
    view = resolve('/blog/list')
    self.assertEqual(view.func.view_class, PostListView)

Now let's run the test once.

(blog) bash-3.2$ python3 manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.007s

OK
Destroying test database for alias 'default'...

test_views.py Finally, let's test the view.

views.py looked like this.

views.py


from django.views import generic
from .models import Post  #Import Post model

class IndexView(generic.TemplateView):
    template_name = 'blog/index.html'

class PostListView(generic.ListView): #Inherit the generic ListView class
    model = Post #Call the model you want to list

The IndexView test confirms that the status code 200 (= success) is returned when accessed by the GET method.

test_views.py


from django.test import TestCase
from django.urls import reverse

from ..models import Post

class IndexTests(TestCase):
  """IndexView test class"""

  def test_get(self):
    """Confirm that it is accessed by the GET method and status code 200 is returned."""
    response = self.client.get(reverse('blog:index'))
    self.assertEqual(response.status_code, 200)

When you add a method in any view, No matter how much time you have to write a test, get in the habit of creating this as a minimum test case.

We will also test the ListView.

Not to mention confirming that the status code of 200 is returned as well. Here, after adding two data (articles), the article list is displayed and Create a test to make sure that each of the registered article titles is included in the list.

Note that we will use a slightly special method here. I mentioned earlier that the test method starts with "test", but there are methods ** setUp ** and ** tearDown **.

In the setUp method, register the data used in the test case and With the tearDown method, you can delete the data registered in the setUp method. (Note that both need to explicitly state what data to register)

Writing a process that registers data many times in the same test case is a factor that takes time and time for testing. The common process is to put them together in one place.

If you use these methods to create test_views.py, it will look like this.

test_views.py


from django.test import TestCase
from django.urls import reverse

from ..models import Post

class IndexTests(TestCase):
  """IndexView test class"""

  def test_get(self):
    """Confirm that it is accessed by the GET method and status code 200 is returned."""
    response = self.client.get(reverse('blog:index'))
    self.assertEqual(response.status_code, 200)

class PostListTests(TestCase):

  def setUp(self):
    """
A method for preparing the test environment. Be sure to name it "setUp".
If there is data that you want to use in common in the same test class, create it here.
    """
    post1 = Post.objects.create(title='title1', text='text1')
    post2 = Post.objects.create(title='title2', text='text2')

  def test_get(self):
    """Confirm that it is accessed by the GET method and status code 200 is returned."""
    response = self.client.get(reverse('blog:list'))
    self.assertEqual(response.status_code, 200)
  
  def test_get_2posts_by_list(self):
    """Confirm that the 2 additions added by the setUp method are returned when accessing with GET"""
    response = self.client.get(reverse('blog:list'))
    self.assertEqual(response.status_code, 200)
    self.assertQuerysetEqual(
      #In the Post model__str__Since it is set to return the title as a result of, check if the returned title is as posted
      response.context['post_list'],
      ['<Post: title1>', '<Post: title2>'],
      ordered = False #Specify to ignore the order
    )
    self.assertContains(response, 'title1') #Make sure the post1 title is included in the html
    self.assertContains(response, 'title2') #Make sure the post2 title is included in the html

  def tearDown(self):
      """
A cleaning method that erases the data added by setUp.
Although it is "create", by setting the method name to "tearDown", the reverse processing of setUp is performed = it is deleted.
      """
      post1 = Post.objects.create(title='title1', text='text1')
      post2 = Post.objects.create(title='title2', text='text2')

If you run the test in this state, a total of 8 tests will be run on model, url, and view.

(blog) bash-3.2$ python3 manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
........
----------------------------------------------------------------------
Ran 8 tests in 0.183s

OK
Destroying test database for alias 'default'...

You have now created a unit test for the code you have written so far. Whether other expected templates are called, etc. There is also a way to check redundantly with a test using Django's own test method. Get in the habit of writing tests before writing code and save yourself the trouble of checking later.

Next time, we will be able to create articles within the app. Let's go with the TDD (Test Driven Development) style of writing the unit tests we learned this time first.

Recommended Posts

Django Tutorial (Blog App Creation) ④ --Unit Test
Django Tutorial (Blog App Creation) ⑤ --Article Creation Function
Django Tutorial (Blog App Creation) ① --Preparation, Top Page Creation
Django Tutorial (Blog App Creation) ③ --Article List Display
Django Tutorial (Blog App Creation) ⑦ --Front End Complete
Django Tutorial (Blog App Creation) ⑥ --Article Details / Editing / Deleting Functions
Django tutorial (blog application creation) ② --model creation, management site preparation
Django tutorial summary for beginners by beginners ⑤ (test)
Python Django Tutorial (5)
Python Django Tutorial (2)
django table creation
numpy unit test
django tutorial memo
Django tutorial summary for beginners by beginners ① (project creation ~)
Start Django Tutorial 1
Python Django Tutorial (1)
Python Django tutorial tutorial
Python Django Tutorial (3)
Python Django Tutorial (4)
What is a dog? Django App Creation Start Volume--startapp
What is a dog? Django App Creation Start Volume--startproject
[Django] Added new question creation function to polls app
Write code to Unit Test a Python web app
Python Django tutorial summary
Launch my Django app
Django Polymorphic Associations Tutorial
Initialize your Django app
django oscar simple tutorial
Django shift creation feature
Django Girls Tutorial Note
Unit test Databricks Notebook
python unit test template