Testing

Test-Driven Development (TDD) of Django APP

Testing Django App With Pytest

Pytest is a framework for the testing application. Pytest suggests much more pythonic tests without boilerplate.

circle-info

Why we should use Pytest?

Pytest provides a new approach for writing tests, namely, functional testing for applications and libraries.

  • Integration tests

  • Less boilerplate code

  • one asserts keyword for the test (No need to remember assertEqual, assertTrue... )

  • Auto-discovery

  • Parametrizing

  • Detailed info on failing assert statements

  • External plugins

  • Coverage

  • Parallel testing

  • Better integration with CI/CD

  • Run Django tests

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.

circle-check

All fixtures have scope arguments with available values.

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

Use Fixtures with tests in Pytest

To use the fixture in the test, we can put fixture name as a function argument:

circle-check

Parametrize in Pytest

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

Test-Driven Development (TDD) of APIs

As you may have already learned, test-driven development is an approach that focuses on writing tests before you start implementing the business logic of your application. Writing tests first requires you to really consider what do you want from the code. But apart from this, TDD has numerous other benefits:

  1. Fast feedback and detailed specification;

  2. Reduced time spent on reworking and time spent in the debugger;

  3. Maintainable, flexible, and easily extensible code;

  4. Shorter development time to market;

  5. Increased developer’s productivity;

  6. SOLID code; and

  7. A clean interface.

Setting up Pytest for Django Project

1. Installation

2. Point our Django Settings to Pytest

We 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:

3. Run tests

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

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.

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

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 the shortcut to the User model configured for use by the current Django project, like settings.AUTH_USER_MODEL

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:

Re-write tests above:

5. Auto Login Client

Let’s test some authenticated endpoints:

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 users:

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

Use our new fixture for the test above:

6. Parametrizing 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 the upgrade we had the next tests:

7. Test Mail Outbox with Pytest

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

Testing Django REST Framework with Pytest

1. API Client

The first thing to do here is to create our 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 uses Token. Let’s create a fixture to get or create a 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:

Useful Tips for Pytest

1. Using Factory Boy as fixtures for testing 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:

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-factoryboyarrow-up-right and factoryboyarrow-up-right.

2. Mocking your Pytest test with fixture

Using pytest-mockarrow-up-right 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.patcharrow-up-right and supports the same methods as:

  • mocker.patch

  • mocker.patch.object

  • mocker.patch.multiple

  • mocker.patch.dict

  • mocker.stopall

3. 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-xdistarrow-up-right plugin which expands pytest functionality

1) Install the plugin:

2) Running test with multiprocessing:

4. Config pytest.ini file

Example of pytest.inifile 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:

5. Show your coverage of the test

To check coverage of your app you can use pytest-covarrow-up-right plugin

1) Install plugin:

2) Coverage of your project and example of report:

References

djangostars blog for django-pytest-testingarrow-up-right

Last updated