By default, Django hides errors like non-existent template variables in templates. One way to un-hide them is to install the django-fastdev module.

After installing fastdev, you'll run into errors on startup. Try firing up runserver now that new fatal errors can interrupt app startup.

Next, you'll want to install the coverage python plugin. This allows you to assess test coverage across any Python project. The coverage plugin will assess coverage for most statements, but it doesn't know how to check Django templates. For that, you'll need to get an additional module,  django-coverage-plugin. To get it working in VScode, I needed to add an environment variable to my .zshrc file. This tells the plugin where your django settings.py file is located.

export DJANGO_SETTINGS_MODULE=django_project.settings

The coverage module works hand-in-hand with unittest (probably pytest and nose too). These are the three most useful CLI commands I run. Check the docs for more info.

coverage run -m unittest discover
coverage report -m --omit 'tests/*' --skip-empty --skip-covered
coverage html --skip-empty --omit tests/tests_isolated -d blog/templates/htmlcov

The first command runs unittest and logs results to a .coverage file used to generate subsequent reports.

The second command generates a report right in the terminal with code coverage. I've added additional parameters.

  • omit 'tests/* ...I don't need coverage for my test files. I guess that begs a philosophical question...should unit tests be unit tested? Oh boy...
  • skip-empty ...Skips any file that has nothing in it. Otherwise, there will be __init__.py files in the report. Gross.
  • skip-covered ...It feels good to have 100% coverage, but it's not very useful. This removes files from the report if they already have 100% code coverage.

The final command, coverage html, generates an html report to send to your colleagues!

Code Coverage for templates

Now that we have a handle on where coverage is lacking, we'll want to test those templates! We don't test templates directly. We test them along with our views. In order to get coverage,  write your view tests so that each statement in a template evaluates.

My test_views.py file, contains a function called test_category_view( ). At first, I was missing coverage in my templates because it wasn't evaluating all the pagination logic. To get more coverage, I needed to make sure there were enough posts to fill several pages besides fetching those pages in order to make sure I hit all the pagination logic contained in my template.

# Paginated list appears when there are many posts
create_several_posts(self.category1.name, self.super_user, 20)
response = self.client.get(self.category_url)
self.assertTrue(response.context['is_paginated'])
self.assertEqual(response.context['posts'].count(), 5) # 5 per page

# Paginated list works when user has moved forward at least one page
response = self.client.get(self.category_url, {'page': 2})
self.assertTrue(response.context['page_obj'].has_previous())

The create_several_posts function ensures 20 posts are created before the test continues. You'll have to keep playing with your test_views file to get more and more template coverage. Eventually, coverage will look like this:

Name                                                                Stmts   Miss  Cover   Missing
-------------------------------------------------------------------------------------------------
django_project/blog/admin.py                                           11      0   100%
django_project/blog/apps.py                                             3      0   100%
django_project/blog/forms.py                                           13      0   100%
django_project/blog/models.py                                          64      0   100%
django_project/blog/templates/blog/add_comment.html                    12      0   100%
django_project/blog/templates/blog/add_post.html                       16      0   100%
django_project/blog/templates/blog/categories.html                      8      0   100%
django_project/blog/templates/blog/edit_post.html                      12      0   100%
django_project/blog/templates/blog/home.html                           14      0   100%
django_project/blog/templates/blog/parts/about_me.html                 28      0   100%
django_project/blog/templates/blog/parts/base.html                     70      0   100%
django_project/blog/templates/blog/parts/footer.html                   18      0   100%
django_project/blog/templates/blog/parts/header.html                   61      0   100%
django_project/blog/templates/blog/parts/kofi_donation.html             1      0   100%
django_project/blog/templates/blog/parts/mailchimp_embed.html          37      0   100%
django_project/blog/templates/blog/parts/pagination.html               20      0   100%
django_project/blog/templates/blog/parts/posts.html                    16      0   100%
django_project/blog/templates/blog/parts/sidebar.html                  13      0   100%
django_project/blog/templates/blog/pgp-key.txt                         98      0   100%
django_project/blog/templates/blog/post_confirm_delete.html            15      0   100%
django_project/blog/templates/blog/post_detail.html                    63      0   100%
django_project/blog/templates/blog/roadmap.html                        34      0   100%
django_project/blog/templates/blog/search_posts.html                   15      0   100%
django_project/blog/templates/blog/security.txt                         5      0   100%
django_project/blog/templates/blog/user_posts.html                      3      0   100%
django_project/blog/templates/blog/works_cited.html                    11      0   100%
django_project/blog/urls.py                                             7      0   100%
django_project/blog/utils.py                                           19      0   100%
django_project/blog/views.py                                          171      0   100%
django_project/django_project/settings.py                              65      0   100%
django_project/django_project/sitemaps.py                              17      0   100%
django_project/django_project/urls.py                                  12      0   100%
django_project/users/admin.py                                           3      0   100%
django_project/users/apps.py                                            5      0   100%
django_project/users/forms.py                                          23      0   100%
django_project/users/models.py                                         18      0   100%
django_project/users/signals.py                                        11      0   100%
django_project/users/templates/users/login.html                        22      0   100%
django_project/users/templates/users/logout.html                        7      0   100%
django_project/users/templates/users/password_reset.html               13      0   100%
django_project/users/templates/users/password_reset_complete.html       5      0   100%
django_project/users/templates/users/password_reset_done.html           4      0   100%
django_project/users/templates/users/profile.html                      21      0   100%
django_project/users/templates/users/register.html                     20      0   100%
django_project/users/views.py                                          67      0   100%
-------------------------------------------------------------------------------------------------
TOTAL                                                                1171      0   100%

Good luck testing your templates!

Comments

Back to Home
John Solly Profile Picture
John Solly Profile Picture

John Solly

Hi, I'm John, a Software Engineer with a decade of experience building, deploying, and maintaining cloud-native geospatial solutions. I currently serve as a senior software engineer at New Light Technologies (NLT), where I work on a variety of infrastructure and application development projects.

Throughout my career, I've built applications on platforms like Esri and Mapbox while also leveraging open-source GIS technologies such as OpenLayers, GeoServer, and GDAL. This blog is where I share useful articles with the GeoDev community. Check out my portfolio to see my latest work!