• United States+1
  • United Kingdom+44
  • Afghanistan (‫افغانستان‬‎)+93
  • Albania (Shqipëri)+355
  • Algeria (‫الجزائر‬‎)+213
  • American Samoa+1684
  • Andorra+376
  • Angola+244
  • Anguilla+1264
  • Antigua and Barbuda+1268
  • Argentina+54
  • Armenia (Հայաստան)+374
  • Aruba+297
  • Australia+61
  • Austria (Österreich)+43
  • Azerbaijan (Azərbaycan)+994
  • Bahamas+1242
  • Bahrain (‫البحرين‬‎)+973
  • Bangladesh (বাংলাদেশ)+880
  • Barbados+1246
  • Belarus (Беларусь)+375
  • Belgium (België)+32
  • Belize+501
  • Benin (Bénin)+229
  • Bermuda+1441
  • Bhutan (འབྲུག)+975
  • Bolivia+591
  • Bosnia and Herzegovina (Босна и Херцеговина)+387
  • Botswana+267
  • Brazil (Brasil)+55
  • British Indian Ocean Territory+246
  • British Virgin Islands+1284
  • Brunei+673
  • Bulgaria (България)+359
  • Burkina Faso+226
  • Burundi (Uburundi)+257
  • Cambodia (កម្ពុជា)+855
  • Cameroon (Cameroun)+237
  • Canada+1
  • Cape Verde (Kabu Verdi)+238
  • Caribbean Netherlands+599
  • Cayman Islands+1345
  • Central African Republic (République centrafricaine)+236
  • Chad (Tchad)+235
  • Chile+56
  • China (中国)+86
  • Christmas Island+61
  • Cocos (Keeling) Islands+61
  • Colombia+57
  • Comoros (‫جزر القمر‬‎)+269
  • Congo (DRC) (Jamhuri ya Kidemokrasia ya Kongo)+243
  • Congo (Republic) (Congo-Brazzaville)+242
  • Cook Islands+682
  • Costa Rica+506
  • Côte d’Ivoire+225
  • Croatia (Hrvatska)+385
  • Cuba+53
  • Curaçao+599
  • Cyprus (Κύπρος)+357
  • Czech Republic (Česká republika)+420
  • Denmark (Danmark)+45
  • Djibouti+253
  • Dominica+1767
  • Dominican Republic (República Dominicana)+1
  • Ecuador+593
  • Egypt (‫مصر‬‎)+20
  • El Salvador+503
  • Equatorial Guinea (Guinea Ecuatorial)+240
  • Eritrea+291
  • Estonia (Eesti)+372
  • Ethiopia+251
  • Falkland Islands (Islas Malvinas)+500
  • Faroe Islands (Føroyar)+298
  • Fiji+679
  • Finland (Suomi)+358
  • France+33
  • French Guiana (Guyane française)+594
  • French Polynesia (Polynésie française)+689
  • Gabon+241
  • Gambia+220
  • Georgia (საქართველო)+995
  • Germany (Deutschland)+49
  • Ghana (Gaana)+233
  • Gibraltar+350
  • Greece (Ελλάδα)+30
  • Greenland (Kalaallit Nunaat)+299
  • Grenada+1473
  • Guadeloupe+590
  • Guam+1671
  • Guatemala+502
  • Guernsey+44
  • Guinea (Guinée)+224
  • Guinea-Bissau (Guiné Bissau)+245
  • Guyana+592
  • Haiti+509
  • Honduras+504
  • Hong Kong (香港)+852
  • Hungary (Magyarország)+36
  • Iceland (Ísland)+354
  • India (भारत)+91
  • Indonesia+62
  • Iran (‫ایران‬‎)+98
  • Iraq (‫العراق‬‎)+964
  • Ireland+353
  • Isle of Man+44
  • Israel (‫ישראל‬‎)+972
  • Italy (Italia)+39
  • Jamaica+1876
  • Japan (日本)+81
  • Jersey+44
  • Jordan (‫الأردن‬‎)+962
  • Kazakhstan (Казахстан)+7
  • Kenya+254
  • Kiribati+686
  • Kosovo+383
  • Kuwait (‫الكويت‬‎)+965
  • Kyrgyzstan (Кыргызстан)+996
  • Laos (ລາວ)+856
  • Latvia (Latvija)+371
  • Lebanon (‫لبنان‬‎)+961
  • Lesotho+266
  • Liberia+231
  • Libya (‫ليبيا‬‎)+218
  • Liechtenstein+423
  • Lithuania (Lietuva)+370
  • Luxembourg+352
  • Macau (澳門)+853
  • Macedonia (FYROM) (Македонија)+389
  • Madagascar (Madagasikara)+261
  • Malawi+265
  • Malaysia+60
  • Maldives+960
  • Mali+223
  • Malta+356
  • Marshall Islands+692
  • Martinique+596
  • Mauritania (‫موريتانيا‬‎)+222
  • Mauritius (Moris)+230
  • Mayotte+262
  • Mexico (México)+52
  • Micronesia+691
  • Moldova (Republica Moldova)+373
  • Monaco+377
  • Mongolia (Монгол)+976
  • Montenegro (Crna Gora)+382
  • Montserrat+1664
  • Morocco (‫المغرب‬‎)+212
  • Mozambique (Moçambique)+258
  • Myanmar (Burma) (မြန်မာ)+95
  • Namibia (Namibië)+264
  • Nauru+674
  • Nepal (नेपाल)+977
  • Netherlands (Nederland)+31
  • New Caledonia (Nouvelle-Calédonie)+687
  • New Zealand+64
  • Nicaragua+505
  • Niger (Nijar)+227
  • Nigeria+234
  • Niue+683
  • Norfolk Island+672
  • North Korea (조선 민주주의 인민 공화국)+850
  • Northern Mariana Islands+1670
  • Norway (Norge)+47
  • Oman (‫عُمان‬‎)+968
  • Pakistan (‫پاکستان‬‎)+92
  • Palau+680
  • Palestine (‫فلسطين‬‎)+970
  • Panama (Panamá)+507
  • Papua New Guinea+675
  • Paraguay+595
  • Peru (Perú)+51
  • Philippines+63
  • Poland (Polska)+48
  • Portugal+351
  • Puerto Rico+1
  • Qatar (‫قطر‬‎)+974
  • Réunion (La Réunion)+262
  • Romania (România)+40
  • Russia (Россия)+7
  • Rwanda+250
  • Saint Barthélemy (Saint-Barthélemy)+590
  • Saint Helena+290
  • Saint Kitts and Nevis+1869
  • Saint Lucia+1758
  • Saint Martin (Saint-Martin (partie française))+590
  • Saint Pierre and Miquelon (Saint-Pierre-et-Miquelon)+508
  • Saint Vincent and the Grenadines+1784
  • Samoa+685
  • San Marino+378
  • São Tomé and Príncipe (São Tomé e Príncipe)+239
  • Saudi Arabia (‫المملكة العربية السعودية‬‎)+966
  • Senegal (Sénégal)+221
  • Serbia (Србија)+381
  • Seychelles+248
  • Sierra Leone+232
  • Singapore+65
  • Sint Maarten+1721
  • Slovakia (Slovensko)+421
  • Slovenia (Slovenija)+386
  • Solomon Islands+677
  • Somalia (Soomaaliya)+252
  • South Africa+27
  • South Korea (대한민국)+82
  • South Sudan (‫جنوب السودان‬‎)+211
  • Spain (España)+34
  • Sri Lanka (ශ්‍රී ලංකාව)+94
  • Sudan (‫السودان‬‎)+249
  • Suriname+597
  • Svalbard and Jan Mayen+47
  • Swaziland+268
  • Sweden (Sverige)+46
  • Switzerland (Schweiz)+41
  • Syria (‫سوريا‬‎)+963
  • Taiwan (台灣)+886
  • Tajikistan+992
  • Tanzania+255
  • Thailand (ไทย)+66
  • Timor-Leste+670
  • Togo+228
  • Tokelau+690
  • Tonga+676
  • Trinidad and Tobago+1868
  • Tunisia (‫تونس‬‎)+216
  • Turkey (Türkiye)+90
  • Turkmenistan+993
  • Turks and Caicos Islands+1649
  • Tuvalu+688
  • U.S. Virgin Islands+1340
  • Uganda+256
  • Ukraine (Україна)+380
  • United Arab Emirates (‫الإمارات العربية المتحدة‬‎)+971
  • United Kingdom+44
  • United States+1
  • Uruguay+598
  • Uzbekistan (Oʻzbekiston)+998
  • Vanuatu+678
  • Vatican City (Città del Vaticano)+39
  • Venezuela+58
  • Vietnam (Việt Nam)+84
  • Wallis and Futuna+681
  • Western Sahara (‫الصحراء الغربية‬‎)+212
  • Yemen (‫اليمن‬‎)+967
  • Zambia+260
  • Zimbabwe+263
  • Åland Islands+358
Thanks! We'll be in touch in the next 12 hours
Oops! Something went wrong while submitting the form.

How to Use Pytest Fixtures With Django Models

Suraj Patil

Full-stack Development

With the test framework that Python and Django provide, there is a lot of code boilerplate, maintainability, and duplication issues rise as your projects grow. It’s also not a very pythonic way of writing tests.

Pytest provides a simple and more elegant way to write tests.

It provides the ability to write tests as functions, which means a lot of boilerplate code has been removed, making your code more readable and easy to maintain. Pytest also provides functionality in terms of test discovery—and defining and using fixtures.

Why Pytest Fixtures?

When writing tests, it's very common that the test will need objects, and those objects may be needed by multiple tests. There might be a complicated process for the creation of these objects. It will be difficult to add that complex process in each of the test cases, and on any model changes, we will need to update our logic in all places. This will create issues of code duplication and its maintainability.

To avoid all of this, we can use the fixture provided by the pytest, where we will define the fixture in one place, and then we can inject that fixture in any of the tests in a much simpler way.

Briefly, if we have to understand fixtures, in the literal sense, they are where we prepare everything for our test. They’re everything that the test needs to do its thing.

We are going to explore how effectively we can make use of fixtures with Django models that are more readable and easy to maintain. These are the fixtures provided by the pytest and not to be confused with Django fixtures.

Installation and Setup

For this blog, we will set up a basic e-commerce application and set up the test suite for pytest.

Creating Django App

Before we begin testing, let's create a basic e-commerce application and add a few models on which we can perform tests later.

To create a Django app, go to the folder you want to work in, open the terminal, and run the below commands:

$ django-admin startproject e_commerce_app
$ cd e-commerce-app
$ python manage.py startapp product
view raw create_app.sh hosted with ❤ by GitHub

Once the app is created, go to the settings.py and add the newly created product app to the INSTALLED_APPS.

# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'product'
]
view raw settings.py hosted with ❤ by GitHub

Now, let's create basic models in the models.py of the product app.

from django.db import models
class Retail(models.Model):
name = models.CharField(max_length=128)
class Category(models.Model):
name = models.CharField(max_length=128, unique=True)
class Product(models.Model):
sku = models.CharField(max_length=50, unique=True) # unique model number
name = models.CharField(max_length=50)
description = models.TextField(default="", blank=True)
mrp = models.DecimalField(max_digits=10, decimal_places=2)
weight = models.DecimalField(max_digits=10, decimal_places=2)
retails = models.ManyToManyField(
Retail,
related_name="products",
verbose_name="Retail stores that carry the product",
)
category = models.ForeignKey(
Category,
related_name="products",
on_delete=models.CASCADE,
blank=True,
null=True,
)
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
view raw models.py hosted with ❤ by GitHub

Here, each product will have a category and will be available at many retail stores. Now, let's run the migration file and migrate the changes:

$ python manage.py makemigrations
$ python manage.py migrate
view raw migrate.sh hosted with ❤ by GitHub

The models and database is now ready, and we can move on to writing test cases for these models.

Let's set up the pytest in our Django app first.

For testing our Django applications with pytest, we will use the plugin pytest-django, which provides a set of useful tools for testing Django apps and projects. Let’s start with installing and configuration of the plugin.

Installing pytest

Pytest can be installed with pip:

$ pip install pytest-django

Installing pytest-django will also automatically install the latest version of pytest. Once installed, we need to tell pytest-django where our settings.py file is located.

The easiest way to do this is to create a pytest configuration file with this information.

Create a file called pytest.ini in your project directory and add this content:

[pytest]
DJANGO_SETTINGS_MODULE=e_commerce_app.settings
view raw pytest.ini hosted with ❤ by GitHub

You can provide various configurations in the file that will define how our tests should run.

e.g. To configure how test files should be detected across project, we can add this line:

[pytest]
DJANGO_SETTINGS_MODULE=e_commerce_app.settings
python_files = tests.py test_*.py *_tests.py
view raw pytest.ini hosted with ❤ by GitHub

Adding Test Suite to the Django App

Django and pytest automatically detect and run your test cases in files whose name starts with 'test'.

In the product app folder, create a new module named tests. Then add a file called test_models.py in which we will write all the model test cases for this app.

$ cd product
$ mkdir tests
$ cd tests && touch test_models.py
view raw create_tests.sh hosted with ❤ by GitHub

Running your Test Suite

Tests are invoked directly with the pytest command:

$ pytest
$ pytest tests # test a directory
$ pytest test.py # test file

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

Writing Tests with Pytest

Here, we will write a few test cases to test the models we have written in the models.py file. To start with, let's create a simple test case to test the category creation.

from product.models import Category
def test_create_category():
category = Category.objects.create(name="Books")
assert category.name == "Books"
view raw models.py hosted with ❤ by GitHub

Now, try to execute this test from your command line:

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
django: settings: pytest_fixtures.settings (from ini)
rootdir: /home/suraj/PycharmProjects/e_commerce_app, configfile: pytest.ini
plugins: django-4.1.0
collected 1 item
product/tests/test_models.py F [100%]
=================================== FAILURES ===================================
_____________________________ test_create_category _____________________________
def test_create_category():
> category = Category.objects.create(name="Books")
product/tests/test_models.py:5:
...
E RuntimeError: Database access not allowed, use the "django_db" mark, or the "db" or "transactional_db" fixtures to enable it.
venv/lib/python3.7/site-packages/django/db/backends/base/base.py:235: RuntimeError
=========================== short test summary info ============================
FAILED product/tests/test_models.py::test_create_category - RuntimeError: Dat...
============================== 1 failed in 0.21s ===============================
view raw shell.sh hosted with ❤ by GitHub

The tests failed. If you look at the error, it has to do something with the database. The pytest-django doc says:

pytest-django takes a conservative approach to enabling database access. By default your tests will fail if they try to access the database. Only if you explicitly request database access will this be allowed. This encourages you to keep database-needing tests to a minimum which makes it very clear what code uses the database.

This means we need to explicitly provide database access to our test cases. For this, we need to use [pytest marks](<https://docs.pytest.org/en/stable/mark.html#mark>) to tell pytest-django your test needs database access.

from product.models import Category
@pytest.mark.django_db
def test_create_category():
category = Category.objects.create(name="Books")
assert category.name == "Books"
view raw test_models.py hosted with ❤ by GitHub

Alternatively, there is one more way we can access the database in the test cases, i.e., using the db helper fixture provided by the pytest-django. This fixture will ensure the Django database is set up. It’s only required for fixtures that want to use the database themselves.

from product.models import Category
def test_create_category(db):
category = Category.objects.create(name="Books")
assert category.name == "Books"
view raw test_models.py hosted with ❤ by GitHub

Going forward, we will use the db fixture approach as it promotes code reusability using fixtures.

Run the test again:

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
django: settings: pytest_fixtures.settings (from ini)
rootdir: /home/suraj/PycharmProjects/e_commerce_app, configfile: pytest.ini
plugins: django-4.1.0
collected 1 item
product/tests/test_models.py . [100%]
============================== 1 passed in 0.24s ===============================
view raw shell.sh hosted with ❤ by GitHub

The command completed successfully and your test passed. Great! We have successfully written our first test case using pytest.

Creating Fixtures for Django Models

Now that you’re familiar with Django and pytest, let's add a test case to check if the to-check category updates.

from product.models import Category
def test_filter_category(db):
Category.objects.create(name="Books")
assert Category.objects.filter(name="Books").exists()
def test_update_category(db):
category = Category.objects.create(name="Books")
category.name = "DVDs"
category.save()
category_from_db = Category.objects.get(name="DVDs")
assert category_from_db.name == "DVDs"
view raw test_models.py hosted with ❤ by GitHub

If you look at both the test cases, one thing you can observe is that both the test cases do not test Category creation logic, and the Category instance is also getting created twice, once per test case. Once the project becomes large, we might have many test cases that will need the Category instance. If every test is creating its own category, then you might face trouble if any changes to the Category model happen.

This is where fixtures come to the rescue. It promotes code reusability in your test cases. To reuse an object in many test cases, you can create a test fixture:

import pytest
from product.models import Category
@pytest.fixture
def category(db) -> Category:
return Category.objects.create(name="Books")
def test_filter_category(category):
assert Category.objects.filter(name="Books").exists()
def test_update_category(category):
category.name = "DVDs"
category.save()
category_from_db = Category.objects.get(name="DVDs")
assert category_from_db.name == "DVDs"
view raw test_models.py hosted with ❤ by GitHub

Here, we have created a simple function called category and decorated it with @pytest.fixture to mark it as a fixture. It can now be injected into the test cases just like we injected the fixture db.

Now, if a new requirement comes in that every category should have a description and a small icon to represent the category, we don't need to now go to each test case and update the category to create logic. We just need to update the fixture, i.e., only one place. And it will take effect in every test case.

import pytest
from product.models import Category
@pytest.fixture
def category(db) -> Category:
return Category.objects.create(
name="Books", description="Category of Books", icon="books.png"
)
view raw test_models.py hosted with ❤ by GitHub

Using fixtures, you can avoid code duplication and make tests more maintainable.

Parametrizing fixtures

It is recommended to have a single fixture function that can be executed across different input values. This can be achieved via parameterized pytest fixtures.

Let's write the fixture for the product and consider we will need to create a SKU product number that has 6 characters and contains only alphanumeric characters.

import pytest
from product.models import Category, Product
@pytest.fixture
def product_one(db):
return Product.objects.create(name="Book 1", sku="ABC123")
def test_product_sku(product_one):
assert all(letter.isalnum() for letter in product_one.sku)
assert len(product_one.sku) == 6
view raw test_models.py hosted with ❤ by GitHub

We now want to test the case against multiple sku cases and make sure for all types of inputs the test is validated. We can flag the fixture to create three different product_one fixture instances. The fixture function gets access to each parameter through the special request object:

import pytest
from product.models import Product
@pytest.fixture(params=("ABC123", "123456", "ABCDEF"))
def product_one(db,request):
return Product.objects.create(name="Book 1",sku=request.param)
def test_product_sku(product_one):
assert all(letter.isalnum() for letter in product_one.sku)
assert len(product_one.sku) == 6
view raw test_models.py hosted with ❤ by GitHub

Fixture functions can be parametrized in which case they will be called multiple times, each time executing the set of dependent tests, i.e., the tests that depend on this fixture.

Test functions usually do not need to be aware of their re-running. Fixture parametrization helps to write exhaustive functional tests for components that can be configured in multiple ways.

Open the terminal and run the test:

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
django: settings: pytest_fixtures.settings (from ini)
rootdir: /home/suraj/PycharmProjects/e_commerce_app, configfile: pytest.ini
plugins: django-4.1.0
collected 3 items
product/tests/test_models.py ... [100%]
============================== 3 passed in 0.27s ===============================
view raw shell.sh hosted with ❤ by GitHub

We can see that our test_product_sku function ran thrice.

Injecting Fixtures into Other fixtures.

We will often come across a case wherein, we will need an object for a case that will be dependent on some other object. Let's try to create a few products under the category "Books".

import pytest
from product.models import Category, Product
@pytest.fixture
def product_one(db):
category = Category.objects.create(name="Books")
return Product.objects.create(name="Book 1", category=category)
@pytest.fixture
def product_two(db):
category = Category.objects.create(name="Books")
return Product.objects.create(name="Book 2", category=category)
def test_two_different_books_create(product_one, product_two):
assert product_one.pk != product_two.pk
view raw test_models.py hosted with ❤ by GitHub

If we try to test this in the terminal, we will encounter an error:

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
django: settings: pytest_fixtures.settings (from ini)
rootdir: /home/suraj/PycharmProjects/e_commerce_app, configfile: pytest.ini
plugins: django-4.1.0
collected 1 item
product/tests/test_models.py E [100%]
==================================== ERRORS ====================================
______________ ERROR at setup of test_two_different_books_create _______________
...
query = 'INSERT INTO "product_category" ("name") VALUES (?)', params = ['Books']
...
E django.db.utils.IntegrityError: UNIQUE constraint failed: product_category.name
venv/lib/python3.7/site-packages/django/db/backends/sqlite3/base.py:413: IntegrityError
=========================== short test summary info ============================
ERROR product/tests/test_models.py::test_two_different_books_create - django....
=============================== 1 error in 0.44s ===============================
view raw shell.sh hosted with ❤ by GitHub

The test case throws an IntegrityError, saying we tried to create the "Books" category twice. And if you look at the code, we have created the category in both product_one and product_two fixtures. What could we have done better?

If you look carefully, we have injected db in both the product_one and product_two fixtures, and db is just another fixture. So that means fixtures can be injected into other fixtures.

One of pytest’s greatest strengths is its extremely flexible fixture system. It allows us to boil down complex requirements for tests into more simple and organized functions, where we only need to have each one describe the things they are dependent on.

You can use this feature to address the IntegrityError above. Create the category fixture and inject it into both the product fixtures.

import pytest
from product.models import Category, Product
@pytest.fixture
def category(db) -> Category:
return Category.objects.create(name="Books")
@pytest.fixture
def product_one(db, category):
return Product.objects.create(name="Book 1", category=category)
@pytest.fixture
def product_two(db, category):
return Product.objects.create(name="Book 2", category=category)
def test_two_different_books_create(product_one, product_two):
assert product_one.pk != product_two.pk
view raw test_models.py hosted with ❤ by GitHub

If we try to run the test now, it should run successfully.

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
django: settings: pytest_fixtures.settings (from ini)
rootdir: /home/suraj/PycharmProjects/e_commerce_app, configfile: pytest.ini
plugins: django-4.1.0
collected 1 item
product/tests/test_models.py . [100%]
============================== 1 passed in 0.20s ===============================
view raw shell.sh hosted with ❤ by GitHub

By restructuring the fixtures this way, we have made code easier to maintain. By simply injecting fixtures, we can maintain a lot of complex model fixtures in a much simpler way.

Let's say we need to add an example where product one and product two will be sold by retail shop "ABC". This can be easily achieved by injecting retailer fixtures into the product fixture.

import pytest
from product.models import Category, Product, Retail
@pytest.fixture
def category(db) -> Category:
return Category.objects.create(name="Books")
@pytest.fixture
def retailer_abc(db):
return Retail.objects.create(name="ABC")
@pytest.fixture
def product_one(db, category, retailer_abc):
product = Product.objects.create(name="Book 1", category=category)
product.retails.add(retailer_abc)
return product
def test_product_retailer(db, retailer_abc, product_one):
assert product_one.retails.filter(name=retailer_abc.name).exists()
view raw test_models.py hosted with ❤ by GitHub

Autouse Fixtures

Sometimes, you may want to have a fixture (or even several) that you know all your tests will depend on. “Autouse” fixtures are a convenient way of making all tests automatically request them. This can cut out a lot of redundant requests, and can even provide more advanced fixture usage.

We can make a fixture an autouse fixture by passing in autouse=True to the fixture’s decorator. Here’s a simple example of how they can be used:

import pytest
from product.models import Category, Product, Retail
...
@pytest.fixture
def retailer_abc(db):
return Retail.objects.create(name="ABC")
@pytest.fixture
def retailers(db) -> list:
return []
@pytest.fixture(autouse=True)
def append_retailers(retailers, retailer_abc):
return retailers.append(retailer_abc)
@pytest.fixture
def product_one(db, category, retailers):
product = Product.objects.create(name="Book 1", category=category)
product.retails.set(retailers)
return product
def test_product_retailer(db, retailer_abc, product_one):
assert product_one.retails.filter(name=retailer_abc.name).exists()
view raw test_models.py hosted with ❤ by GitHub

In this example, the append_retailers fixture is an autouse fixture. Because it happens automatically, test_product_retailer is affected by it, even though the test did not request it. That doesn’t mean they can’t be requested though; just that it isn’t necessary.

Factories as Fixtures

So far, we have created objects with a small number of arguments. However, practically models are a bit more complex and may require more inputs. Let's say we will need to store the sku, mrp, and weight information along with name and category.

If we decide to provide every input to the product fixture, then the logic inside the product fixtures will get a little complicated.

import random
import string
import pytest
from product.models import Category, Product, Retail
@pytest.fixture
def category(db) -> Category:
return Category.objects.create(name="Books")
@pytest.fixture
def retailer_abc(db):
return Retail.objects.create(name="ABC")
@pytest.fixture
def product_one(db, category, retailer_abc):
sku = "".join(random.choices(string.ascii_uppercase + string.digits, k=6))
product = Product.objects.create(
sku=sku,
name="Book 1",
description="A book for educational purpose.",
mrp="100.00",
is_available=True,
category=category,
)
product.retails.set([retailer_abc])
return product
@pytest.fixture
def product_two(db, category, retailer):
sku = "".join(random.choices(string.ascii_uppercase + string.digits, k=6))
product = Product.objects.create(
sku=sku,
name="Book 2",
description="A book with thriller story.",
mrp="50.00",
is_available=True,
category=category,
)
product.retails.add([retailer])
return product
view raw test_models.py hosted with ❤ by GitHub

Product creation has a somewhat complex logic of managing retailers and generating unique SKU. And the product creation logic will grow as we keep adding requirements. There may be some extra logic needed if we consider discounts and coupon code complexity for every retailer. There may also be a lot of versions of the product instance we may want to test against, and you have already learned how difficult it is to maintain such a complex code.

The “factory as fixture” pattern can help in these cases where the same class instance is needed for different tests. Instead of returning an instance directly, the fixture will return a function, and upon calling which one, you can get the distance that you wanted to test.

import random
import string
import pytest
from product.models import Category, Product, Retail
@pytest.fixture
def category(db) -> Category:
return Category.objects.create(name="Books")
@pytest.fixture
def retailer_abc(db):
return Retail.objects.create(name="ABC")
@pytest.fixture
def product_factory(db, category, retailer_abc):
def create_product(
name, description="A Book", mrp=None, is_available=True, retailers=None
):
if retailers is None:
retailers = []
sku = "".join(random.choices(string.ascii_uppercase +
string.digits, k=6))
product = Product.objects.create(
sku=sku,
name=name,
description=description,
mrp=mrp,
is_available=is_available,
category=category,
)
product.retails.add(retailer_abc)
if retailers:
product.retails.set(retailers)
return product
return create_product
@pytest.fixture
def product_one(product_factory):
return product_factory(name="Book 1", mrp="100.2")
@pytest.fixture
def product_two(product_factory):
return product_factory(name="Novel Book", mrp="51")
def test_product_retailer(db, retailer_abc, product_one):
assert product_one.retails.filter(name=retailer_abc.name).exists()
def test_product_one(product_one):
assert product_one.name == "Book 1"
assert product_one.is_available
view raw test_models.py hosted with ❤ by GitHub

This is not far from what you’ve already done, so let’s break it down:

  • The category and retailer_abc fixture remains the same.
  • A new product_factory fixture is added, and it is injected with the category  and  retailer_abc fixture.
  • The fixture product_factory creates a wrapper and returns an inner function called create_product.
  • Inject product_factory into another fixture and use it to create a product instance

The factory fixture works similar to how decorators work in python.

Sharing Fixtures Using Scopes

Fixtures requiring network or db access depend on connectivity and are usually time-expensive to create. In the previous example, every time we request any fixture within our tests, it is used to run the method, generate an instance and pass them to the test. So if we have written ‘n’ tests, and every test calls for the same fixture then that fixture instance will be created n times during the entire execution.

This is mainly happening because fixtures are created when first requested by a test, and are destroyed based on their scope:

  • Function: the default scope, the fixture is destroyed at the end of the test.
  • Class: the fixture is destroyed during the teardown of the last test in the class.
  • Module: the fixture is destroyed during teardown of the last test in the module.
  • Package: the fixture is destroyed during teardown of the last test in the package.
  • Session: the fixture is destroyed at the end of the test session.

In the previous example, we can add scope="module" so that the category, retailer_abc, product_one, and product_two instances will only be invoked once per test module.

Multiple test functions in a test module will thus each receive the same category, retailer_abc, product_one, and product_two fixture instance, thus saving time.

@pytest.fixture(scope="module")
def category(db) -> Category:
return Category.objects.create(name="Books")
@pytest.fixture(scope="module")
def retailer_abc(db):
return Retail.objects.create(name="ABC")
@pytest.fixture(scope="module")
def product_one(product_factory):
return product_factory(name="Book 1", mrp="100.2")
@pytest.fixture(scope="module")
def product_two(product_factory):
return product_factory(name="Novel Book", mrp="51")
view raw test_models.py hosted with ❤ by GitHub

This is how we can add scope to the fixtures, and you can do it for all the fixtures.

But, If we try to test this in the terminal, we will encounter an error:

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
django: settings: pytest_fixtures.settings (from ini)
rootdir: /home/suraj/PycharmProjects/e_commerce_app, configfile: pytest.ini
plugins: django-4.1.0
collected 2 items
product/tests/test_models.py EE [100%]
==================================== ERRORS ====================================
___________________ ERROR at setup of test_product_retailer ____________________
ScopeMismatch: You tried to access the 'function' scoped fixture 'db' with a 'module' scoped request object, involved factories
product/tests/test_models.py:13: def retailer_abc(db) -> product.models.Category
venv/lib/python3.7/site-packages/pytest_django/fixtures.py:193: def db(request, django_db_setup, django_db_blocker)
______________________ ERROR at setup of test_product_one ______________________
ScopeMismatch: You tried to access the 'function' scoped fixture 'db' with a 'module' scoped request object, involved factories
...
============================== 2 errors in 0.24s =============================== [100%]
view raw shell.sh hosted with ❤ by GitHub

The reason for this error is that the db fixture has the function scope for a reason, so the transaction rollbacks on the end of each test ensure the database is left in the same state it has when the test starts. Nevertheless, you can have the session/module scoped access to the database in the fixture by using the django_db_blocker fixture:

import random
import string
import pytest
from product.models import Category, Product, Retail
@pytest.fixture(scope="module")
def category(django_db_blocker):
with django_db_blocker.unblock():
return Category.objects.create(name="Books")
@pytest.fixture(scope="module")
def retailer_abc(django_db_blocker):
with django_db_blocker.unblock():
return Retail.objects.create(name="ABC")
@pytest.fixture(scope="module")
def product_factory(django_db_blocker, category, retailer_abc):
def create_product(
name, description="A Book", mrp=None, is_available=True, retailers=None
):
if retailers is None:
retailers = []
sku = "".join(random.choices(
string.ascii_uppercase + string.digits, k=6)
)
with django_db_blocker.unblock():
product = Product.objects.create(
sku=sku,
name=name,
description=description,
mrp=mrp,
is_available=is_available,
category=category,
)
product.retails.add(retailer_abc)
if retailers:
product.retails.set(retailers)
return product
return create_product
@pytest.fixture(scope="module")
def product_one(product_factory):
return product_factory(name="Book 1", mrp="100.2")
@pytest.fixture(scope="module")
def product_two(product_factory):
return product_factory(name="Novel Book", mrp="51")
def test_product_retailer(db, retailer_abc, product_one):
assert product_one.retails.filter(name=retailer_abc.name).exists()
def test_product_one(product_one):
assert product_one.name == "Book 1"
assert product_one.is_available
view raw test_models.py hosted with ❤ by GitHub

Now, if we go to the terminal and run the tests, it will run successfully.

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
django: settings: pytest_fixtures.settings (from ini)
rootdir: /home/suraj/PycharmProjects/e_commerce_app, configfile: pytest.ini
plugins: django-4.1.0
collected 2 items
product/tests/test_models.py .. [100%]
============================== 2 passed in 0.22s ===============================
view raw shell.sh hosted with ❤ by GitHub

Warning: Beware that when unlocking the database in session scope, you're on your own if you alter the database in other fixtures or tests.

Conclusion

We have successfully learned various features pytest fixtures provide and how we can benefit from the code reusability perspective and have maintainable code in your tests. Dependency management and arranging your test data becomes easy with the help of fixtures.

This was a blog about how you can use fixtures and the various features it provides along with the Django models. You can check more on fixtures by referring to the official documentation.

Get the latest engineering blogs delivered straight to your inbox.
No spam. Only expert insights.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Did you like the blog? If yes, we're sure you'll also like to work with the people who write them - our best-in-class engineering team.

We're looking for talented developers who are passionate about new emerging technologies. If that's you, get in touch with us.

Explore current openings

How to Use Pytest Fixtures With Django Models

With the test framework that Python and Django provide, there is a lot of code boilerplate, maintainability, and duplication issues rise as your projects grow. It’s also not a very pythonic way of writing tests.

Pytest provides a simple and more elegant way to write tests.

It provides the ability to write tests as functions, which means a lot of boilerplate code has been removed, making your code more readable and easy to maintain. Pytest also provides functionality in terms of test discovery—and defining and using fixtures.

Why Pytest Fixtures?

When writing tests, it's very common that the test will need objects, and those objects may be needed by multiple tests. There might be a complicated process for the creation of these objects. It will be difficult to add that complex process in each of the test cases, and on any model changes, we will need to update our logic in all places. This will create issues of code duplication and its maintainability.

To avoid all of this, we can use the fixture provided by the pytest, where we will define the fixture in one place, and then we can inject that fixture in any of the tests in a much simpler way.

Briefly, if we have to understand fixtures, in the literal sense, they are where we prepare everything for our test. They’re everything that the test needs to do its thing.

We are going to explore how effectively we can make use of fixtures with Django models that are more readable and easy to maintain. These are the fixtures provided by the pytest and not to be confused with Django fixtures.

Installation and Setup

For this blog, we will set up a basic e-commerce application and set up the test suite for pytest.

Creating Django App

Before we begin testing, let's create a basic e-commerce application and add a few models on which we can perform tests later.

To create a Django app, go to the folder you want to work in, open the terminal, and run the below commands:

$ django-admin startproject e_commerce_app
$ cd e-commerce-app
$ python manage.py startapp product
view raw create_app.sh hosted with ❤ by GitHub

Once the app is created, go to the settings.py and add the newly created product app to the INSTALLED_APPS.

# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'product'
]
view raw settings.py hosted with ❤ by GitHub

Now, let's create basic models in the models.py of the product app.

from django.db import models
class Retail(models.Model):
name = models.CharField(max_length=128)
class Category(models.Model):
name = models.CharField(max_length=128, unique=True)
class Product(models.Model):
sku = models.CharField(max_length=50, unique=True) # unique model number
name = models.CharField(max_length=50)
description = models.TextField(default="", blank=True)
mrp = models.DecimalField(max_digits=10, decimal_places=2)
weight = models.DecimalField(max_digits=10, decimal_places=2)
retails = models.ManyToManyField(
Retail,
related_name="products",
verbose_name="Retail stores that carry the product",
)
category = models.ForeignKey(
Category,
related_name="products",
on_delete=models.CASCADE,
blank=True,
null=True,
)
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
view raw models.py hosted with ❤ by GitHub

Here, each product will have a category and will be available at many retail stores. Now, let's run the migration file and migrate the changes:

$ python manage.py makemigrations
$ python manage.py migrate
view raw migrate.sh hosted with ❤ by GitHub

The models and database is now ready, and we can move on to writing test cases for these models.

Let's set up the pytest in our Django app first.

For testing our Django applications with pytest, we will use the plugin pytest-django, which provides a set of useful tools for testing Django apps and projects. Let’s start with installing and configuration of the plugin.

Installing pytest

Pytest can be installed with pip:

$ pip install pytest-django

Installing pytest-django will also automatically install the latest version of pytest. Once installed, we need to tell pytest-django where our settings.py file is located.

The easiest way to do this is to create a pytest configuration file with this information.

Create a file called pytest.ini in your project directory and add this content:

[pytest]
DJANGO_SETTINGS_MODULE=e_commerce_app.settings
view raw pytest.ini hosted with ❤ by GitHub

You can provide various configurations in the file that will define how our tests should run.

e.g. To configure how test files should be detected across project, we can add this line:

[pytest]
DJANGO_SETTINGS_MODULE=e_commerce_app.settings
python_files = tests.py test_*.py *_tests.py
view raw pytest.ini hosted with ❤ by GitHub

Adding Test Suite to the Django App

Django and pytest automatically detect and run your test cases in files whose name starts with 'test'.

In the product app folder, create a new module named tests. Then add a file called test_models.py in which we will write all the model test cases for this app.

$ cd product
$ mkdir tests
$ cd tests && touch test_models.py
view raw create_tests.sh hosted with ❤ by GitHub

Running your Test Suite

Tests are invoked directly with the pytest command:

$ pytest
$ pytest tests # test a directory
$ pytest test.py # test file

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

Writing Tests with Pytest

Here, we will write a few test cases to test the models we have written in the models.py file. To start with, let's create a simple test case to test the category creation.

from product.models import Category
def test_create_category():
category = Category.objects.create(name="Books")
assert category.name == "Books"
view raw models.py hosted with ❤ by GitHub

Now, try to execute this test from your command line:

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
django: settings: pytest_fixtures.settings (from ini)
rootdir: /home/suraj/PycharmProjects/e_commerce_app, configfile: pytest.ini
plugins: django-4.1.0
collected 1 item
product/tests/test_models.py F [100%]
=================================== FAILURES ===================================
_____________________________ test_create_category _____________________________
def test_create_category():
> category = Category.objects.create(name="Books")
product/tests/test_models.py:5:
...
E RuntimeError: Database access not allowed, use the "django_db" mark, or the "db" or "transactional_db" fixtures to enable it.
venv/lib/python3.7/site-packages/django/db/backends/base/base.py:235: RuntimeError
=========================== short test summary info ============================
FAILED product/tests/test_models.py::test_create_category - RuntimeError: Dat...
============================== 1 failed in 0.21s ===============================
view raw shell.sh hosted with ❤ by GitHub

The tests failed. If you look at the error, it has to do something with the database. The pytest-django doc says:

pytest-django takes a conservative approach to enabling database access. By default your tests will fail if they try to access the database. Only if you explicitly request database access will this be allowed. This encourages you to keep database-needing tests to a minimum which makes it very clear what code uses the database.

This means we need to explicitly provide database access to our test cases. For this, we need to use [pytest marks](<https://docs.pytest.org/en/stable/mark.html#mark>) to tell pytest-django your test needs database access.

from product.models import Category
@pytest.mark.django_db
def test_create_category():
category = Category.objects.create(name="Books")
assert category.name == "Books"
view raw test_models.py hosted with ❤ by GitHub

Alternatively, there is one more way we can access the database in the test cases, i.e., using the db helper fixture provided by the pytest-django. This fixture will ensure the Django database is set up. It’s only required for fixtures that want to use the database themselves.

from product.models import Category
def test_create_category(db):
category = Category.objects.create(name="Books")
assert category.name == "Books"
view raw test_models.py hosted with ❤ by GitHub

Going forward, we will use the db fixture approach as it promotes code reusability using fixtures.

Run the test again:

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
django: settings: pytest_fixtures.settings (from ini)
rootdir: /home/suraj/PycharmProjects/e_commerce_app, configfile: pytest.ini
plugins: django-4.1.0
collected 1 item
product/tests/test_models.py . [100%]
============================== 1 passed in 0.24s ===============================
view raw shell.sh hosted with ❤ by GitHub

The command completed successfully and your test passed. Great! We have successfully written our first test case using pytest.

Creating Fixtures for Django Models

Now that you’re familiar with Django and pytest, let's add a test case to check if the to-check category updates.

from product.models import Category
def test_filter_category(db):
Category.objects.create(name="Books")
assert Category.objects.filter(name="Books").exists()
def test_update_category(db):
category = Category.objects.create(name="Books")
category.name = "DVDs"
category.save()
category_from_db = Category.objects.get(name="DVDs")
assert category_from_db.name == "DVDs"
view raw test_models.py hosted with ❤ by GitHub

If you look at both the test cases, one thing you can observe is that both the test cases do not test Category creation logic, and the Category instance is also getting created twice, once per test case. Once the project becomes large, we might have many test cases that will need the Category instance. If every test is creating its own category, then you might face trouble if any changes to the Category model happen.

This is where fixtures come to the rescue. It promotes code reusability in your test cases. To reuse an object in many test cases, you can create a test fixture:

import pytest
from product.models import Category
@pytest.fixture
def category(db) -> Category:
return Category.objects.create(name="Books")
def test_filter_category(category):
assert Category.objects.filter(name="Books").exists()
def test_update_category(category):
category.name = "DVDs"
category.save()
category_from_db = Category.objects.get(name="DVDs")
assert category_from_db.name == "DVDs"
view raw test_models.py hosted with ❤ by GitHub

Here, we have created a simple function called category and decorated it with @pytest.fixture to mark it as a fixture. It can now be injected into the test cases just like we injected the fixture db.

Now, if a new requirement comes in that every category should have a description and a small icon to represent the category, we don't need to now go to each test case and update the category to create logic. We just need to update the fixture, i.e., only one place. And it will take effect in every test case.

import pytest
from product.models import Category
@pytest.fixture
def category(db) -> Category:
return Category.objects.create(
name="Books", description="Category of Books", icon="books.png"
)
view raw test_models.py hosted with ❤ by GitHub

Using fixtures, you can avoid code duplication and make tests more maintainable.

Parametrizing fixtures

It is recommended to have a single fixture function that can be executed across different input values. This can be achieved via parameterized pytest fixtures.

Let's write the fixture for the product and consider we will need to create a SKU product number that has 6 characters and contains only alphanumeric characters.

import pytest
from product.models import Category, Product
@pytest.fixture
def product_one(db):
return Product.objects.create(name="Book 1", sku="ABC123")
def test_product_sku(product_one):
assert all(letter.isalnum() for letter in product_one.sku)
assert len(product_one.sku) == 6
view raw test_models.py hosted with ❤ by GitHub

We now want to test the case against multiple sku cases and make sure for all types of inputs the test is validated. We can flag the fixture to create three different product_one fixture instances. The fixture function gets access to each parameter through the special request object:

import pytest
from product.models import Product
@pytest.fixture(params=("ABC123", "123456", "ABCDEF"))
def product_one(db,request):
return Product.objects.create(name="Book 1",sku=request.param)
def test_product_sku(product_one):
assert all(letter.isalnum() for letter in product_one.sku)
assert len(product_one.sku) == 6
view raw test_models.py hosted with ❤ by GitHub

Fixture functions can be parametrized in which case they will be called multiple times, each time executing the set of dependent tests, i.e., the tests that depend on this fixture.

Test functions usually do not need to be aware of their re-running. Fixture parametrization helps to write exhaustive functional tests for components that can be configured in multiple ways.

Open the terminal and run the test:

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
django: settings: pytest_fixtures.settings (from ini)
rootdir: /home/suraj/PycharmProjects/e_commerce_app, configfile: pytest.ini
plugins: django-4.1.0
collected 3 items
product/tests/test_models.py ... [100%]
============================== 3 passed in 0.27s ===============================
view raw shell.sh hosted with ❤ by GitHub

We can see that our test_product_sku function ran thrice.

Injecting Fixtures into Other fixtures.

We will often come across a case wherein, we will need an object for a case that will be dependent on some other object. Let's try to create a few products under the category "Books".

import pytest
from product.models import Category, Product
@pytest.fixture
def product_one(db):
category = Category.objects.create(name="Books")
return Product.objects.create(name="Book 1", category=category)
@pytest.fixture
def product_two(db):
category = Category.objects.create(name="Books")
return Product.objects.create(name="Book 2", category=category)
def test_two_different_books_create(product_one, product_two):
assert product_one.pk != product_two.pk
view raw test_models.py hosted with ❤ by GitHub

If we try to test this in the terminal, we will encounter an error:

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
django: settings: pytest_fixtures.settings (from ini)
rootdir: /home/suraj/PycharmProjects/e_commerce_app, configfile: pytest.ini
plugins: django-4.1.0
collected 1 item
product/tests/test_models.py E [100%]
==================================== ERRORS ====================================
______________ ERROR at setup of test_two_different_books_create _______________
...
query = 'INSERT INTO "product_category" ("name") VALUES (?)', params = ['Books']
...
E django.db.utils.IntegrityError: UNIQUE constraint failed: product_category.name
venv/lib/python3.7/site-packages/django/db/backends/sqlite3/base.py:413: IntegrityError
=========================== short test summary info ============================
ERROR product/tests/test_models.py::test_two_different_books_create - django....
=============================== 1 error in 0.44s ===============================
view raw shell.sh hosted with ❤ by GitHub

The test case throws an IntegrityError, saying we tried to create the "Books" category twice. And if you look at the code, we have created the category in both product_one and product_two fixtures. What could we have done better?

If you look carefully, we have injected db in both the product_one and product_two fixtures, and db is just another fixture. So that means fixtures can be injected into other fixtures.

One of pytest’s greatest strengths is its extremely flexible fixture system. It allows us to boil down complex requirements for tests into more simple and organized functions, where we only need to have each one describe the things they are dependent on.

You can use this feature to address the IntegrityError above. Create the category fixture and inject it into both the product fixtures.

import pytest
from product.models import Category, Product
@pytest.fixture
def category(db) -> Category:
return Category.objects.create(name="Books")
@pytest.fixture
def product_one(db, category):
return Product.objects.create(name="Book 1", category=category)
@pytest.fixture
def product_two(db, category):
return Product.objects.create(name="Book 2", category=category)
def test_two_different_books_create(product_one, product_two):
assert product_one.pk != product_two.pk
view raw test_models.py hosted with ❤ by GitHub

If we try to run the test now, it should run successfully.

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
django: settings: pytest_fixtures.settings (from ini)
rootdir: /home/suraj/PycharmProjects/e_commerce_app, configfile: pytest.ini
plugins: django-4.1.0
collected 1 item
product/tests/test_models.py . [100%]
============================== 1 passed in 0.20s ===============================
view raw shell.sh hosted with ❤ by GitHub

By restructuring the fixtures this way, we have made code easier to maintain. By simply injecting fixtures, we can maintain a lot of complex model fixtures in a much simpler way.

Let's say we need to add an example where product one and product two will be sold by retail shop "ABC". This can be easily achieved by injecting retailer fixtures into the product fixture.

import pytest
from product.models import Category, Product, Retail
@pytest.fixture
def category(db) -> Category:
return Category.objects.create(name="Books")
@pytest.fixture
def retailer_abc(db):
return Retail.objects.create(name="ABC")
@pytest.fixture
def product_one(db, category, retailer_abc):
product = Product.objects.create(name="Book 1", category=category)
product.retails.add(retailer_abc)
return product
def test_product_retailer(db, retailer_abc, product_one):
assert product_one.retails.filter(name=retailer_abc.name).exists()
view raw test_models.py hosted with ❤ by GitHub

Autouse Fixtures

Sometimes, you may want to have a fixture (or even several) that you know all your tests will depend on. “Autouse” fixtures are a convenient way of making all tests automatically request them. This can cut out a lot of redundant requests, and can even provide more advanced fixture usage.

We can make a fixture an autouse fixture by passing in autouse=True to the fixture’s decorator. Here’s a simple example of how they can be used:

import pytest
from product.models import Category, Product, Retail
...
@pytest.fixture
def retailer_abc(db):
return Retail.objects.create(name="ABC")
@pytest.fixture
def retailers(db) -> list:
return []
@pytest.fixture(autouse=True)
def append_retailers(retailers, retailer_abc):
return retailers.append(retailer_abc)
@pytest.fixture
def product_one(db, category, retailers):
product = Product.objects.create(name="Book 1", category=category)
product.retails.set(retailers)
return product
def test_product_retailer(db, retailer_abc, product_one):
assert product_one.retails.filter(name=retailer_abc.name).exists()
view raw test_models.py hosted with ❤ by GitHub

In this example, the append_retailers fixture is an autouse fixture. Because it happens automatically, test_product_retailer is affected by it, even though the test did not request it. That doesn’t mean they can’t be requested though; just that it isn’t necessary.

Factories as Fixtures

So far, we have created objects with a small number of arguments. However, practically models are a bit more complex and may require more inputs. Let's say we will need to store the sku, mrp, and weight information along with name and category.

If we decide to provide every input to the product fixture, then the logic inside the product fixtures will get a little complicated.

import random
import string
import pytest
from product.models import Category, Product, Retail
@pytest.fixture
def category(db) -> Category:
return Category.objects.create(name="Books")
@pytest.fixture
def retailer_abc(db):
return Retail.objects.create(name="ABC")
@pytest.fixture
def product_one(db, category, retailer_abc):
sku = "".join(random.choices(string.ascii_uppercase + string.digits, k=6))
product = Product.objects.create(
sku=sku,
name="Book 1",
description="A book for educational purpose.",
mrp="100.00",
is_available=True,
category=category,
)
product.retails.set([retailer_abc])
return product
@pytest.fixture
def product_two(db, category, retailer):
sku = "".join(random.choices(string.ascii_uppercase + string.digits, k=6))
product = Product.objects.create(
sku=sku,
name="Book 2",
description="A book with thriller story.",
mrp="50.00",
is_available=True,
category=category,
)
product.retails.add([retailer])
return product
view raw test_models.py hosted with ❤ by GitHub

Product creation has a somewhat complex logic of managing retailers and generating unique SKU. And the product creation logic will grow as we keep adding requirements. There may be some extra logic needed if we consider discounts and coupon code complexity for every retailer. There may also be a lot of versions of the product instance we may want to test against, and you have already learned how difficult it is to maintain such a complex code.

The “factory as fixture” pattern can help in these cases where the same class instance is needed for different tests. Instead of returning an instance directly, the fixture will return a function, and upon calling which one, you can get the distance that you wanted to test.

import random
import string
import pytest
from product.models import Category, Product, Retail
@pytest.fixture
def category(db) -> Category:
return Category.objects.create(name="Books")
@pytest.fixture
def retailer_abc(db):
return Retail.objects.create(name="ABC")
@pytest.fixture
def product_factory(db, category, retailer_abc):
def create_product(
name, description="A Book", mrp=None, is_available=True, retailers=None
):
if retailers is None:
retailers = []
sku = "".join(random.choices(string.ascii_uppercase +
string.digits, k=6))
product = Product.objects.create(
sku=sku,
name=name,
description=description,
mrp=mrp,
is_available=is_available,
category=category,
)
product.retails.add(retailer_abc)
if retailers:
product.retails.set(retailers)
return product
return create_product
@pytest.fixture
def product_one(product_factory):
return product_factory(name="Book 1", mrp="100.2")
@pytest.fixture
def product_two(product_factory):
return product_factory(name="Novel Book", mrp="51")
def test_product_retailer(db, retailer_abc, product_one):
assert product_one.retails.filter(name=retailer_abc.name).exists()
def test_product_one(product_one):
assert product_one.name == "Book 1"
assert product_one.is_available
view raw test_models.py hosted with ❤ by GitHub

This is not far from what you’ve already done, so let’s break it down:

  • The category and retailer_abc fixture remains the same.
  • A new product_factory fixture is added, and it is injected with the category  and  retailer_abc fixture.
  • The fixture product_factory creates a wrapper and returns an inner function called create_product.
  • Inject product_factory into another fixture and use it to create a product instance

The factory fixture works similar to how decorators work in python.

Sharing Fixtures Using Scopes

Fixtures requiring network or db access depend on connectivity and are usually time-expensive to create. In the previous example, every time we request any fixture within our tests, it is used to run the method, generate an instance and pass them to the test. So if we have written ‘n’ tests, and every test calls for the same fixture then that fixture instance will be created n times during the entire execution.

This is mainly happening because fixtures are created when first requested by a test, and are destroyed based on their scope:

  • Function: the default scope, the fixture is destroyed at the end of the test.
  • Class: the fixture is destroyed during the teardown of the last test in the class.
  • Module: the fixture is destroyed during teardown of the last test in the module.
  • Package: the fixture is destroyed during teardown of the last test in the package.
  • Session: the fixture is destroyed at the end of the test session.

In the previous example, we can add scope="module" so that the category, retailer_abc, product_one, and product_two instances will only be invoked once per test module.

Multiple test functions in a test module will thus each receive the same category, retailer_abc, product_one, and product_two fixture instance, thus saving time.

@pytest.fixture(scope="module")
def category(db) -> Category:
return Category.objects.create(name="Books")
@pytest.fixture(scope="module")
def retailer_abc(db):
return Retail.objects.create(name="ABC")
@pytest.fixture(scope="module")
def product_one(product_factory):
return product_factory(name="Book 1", mrp="100.2")
@pytest.fixture(scope="module")
def product_two(product_factory):
return product_factory(name="Novel Book", mrp="51")
view raw test_models.py hosted with ❤ by GitHub

This is how we can add scope to the fixtures, and you can do it for all the fixtures.

But, If we try to test this in the terminal, we will encounter an error:

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
django: settings: pytest_fixtures.settings (from ini)
rootdir: /home/suraj/PycharmProjects/e_commerce_app, configfile: pytest.ini
plugins: django-4.1.0
collected 2 items
product/tests/test_models.py EE [100%]
==================================== ERRORS ====================================
___________________ ERROR at setup of test_product_retailer ____________________
ScopeMismatch: You tried to access the 'function' scoped fixture 'db' with a 'module' scoped request object, involved factories
product/tests/test_models.py:13: def retailer_abc(db) -> product.models.Category
venv/lib/python3.7/site-packages/pytest_django/fixtures.py:193: def db(request, django_db_setup, django_db_blocker)
______________________ ERROR at setup of test_product_one ______________________
ScopeMismatch: You tried to access the 'function' scoped fixture 'db' with a 'module' scoped request object, involved factories
...
============================== 2 errors in 0.24s =============================== [100%]
view raw shell.sh hosted with ❤ by GitHub

The reason for this error is that the db fixture has the function scope for a reason, so the transaction rollbacks on the end of each test ensure the database is left in the same state it has when the test starts. Nevertheless, you can have the session/module scoped access to the database in the fixture by using the django_db_blocker fixture:

import random
import string
import pytest
from product.models import Category, Product, Retail
@pytest.fixture(scope="module")
def category(django_db_blocker):
with django_db_blocker.unblock():
return Category.objects.create(name="Books")
@pytest.fixture(scope="module")
def retailer_abc(django_db_blocker):
with django_db_blocker.unblock():
return Retail.objects.create(name="ABC")
@pytest.fixture(scope="module")
def product_factory(django_db_blocker, category, retailer_abc):
def create_product(
name, description="A Book", mrp=None, is_available=True, retailers=None
):
if retailers is None:
retailers = []
sku = "".join(random.choices(
string.ascii_uppercase + string.digits, k=6)
)
with django_db_blocker.unblock():
product = Product.objects.create(
sku=sku,
name=name,
description=description,
mrp=mrp,
is_available=is_available,
category=category,
)
product.retails.add(retailer_abc)
if retailers:
product.retails.set(retailers)
return product
return create_product
@pytest.fixture(scope="module")
def product_one(product_factory):
return product_factory(name="Book 1", mrp="100.2")
@pytest.fixture(scope="module")
def product_two(product_factory):
return product_factory(name="Novel Book", mrp="51")
def test_product_retailer(db, retailer_abc, product_one):
assert product_one.retails.filter(name=retailer_abc.name).exists()
def test_product_one(product_one):
assert product_one.name == "Book 1"
assert product_one.is_available
view raw test_models.py hosted with ❤ by GitHub

Now, if we go to the terminal and run the tests, it will run successfully.

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
django: settings: pytest_fixtures.settings (from ini)
rootdir: /home/suraj/PycharmProjects/e_commerce_app, configfile: pytest.ini
plugins: django-4.1.0
collected 2 items
product/tests/test_models.py .. [100%]
============================== 2 passed in 0.22s ===============================
view raw shell.sh hosted with ❤ by GitHub

Warning: Beware that when unlocking the database in session scope, you're on your own if you alter the database in other fixtures or tests.

Conclusion

We have successfully learned various features pytest fixtures provide and how we can benefit from the code reusability perspective and have maintainable code in your tests. Dependency management and arranging your test data becomes easy with the help of fixtures.

This was a blog about how you can use fixtures and the various features it provides along with the Django models. You can check more on fixtures by referring to the official documentation.

Did you like the blog? If yes, we're sure you'll also like to work with the people who write them - our best-in-class engineering team.

We're looking for talented developers who are passionate about new emerging technologies. If that's you, get in touch with us.

Explore current openings