Learning How to Write Tests with pytest
in Late 2024
Introduction
I’ve been working with Django for a while now, and I’ve always used Django’s TestCase for testing. But recently, I came across some code in the Sentry repository where they were using pytest.fixture, and it got me curious. So, I decided to dive into learning pytest. I’m still getting the hang of it, but I’m making progress. I’m writing this as a note to myself on how to set it up and how I’m using it.
In this post, I’ll walk through setting up pytest-django
using uv
to install the necessary packages, but you can also use pip
if you prefer.
Setting Up pytest-django in Your Django Project
- Install
pytest-django
and Other Dependencies
If you’re using uv
, you can install the required packages with the following command:
uv pip install pytest pytest-django
Alternatively, if you’re using pip
, you can install them like this:
pip install pytest pytest-django
2. Create a pytest.ini
file
Create a pytest.ini
file in the root directory of your project
[pytest]
DJANGO_SETTINGS_MODULE = my_django_project.settings
Replace my_django_project.settings
with the actual path to your Django settings file.
Writing Basic Tests
In this example, @pytest.mark.django_db
indicates that the test requires database access. Use this marker only when necessary to avoid slowing down your tests.
#tests.py
import pytest
from django.urls import reverse
from rest_framework.test import APIClient
@pytest.mark.django_db
def test_my_view():
client = APIClient()
url = reverse('my_view')
response = client.get(url)
assert response.status_code == 200
Run Test
Run test with the following command uv pytest
or pytest
Best Practices for Writing Tests
- Use
pytest.mark.django_db
Only When Needed:
Adding @pytest.mark.django_db
to your tests can slow them down because it involves setting up and tearing down the database. Use this marker only when necessary to keep your tests running quickly.
2. Leverage Fixtures for Reusable Test Data:
Fixtures help keep your tests clean and avoid redundancy. Define a fixture once and reuse it across multiple tests.
3. Use client
Fixture for DRF API Tests:
Instead of manually creating an API client in every test, use the client
fixture provided by pytest-django
4. Parameterize Tests to Cover Multiple Scenarios:
Use pytest.mark.parametrize
to run a test with different sets of inputs. It helps you avoid writing multiple similar tests and makes it easy to test various scenarios.
@pytest.mark.parametrize("username, expected_status", [
('validuser', 200),
('invaliduser', 404),
])
def test_user_view(client, username, expected_status):
response = client.get(f'/api/users/{username}/')
assert response.status_code == expected_status
5. Use pytest
's Built-In Assertions:
It makes your test code more readable and easier to write.
6. Organize Tests with conftest.py
:
Place common fixtures and test configurations in a conftest.py
file.
7. Use pytest-xdist
for Parallel Testing:
Speed up your test suite by running tests in parallel using the pytest-xdist
plugin. Parallel execution can significantly reduce the time it takes to run all your tests, especially in large projects.
pip install pytest-xdist
pytest -n auto
8. Use pytest-cov
for Test Coverage:
It helps you understand which parts of your code are tested and which are not, ensuring better coverage.
pip install pytest-cov
pytest --cov=my_django_project
9. Keep Tests Isolated:
Each test should be independent to avoid interdependencies that could lead to unreliable results.
10. Test Edge Cases:
Make sure to test not only the expected behavior but also potential edge cases.
Conclusion
- Switching to
pytest-django
has streamlined my testing process, making it easier to write, maintain, and understand my tests. - Using
uv
to manage my packages has also simplified setup and maintenance.