Testing Your Django App With Pytest
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 and Django suggest much more pythonic tests without boilerplate.
In this comprehensive tutorial, we are talking about PyTest in Python. We have sufficient experience in this topic (what is our portfolio worth), so if you need help or additional advice, please contact us.
Why You Should Use Pytest?
While you can get by without it when testing in Python and Django, 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. We often use this in Django development.
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.
Read more: Python list generator
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 will use it in the next parts.
Q1: What are 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 is considered deprecated.
Q2: How to use Fixtures with tests 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:
When using Pytest, Django settings can also be specified 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.
Read also: Configuring Django Settings: Best Practices
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
for Django 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.
Read also: Python Rule Engine: Logic Automation & Examples
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 PyTest-Django 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 pytest 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.in
i 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 Django 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 intovenv
andold_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 the Django pytest coverage of your Python 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. And if you need consulting or a dedicated team extension, contact Django Stars.
- Can I use PyTest with Django?
- Yes, you can use the pytest-django plugin maintained by the pytest development team. It allows the use of pytest to write tests for Django projects and provides a set of handy tools for this.
- What is the advantage of Pytest?
- Compared to built-in tools, Pytest uses less boilerplate code. Also, Pytest makes it possible to run multiple tests in parallel, saving time. During execution, it allows skipping a test subset. If some test files and functions are not explicitly specified, Pytest auto-detects them. It can run unittest-based and nose test suites out of the box, provide detailed info on failures, and has many other benefits.
- What are the features of Pytest?
- Pytest's features that make it a powerful and flexible tool for testing Django applications include fixtures (to add arbitrary functions to test execution), marks (to simplify adding metadata), assert statements, parametrizing, auto-detecting of test modules and functions, and detailed info on failures. In addition, the functionality of PyTest can be extended with a large selection of external plugins.
- How do I run test py in Django?
- To run written tests in Django, you can use the test command of the project’s manage.py utility:
$ ./manage.py test
. The unittest module's built-in test discovery will discover tests in any file named test*.py (by default) under the current working directory. You can read more about this in the Django documentation. In Pytest, pytest command is used instead of manage.py test.
- How to test Django REST Framework with Pytest?
- First, create your own PyTest fixture for API client of Django REST Framework. Then, create fixture to get or create token for a user or API client with credentials to bypass PyTest authentication. Also, you can use PyTest parametrizing fixture for data validation, which helps avoid creating almost identical tests, and mock extra action in your views.