Testing Your Django App With Pytest

  • Viewed:

    674
  • Shared:

    0
Testing Your Django App With Pytest
dmytriy-chuvardynskyi

Dmytriy Chuvardynskyi

Python Developer @ Django Stars

Helps you write better programs.

— pytest

Many developers from Python community heard of and used unit testing to test their projects and knew about boilerplate code with Python and Django unittest module. But Pytest suggests much more pythonic tests without boilerplate.

Table of Contents

Why You Should Use Pytest?

Pytest provides a new approach for writing tests, namely, functional testing for applications and libraries. Below I’ll list some pros and cons about this framework.

Pros of Using Pytest:

Assert statements (no need to remember `self.assert*` names)
– Detailed info on failures
Fixtures (explicit, modular, scalable)
– Additional features of fixtures (auto-use, scope, request object, nested, finalizers, etc.)
Auto-discovery of test modules and functions
Marks
Parametrizing
– Less boilerplate code: just create file, write function with assert and run (simple is better than complex)
– No Camel сase as PyUnit
– Plugins with over 736+external plugins and thriving community
– Can run unittest and nose test suites out of the box
– Python 3.5+ and PyPy 3

Cons of Using Pytest:

– Requires a bit more advanced Python knowledge than using unittest, like decorators and simple generators
– The need for a separate installation of the module. But it can be an advantage as well, because you don’t depend on Python version. If you want new features, you just need to update pytest package.

Short Introduction to Pytest

First of all, I would like to make a small intro to pytest philosophy and base syntax. I will do it in the format of the most frequently asked questions and answers. This is a very short intro for pytest with basic functionality, but make sure to check it as we we will use it in the next parts.

Q1: What is Pytest Fixtures?

Fixtures are functions that run before and after each test, like setUp and tearDown in unitest and labelled pytest killer feature. Fixtures are used for data configuration, connection/disconnection of databases, calling extra actions, etc.

All fixtures have scope argument with available values:

  • function run once per test
  • class run once per class of tests
  • module run once per module
  • session run once per session

Note: Default value of scope is function

Example of simple fixture creation:

Another kind of fixture is yield fixture which provides access to test before and after the run, analogous to setUp and tearDown.

Example of simple yield fixture creation:

Note: normal fixtures can use yield directly so the yield_fixture decorator is no longer needed and considered deprecated.

Q2: How to use Fixtures with test in Pytest?

To use fixture in test, you can put fixture name as function argument:

Note: Pytest automatically register our fixtures and can have access to fixtures without extra imports.

Q3: What is Marks in Pytest?

Marks is a helper using which you can easily set metadata on your test functions, some builtin markers, for example:

  • skip – always skip a test function
  • xfail – produce an “expected failure” outcome if a certain condition is met

Example of used marks:

Q4: How to create custom Marks for Pytest?

The one way is to register your marks in pytest.ini file:

Note: Everything after the “:” is an optional description for mark

Q5: How to run test with Marks in Pytest?

You can run all tests xfail without slowing down marks with the next command:

Note: when the ‘–strict-markers’ command-line flag is passed, any unknown marks applied with the ‘@pytest.mark.name_of_the_mark’ decorator will trigger an error.

Q6: What is Parametrize in Pytest?

`Parametrize` is a builtin mark and one of the killer features of pytest. With this mark, you can perform multiple calls to the same test function.

Example of simple parametrize in test:

That’s pretty much it. Further we’ll work with these basics to set up pytest for your Django project.

Setting up Pytest for Django Project

For testing our Django applications with pytest we won’t reinvent the wheel and will use existing plugin pytest-django, that provides a set of useful tools for testing Django apps and projects. Let’s start with configuration plugin.

1. Installation

Pytest can be installed with pip

Installing pytest-django will also automatically install the latest version of pytest. pytest-django uses pytest’s plugin system and can be used right away after installation, there is nothing more to configure.

2. Point your Django settings to pytest

You need to tell pytest which Django settings should be used for test runs. The easiest way to achieve this is to create a pytest configuration file with this information.

Create a file called pytest.ini in your project root directory that contains:

You can also specify your Django settings by setting the DJANGO_SETTINGS_MODULE environment variable or specifying the --ds=yourproject.settings command-line flag when running the tests. See the full documentation on Configuring Django settings.

Optionally, also add the following line to the [pytest] section to instruct pytest to collect tests in Django’s default app layouts too.

3. Run your test suite

Tests are invoked directly with the pytest command, instead of manage.py test that you might be used to:

Specific test files or directories or single test can be selected by specifying the test file names directly on the command line:

Note: You may wonder “why would I use this instead of Django manage.py test command”? It’s easy. Running the test suite with pytest offers some features that are not present in Django standard test mechanism:

  • Less boilerplate: no need to import unittest, create a subclass with methods. Just write tests as regular functions.
  • Manage test dependencies with fixtures.
  • Run tests in multiple processes for increased speed.
  • There are a lot of other nice plugins available for pytest.
  • Easy switching: Existing unittest-style tests will still work without any modifications.

For now, we are configured and ready for writing first test with pytest and Django.

Django Testing with Pytest

1. Database Helpers

To gain access to the database pytest-django get django_db mark or request one of the db, transactional_db or django_db_reset_sequences fixtures.

Note: all these database access methods automatically use django.test.TestCase

django_db: to get access to the Django test database, each test will run in its own transaction that will be rolled back at the end of the test. Just like it happens in django.test.TestCase. We’ll use it constantly, because Django needs access to DB.

If you want to get access to the Django database inside a fixture this marker will not help even if the function requesting your fixture has this marker applied. To access the database in a fixture, the fixture itself will have to request the db, transactional_db or django_db_reset_sequences fixture. Below you may find a description of each one.

db: This fixture will ensure the Django database is set up. Only required for fixtures that want to use the database themselves. A test function should normally use the pytest.mark.django_db mark to signal it needs the database.

transactional_db: This fixture can be used to request access to the database including transaction support. This is only required for fixtures which need database access themselves. A test function should normally use the pytest.mark.django_db mark with transaction=True.

django_db_reset_sequences: This fixture provides the same transactional database access as transactional_db, with additional support for reset of auto increment sequences (if your database supports it). This is only required for fixtures which need database access themselves. A test function should normally use the pytest.mark.django_db mark with transaction=True and reset_sequences=True.

2. Client

The more frequently used thing in Django unit testing is django.test.client, because we use it for each request to our app, pytest-django has a build-in fixture client:

3. Admin Client

To get a view with superuser access, we can use admin_client, which gives us client with login superuser:

4. Create User Fixture

To create a user for our test we have two options:

1) Use Pytest Django Fixtures:

django_user_model: pytest-django helper for shortcut to the User model configured for use by the current Django project, like settings.AUTH_USER_MODEL

Cons of this option:

  • must be copied for each test
  • doesn’t allow to set difference fields, because fixture creates User instance instead of us

admin_user: pytest-django helper instance of a superuser, with username “admin” and password “password” (in case there is no “admin” user yet).

2) Create own Fixture:

To fix the disadvantages listed above we create our own custom fixture:

Note: Create user with call to local functions to pass extra arguments as kwargs, because pytest fixture can’t accept arguments.

Re-write tests above:

create_user: basic example how you can make own fixture, we can expand this fixture with two other for example, like create_base_user (for base user) and create_superuser (with fillable is_staff, is_superuser and etc.fields).

5. Auto Login Client

Let’s test some authenticated endpoint:

The major disadvantage of this method is that we must copy the login block for each test.

Let’s create our own fixture for auto login user:

auto_login_user: own fixture, that takes user as parameter or creates a new one and logins it to client fixture. And at the end it returns client and user back for the future actions.

Use our new fixture for the test above:

6. Parametrizing your tests with Pytest

Let’s say we must test very similar functionality, for example, different languages.

Previously, you had to do single tests, like:

It’s very funny to copy paste your test code, but not for a long time.

— Andrew Svetlov

To fix it, pytest has parametrizing fixtures feature. After upgrade we had next tests:

You can see how much easier and less boilerplate our code has become.

7. Test Mail Outbox with Pytest

For testing your mail outbox pytest-django has a built-in fixture mailoutbox:

For this test we use our own auto_login_user fixture and mailoutbox pytest built-in fixture.

To summarize the advantages of the approach demonstrated above: pytest teaches us how to setup our tests easily, so we could be more focused on testing main functionality.

Testing Django REST Framework with Pytest

1. API Client

The first thing to do here is to create your own fixture for API Client of REST Framework:

Now we have api_client for our tests:

2. Get or Create Token

For getting authorized, your API users usually use Token. Let’s create fixture to get or create token for a user:

get_or_create_token: inheritance create_user

3. Auto Credentials

The test demonstrated above is a good example, but setting credentials for each test will end up in a boilerplate code. And we can use other APIClient method to bypass authentication entirely.

We can use yield feature to extend new fixture:

api_client_with_credentials: inheritance create_user and api_client fixtures and also clear our credential after every test.

4. Data Validation with Pytest Parametrizing

Most tests for your API endpoint constitute and focus on data validation. You have to create the same tests without counting the difference in several values. We can use pytest parametrizing fixture for such solution:

By that mean, we test many cases with one test function thanks to this outstanding pytest feature.

5. Mock Extra Action in your Views

Let’s demonstrate how `unittest.mock` can be used with our test use-case. I’d rather use ‘unittest.mock’ than ‘monkeypatch’ fixture. Alternatively, you can use pytest-mock package as it has some useful built-in methods like: assert_called_once() , assert_called_with(*args,**kwargs) , assert_called() and assert_not_called() .

If you want to take a closer look at monkeypatch fixture you may check official documentation page.

For example, we have a third-party service call after we saved our data:

We want to test our endpoint without extra request to service and we can use mock.patch as decorator with Pytest test:

Useful Tips for Pytest

1. Using Factory Boy as fixtures for testing your Django model

There are several ways to create Django Model instance for test and example with fixture:

  • Create object manually  —  traditional variant: “create test data by hand and support it by hand”

If you want to add other fields like relation with Group, your fixture will get more complex and every new required field will change your fixture:

  • Django fixtures  —  slow and hard to maintain… avoid them!

Below I provide an example for comparison:

Create fixture that loads fixture data to your session:

  • Factories  —  a solution for creation of your test data in a simple way.
    I’d prefer to use pytest-factoryboy plugin and factoryboy alongside with Pytest.
    Alternatively, you may use model mommy.

1) Install the plugin:

2) Create User Factory:

3) Register Factory:

Note: Name convention is a lowercase-underscore class name

4) Test your Factory:

You may read more about pytest-factoryboy and factoryboy.

2. Improve your Parametrizing tests

Let’s improve parametrizing test above with some features:

pytest.param: pytest object for setting extra arguments like marks and ids

marks: argument for setting pytest mark

id: argument for setting unique indicator for test

success_request and bad_request: custom pytest marks

Let’s run our test with some condition:

As a result we have:

– Collected test with one of bad_request marks
– Ignore test without pytest.param object, because that don’t have marks parameters
– Show test with custom ID in console

3. Mocking your Pytest test with fixture

Using pytest-mock plugin is another way to mock your code with pytest approach of naming fixtures as parameters.

1) Install the plugin:

2) Re-write example above:

The mocker is a fixture that has the same API as mock.patch and supports the same methods as:

  • mocker.patch
  • mocker.patch.object
  • mocker.patch.multiple
  • mocker.patch.dict
  • mocker.stopall

4. Running tests simultaneously

To speed up your tests, you can run them simultaneously. This can result in significant speed improvements on multi core/multi CPU machines. It’s possible to realize with pytest-xdist plugin which expands pytest functionality

1) Install the plugin:

2) Running test with multiprocessing:

Notes:

– Avoid output and stdout executions in your tests, this will result in considerable speed-ups

– When tests are invoked with xdist, pytest-django will create a separate test database for each process. Each test database will be given a suffix (something like gw0, gw1) to map to a xdist process. If your database name is set to foo, the test database with xdist will be test_foo_gw0, test_foo_gw1, etc.

5. Config pytest.ini file

Example of pytest.ini file for your Django project:

DJANGO_SETTINGS_MODULE and python_files we discussed at the beginning of the article, let’s discover other useful options:

  • addopts
    Add the specified OPTS to the set of command-line arguments as if they had been specified by the user. We’ve specified next options:

--p no:warnings — disables warning capture entirely (this might be useful if your test suites handle warnings using an external system)

--strict-markers — typos and duplication in function markers are treated as an error

--no-migrations will disable Django migrations and create the database by inspecting all models. It may be faster when there are several migrations to run in the database setup.

--reuse-db reuses the testing database between test runs. It provides much faster startup time for tests.

Exemplary workflow with --reuse-db and --create-db:

–  run tests with pytest; on the first run the test database will be created. On the next test run it will be reused.

– when you alter your database schema, run pytest --create-db to force re-creation of the test database.

  • norecursedirs
    Set the exclusion of directory basename patterns when recursing for test discovery. This will tell pytest not to look into venv and old_testsdirectory

Note: Default patterns are '.*', 'build', 'dist', 'CVS', '_darcs', '{arch}', '*.egg', 'venv'

  • markers
    You can list additional markers in this setting to add them to the whitelist and use them in your tests.

Run all tests with mark slow:

6. Show your coverage of the test

To check coverage of your app you can use pytest-cov plugin

1) Install plugin:

2) Coverage of your project and example of report:

To wrap up, using this guide now you can avoid boilerplate code in your tests and make them smoother with Pytest. I hope that this post helped you to explore possibilities of Pytest deeper and bring your coding skills to the next level.

Have an idea? Let's discuss!

Your technical partner for software development and digital transformation.

tags

you may also like

Insights, case studies, success stories and professional discoveries