Writing Unit Tests for Django Applications

The function of our blog is getting more and more perfect, but this also brings a problem, we dare not easily modify the code of existing functions!

How do we know that the code modification has the expected effect? If you make a mistake, not only the new functions are not useful, the existing functions may be destroyed. Previously, we developed a new function, which was manually run the development server to verify, which was not only time-consuming, but also very likely that the verification was insufficient.

How do you not have to manually verify each time you develop a new feature or modify an existing code? The solution is to write automated tests, write the logic of manual verification into a script, run the test script every time after adding or modifying code, and the script automatically helps us complete all the testing work.

Next we will perform two types of tests, one is unit testing and the other is integration testing.

Unit testing is a relatively low-level test. It treats a functional block of code as a unit (for example, a function, method, or an if statement block, etc. The unit should be as small as possible so that the test will be more adequate). The programmer writes test code to test the unit, ensuring that the logic of the unit executes as expected. Generally speaking, we generally treat a function or method as a unit and test it.

Integration testing is a higher-level test. From the perspective of the system, it tests whether a system composed of modules that have been fully unit tested has met its expectations.

We first perform unit tests to ensure that the logic of each unit is OK, and then perform integration tests to test the usability of the entire blog system.

Python generally uses the standard library unittest to provide unit tests. Django extends unit tests and provides a series of classes for different testing situations. Which is the most commonly used django.test.TestCase class, and the Python standard library unittest.TestCase similar, but expanded the following features:

  • A client property is provided, and this client is an instance of Client. You can think of Client as a function library (similar to requests) that makes HTTP requests, so we can easily use this class to test view functions.
  • The database is automatically created before the test is run, and the database is automatically destroyed after the test is run. We certainly don’t want the automatically generated test data to affect the real data.
    The unit testing of a blog application is mainly dealing with this class.

Unit tests for django applications include:

  • Test the model, the model’s methods return the expected data, and the database operation is correct.

  • Test form, data validation logic as expected

  • Test view, whether the expected response is returned for a particular type of request

  • Other helper methods or classes, etc.

Let’s test the above one by one.

Set up a test environment

The test is written in tests.py (this file is automatically created when the application is created). First, a smoke test is performed to verify that the test function is normal. Write the following code in the blog\tests.py file:

from django.test import TestCase


class SmokeTestCase(TestCase):
    def test_smoke(self):
        self.assertEqual(1 + 1, 2)

Using the test command of manage.py will automatically discover the tests files or modules under the django application, and automatically execute the methods starting with test_. run:pipenv run python manage.py test

Creating test database for alias ‘default’ …
System check identified no issues (0 silenced).

.


Ran 1 test in 0.002s

OK
Destroying test database for alias ‘default’ …

OK indicates that our test ran successfully.

However, if there is more code to be tested, and all the test logic is plugged into tests.py, this module will become very bloated and difficult to maintain, so we upgrade the tests.py file to a package with different unit tests. Write to the corresponding module under the package, which is convenient for modular maintenance and management.

Delete the blog\tests.py file, then create a tests package under the blog application, and then create each unit test module:

blog\
    tests\
        __init__.py
        test_smoke.py
        test_models.py
        test_views.py
        test_templatetags.py
        test_utils.py
  • test_models.py stores model-related unit tests
  • test_views.py test view functions
  • test_templatetags.py tests custom template tags
  • test_utils.py tests some helper methods and classes, etc.

Each module in the tests package must start with test_, otherwise django cannot detect the existence of these test files, and will not run the test cases inside.

Test model

There are not many models to test, because they basically use the features of the django base classes models.Model, and they have very little logic. Take the most complicated Post model as an example, it includes the following logical functions:

  • __str__ Method returns the character representation of the title used for the model instance
  • save Set article creation time (created_time) and abstract (exerpt) in the method
  • get_absolute_url Returns the URL path of the article details view
  • increase_views +1 the value of the views field

Unit testing is to test that these methods do return the expected results after execution. We add a new class in test_models.py called PostModelTestCase, and write the above unit test use cases in this class.

from django.apps import apps

class PostModelTestCase(TestCase):
    def setUp(self):
        # Disconnect signal from haystack, no need to generate index for test generated articles
        apps.get_app_config('haystack').signal_processor.teardown()
        user = User.objects.create_superuser(
            username='admin', 
            email='admin@hellogithub.com', 
            password='admin')
        cate = Category.objects.create(name='Test')
        self.post = Post.objects.create(
            title='Test title',
            body='Test content',
            category=cate,
            author=user,
        )

    def test_str_representation(self):
        self.assertEqual(self.post.__str__(), self.post.title)

    def test_auto_populate_modified_time(self):
        self.assertIsNotNone(self.post.modified_time)

        old_post_modified_time = self.post.modified_time
        self.post.body = 'New test content'
        self.post.save()
        self.post.refresh_from_db()
        self.assertTrue(self.post.modified_time > old_post_modified_time)

    def test_auto_populate_excerpt(self):
        self.assertIsNotNone(self.post.excerpt)
        self.assertTrue(0 < len(self.post.excerpt) <= 54)

    def test_get_absolute_url(self):
        expected_url = reverse('blog:detail', kwargs={'pk': self.post.pk})
        self.assertEqual(self.post.get_absolute_url(), expected_url)

    def test_increase_views(self):
        self.post.increase_views()
        self.post.refresh_from_db()
        self.assertEqual(self.post.views, 1)

        self.post.increase_views()
        self.post.refresh_from_db()
        self.assertEqual(self.post.views, 2)

Although there is a lot of code here, what it does is clear. setUp The method is executed before each test case is run. What is done here is to create an article in the database for testing.

The next individual test_ * way is to test each functional unit to test_auto_populate_modified_time post an example, here we want to test articles stored in the database, modifited_time it is set correctly value (expected value should be the time when the article save).

self.assertIsNotNone(self.post.modified_time) Asserts that the article’s modified_time is not empty, indicating that the value is indeed set. The TestCase class provides a series of assert * methods for asserting whether the logical result of the test unit is as expected. Generally, its function can be read from the method’s name. For example, assertIsNotNone is asserting that the value of the tested variable is not None.

Then we try to pass

self.post.body = 'New test content'
self.post.save()

Modify the content of the article and resave the database. The expected result should be that after the article is saved, modifited_time the value is also updated to the time when the article was modified. The next code is an assertion of this expected result:

self.post.refresh_from_db()
self.assertTrue(self.post.modified_time > old_post_modified_time)

This refresh_from_db method will refresh the target self.post date value is in the database, then we assert database modified_time record date later than the original time, if by assertions, indicating that we update articles modified_time of value have also been updated to record the modification time The result is as expected and the test passes.

The other test methods are doing similar things. I won’t explain them one by one here. Please look at the code analysis yourself.

Test view

The basic idea of the view function test is to make a request to the URL corresponding to a view. The view function is called and returns the expected response, including the correct HTTP response code and HTML content.

Our blog application includes the following types of views that need to be tested:

  • Home view IndexView, accessing it will return a list of all articles.
  • Tag view, accessing it will return a list of articles under a tag. If the visited tag does not exist, a 404 response is returned.
  • Category view, accessing it will return a list of articles in a category. If the visited category does not exist, a 404 response is returned.
  • Archive view, accessing it will return a list of all articles under a certain month.
  • Details view, accessing it will return the details of an article, or 404 if the article visited does not exist.
  • Custom admin, add articles automatically populated after the authorvalue of the field.
  • RSS, returns RSS content of all articles.

The home view, tab view, category view, and archive view are all the same type of view. Their expected behavior should be:

  • Returns the correct response code, 200 is returned successfully, and 404 is returned if it does not exist.
  • When there is no article, it correctly prompts that there is no article.
  • The correct html template is rendered.
  • Contains key template variables such as article lists, pagination variables, etc.

Let’s first test these views. In order to generate appropriate data for the test case, we first define a base class that predefines the data content of the blog. Other view function test cases inherit this base class, so there is no need to create data for each test. The test data we created is as follows:

  • Category I, Category II
  • Tag one, tag two
  • Article 1, belongs to category 1 and label 1, article 2, belongs to category 2, without label
class BlogDataTestCase(TestCase):
    def setUp(self):
        apps.get_app_config('haystack').signal_processor.teardown()

        # User
        self.user = User.objects.create_superuser(
            username='admin',
            email='admin@hellogithub.com',
            password='admin'
        )

        # classification
        self.cate1 = Category.objects.create(name='Test category one')
        self.cate2 = Category.objects.create(name='Test category two')

        # label
        self.tag1 = Tag.objects.create(name='Test label one')
        self.tag2 = Tag.objects.create(name='Test label two')

        # article
        self.post1 = Post.objects.create(
            title='Test title one',
            body='Test content one',
            category=self.cate1,
            author=self.user,
        )
        self.post1.tags.add(self.tag1)
        self.post1.save()

        self.post2 = Post.objects.create(
            title='Test title two',
            body='Test content two',
            category=self.cate2,
            author=self.user,
            created_time=timezone.now() - timedelta(days=100)
        )

With CategoryViewTestCase an example:

class CategoryViewTestCase(BlogDataTestCase):
    def setUp(self):
        super().setUp()
        self.url = reverse('blog:category', kwargs={'pk': self.cate1.pk})
        self.url2 = reverse('blog:category', kwargs={'pk': self.cate2.pk})

    def test_visit_a_nonexistent_category(self):
        url = reverse('blog:category', kwargs={'pk': 100})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_without_any_post(self):
        Post.objects.all().delete()
        response = self.client.get(self.url2)
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed('blog/index.html')
        self.assertContains(response, 'No articles published yet!')

    def test_with_posts(self):
        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed('blog/index.html')
        self.assertContains(response, self.post1.title)
        self.assertIn('post_list', response.context)
        self.assertIn('is_paginated', response.context)
        self.assertIn('page_obj', response.context)
        self.assertEqual(response.context['post_list'].count(), 1)
        expected_qs = self.cate1.post_set.all().order_by('-created_time')
        self.assertQuerysetEqual(response.context['post_list'], [repr(p) for p in expected_qs])

This class is inherited from the first BlogDataTestCase, setUp the method do not forget to call the parent class stepUp method, so that in each test case run, blog set up the test data.

Then three case tests were performed:

  • Visit a non-existent category and expect a 404 response code.
  • There is no article classification, return 200, but the prompt has not yet published articles! The rendered template is index.html
  • There is an article classification visit, the response should include a series of key variables template, post_list, is_paginated, page_obj, post_list the number of articles 1, because in our test data in this category, out of an article, post_list is a query set, is expected to be all the posts in that classification , Sort time in reverse order.

Other TagViewTestCase similar tests, etc., refer to code analysis itself.

The logic of the blog post details view is a bit more complicated, so there are more test cases. The main points to be tested are:

  • Visit non-existing article, return 404.
  • Each time an article is accessed, the views are increased by one.
  • The article content was rendered by markdown and a table of contents was generated.

The test code is as follows:

class PostDetailViewTestCase(BlogDataTestCase):
    def setUp(self):
        super().setUp()
        self.md_post = Post.objects.create(
            title='Markdown Test title',
            body='# title',
            category=self.cate1,
            author=self.user,
        )
        self.url = reverse('blog:detail', kwargs={'pk': self.md_post.pk})

    def test_good_view(self):
        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed('blog/detail.html')
        self.assertContains(response, self.md_post.title)
        self.assertIn('post', response.context)

    def test_visit_a_nonexistent_post(self):
        url = reverse('blog:detail', kwargs={'pk': 100})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_increase_views(self):
        self.client.get(self.url)
        self.md_post.refresh_from_db()
        self.assertEqual(self.md_post.views, 1)

        self.client.get(self.url)
        self.md_post.refresh_from_db()
        self.assertEqual(self.md_post.views, 2)

    def test_markdownify_post_body_and_set_toc(self):
        response = self.client.get(self.url)
        self.assertContains(response, 'Article Directory')
        self.assertContains(response, self.md_post.title)

        post_template_var = response.context['post']
        self.assertHTMLEqual(post_template_var.body_html, "<h1 id='Title '> Title</h1>")
        self.assertHTMLEqual(post_template_var.toc, '<li><a href="#Title "> title</li>')

The next step is to test the admin to add articles and rss subscription content. This one is relatively simple, because most of it is django logic. Django has already tested it for us. What we need to test is only the custom part to ensure that the custom logic follows The expected definition worked and got the expected results.

For admin, the expected result is that the author is indeed automatically populated after the article is published:

class AdminTestCase(BlogDataTestCase):
    def setUp(self):
        super().setUp()
        self.url = reverse('admin:blog_post_add')

    def test_set_author_after_publishing_the_post(self):
        data = {
            'title': 'Test title',
            'body': 'Test content',
            'category': self.cate1.pk,
        }
        self.client.login(username=self.user.username, password='admin')
        response = self.client.post(self.url, data=data)
        self.assertEqual(response.status_code, 302)

        post = Post.objects.all().latest('created_time')
        self.assertEqual(post.author, self.user)
        self.assertEqual(post.title, data.get('title'))
        self.assertEqual(post.category, self.cate1)
  • reverse('admin:blog_post_add') Get the URL of the blog post added by the admin management. The view function name of the django admin add article is the admin:blog_post_add naming rule of the view function of the general admin background operation model <app_label>_<model_name>_<action>.
  • self.client.login(username=self.user.username, password='admin') Login user, equivalent to log in to the administrator account in the background.
  • self.client.post(self.url, data=data) Initiate a post request to the URL where the article is added. The data of the post is the content of the article to be published. Only the title, body, and category are specified.
    Then we make a series of assertions to confirm that the article was created correctly.

The RSS test is similar. What we expect is that the content it returns does include the content of all articles:

class RSSTestCase(BlogDataTestCase):

    def setUp(self):
        super().setUp()
        self.url = reverse('rss')

    def test_rss_subscription_content(self):
        response = self.client.get(self.url)
        self.assertContains(response, AllPostsRssFeed.title)
        self.assertContains(response, AllPostsRssFeed.description)
        self.assertContains(response, self.post1.title)
        self.assertContains(response, self.post2.title)
        self.assertContains(response, '[%s] %s' % (self.post1.category, self.post1.title))
        self.assertContains(response, '[%s] %s' % (self.post2.category, self.post2.title))
        self.assertContains(response, self.post1.body)
        self.assertContains(response, self.post2.body)

Test template tags

The core of this test is that the template {% templatetag %} is rendered into the correct HTML content. You can see the corresponding code in the test code:

context = Context(show_recent_posts(self.ctx))
template = Template(
    '{% load blog_extras %}'
    '{% show_recent_posts %}'
)
expected_html = template.render(context)

Note that the template tag is essentially a Python function. In the first sentence of the code, we called this function directly. Since it needs to accept a scalar of type Context, we construct an empty context for it. Calling it will return the required context. Variable, and then we construct a required context variable.

Then we constructed a template object.

Finally we rendered this template using the constructed context.

We called the template engine’s underlying API to render the template, and the view function would render the template and return the response, but we didn’t see this process because Django helped us call this process behind us.

The test routines of all template engines are the same. Construct the required context, construct the template, use the context rendering template, and assert that the rendered template content is as expected. for example:

def test_show_recent_posts_with_posts(self):
    post = Post.objects.create(
        title='Test title',
        body='Test content',
        category=self.cate,
        author=self.user,
    )
    context = Context(show_recent_posts(self.ctx))
    template = Template(
        '{% load blog_extras %}'
        '{% show_recent_posts %}'
    )
    expected_html = template.render(context)
    self.assertInHTML('<h3 class="widget-title">latest articles</h3>', expected_html)
    self.assertInHTML('<a href="{}">{}</a>'.format(post.get_absolute_url(), post.title), expected_html)

This template tag corresponds to the latest article section in the sidebar. We made 2 key content assertions. One contains the title of the latest article section, and the other contains a hyperlink to the article title in the content.

Test helper methods and classes

There is only one logic for keyword highlighting in our blog.

class HighlighterTestCase(TestCase):
    def test_highlight(self):
        document = "This is a long title, used to test keyword highlighting but not truncated."
        highlighter = Highlighter("title")
        expected = 'This is a long one<span class="highlighted">title</span>,Used to test keyword highlighting without being truncated.'
        self.assertEqual(highlighter.highlight(document), expected)

        highlighter = Highlighter("Keyword highlighting")
        expected = 'This is a longer title for testing<span class="highlighted">Keyword highlighting</span>But not truncated.'
        self.assertEqual(highlighter.highlight(document), expected)

Here the Highlighter receives the search keywords as parameters when instantiated, and then Highlight wraps the keywords in the search results with span tags.

Highlighter In fact the class that haystack provides us, we just define the logic of the highlight method. How do we know the logic of the highlight method? How to test it?

I was looking at the source code and had a rough idea of the implementation logic of the Highlighter class. Then I found the highlight test method from the haystack test case.

Therefore, sometimes don’t be afraid to look at the source code. Everything in the Python world is open source. There is no mystery in the source code. It is written by people. Others can write it, and you can write it after you learn. The unit test code is generally verbose and repetitive, but the purpose is also very clear, and most of it is organized in sequential logic. The code is self-documented and very easy to read.

You may still be confused just by looking at the explanations in the article, but read the source code of the test part of the sample project carefully, you will definitely have a clearer understanding of the unit test, and then draw a scoop and write your own project code Unit tests.

#python #django

Writing Unit Tests for Django Applications
26.95 GEEK