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.
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.
All fixtures have scope arguments with available values.
function run once per test
class run once per class of tests
module runs once per module
session runs once per session
NOTE: Default value of scope is the function
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:
Note: Pytest automatically registers our fixtures and can have access to fixtures without extra imports.
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:
Fast feedback and detailed specification;
Reduced time spent on reworking and time spent in the debugger;
Maintainable, flexible, and easily extensible code;
Shorter development time to market;
Increased developer’s productivity;
SOLID code; and
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:
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. 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
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-xdist 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
venvandold_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-cov plugin
1) Install plugin:
2) Coverage of your project and example of report:
References
djangostars blog for django-pytest-testing
Last updated