18.4. Tests Models

  • Split tests into multiple files

  • myproject/shop/tests/__init__.py must be present for tests to be discovered (can be empty)

  • Use TestCase classes to split tests into logical groups

  • In order to use fixtures create a fixtures directory with a .json file for each model

18.4.1. Example

Code 18.1. File myproject/shop/models/customer.py
from datetime import date
from django.db import models
from django.utils.translation import gettext_lazy as _


class Customer(models.Model):
    firstname = models.CharField(verbose_name=_('First Name'), max_length=100)
    lastname = models.CharField(verbose_name=_('Last Name'), max_length=100, db_index=True)
    birthdate = models.DateField(verbose_name=_('Birthdate'), null=True, blank=True, default=None)

    @property
    def emails(self):
        from shop.models import Email
        return Email.objects.filter(customer=self)

    @property
    def name(self):
        return f'{self.firstname} {self.lastname}'

    @property
    def age(self):
        if not self.birthdate:
            return None
        year = 365.25
        difference = date.today() - self.birthdate
        return int(difference.days / year)

    def __str__(self):
        return f'{self.firstname} {self.lastname}'

    class Meta:
        app_label = 'shop'
        verbose_name = _('Customer')
        verbose_name_plural = _('Customers')
Code 18.2. File myproject/shop/tests/test_models_customer.py
from datetime import date
from unittest.mock import patch
from django.test import TestCase
from shop.models import Customer, Email, EmailType


class CustomerCreateTest(TestCase):
    def test_customer_with_firstname_and_lastname(self):
        customer = Customer.objects.create(firstname='Mark', lastname='Watney')
        self.assertEqual(customer.firstname, 'Mark')
        self.assertEqual(customer.lastname, 'Watney')

    def test_customer_with_birthdate(self):
        customer = Customer.objects.create(firstname='Mark', lastname='Watney', birthdate=date(2000, 1, 2))
        self.assertEqual(customer.birthdate, date(2000, 1, 2))


class CustomerFunctionalityTest(TestCase):
    # fixtures = ['customer.json']

    def setUp(self):
        self.customer = Customer.objects.create(firstname='Mark', lastname='Watney')
        self.email1 = Email.objects.create(customer=self.customer, type=EmailType.WORK, address='mwatney@nasa.gov')
        self.email2 = Email.objects.create(customer=self.customer, type=EmailType.HOME, address='mwatney@gmail.com')

    def test_customer_emails(self):
        self.assertEqual(self.customer.emails.count(), 2)
        self.assertListEqual(list(self.customer.emails), [self.email1, self.email2])

    def test_customer_name(self):
        self.assertEqual(self.customer.name, 'Mark Watney')

    def test_customer_str(self):
        self.assertEqual(str(self.customer), 'Mark Watney')

    def test_customer_age_hardcoded(self):
        self.customer.birthdate = date(2000, 1, 2)
        self.assertEqual(self.customer.age, 24)

    def test_customer_age_patch(self):
        self.customer.birthdate = date(2000, 1, 2)
        with patch('shop.models.customer.date') as d:
            d.today.return_value = date(2024, 1, 2)
            self.assertEqual(self.customer.age, 24)

18.4.2. Use Case - 1

Code 18.3. File myproject/shop/models/email.py
from django.db import models
from django.utils.translation import gettext_lazy as _


class EmailType(models.TextChoices):
    HOME = 'home', _('Home')
    WORK = 'work', _('Work')


class Email(models.Model):
    customer = models.ForeignKey(verbose_name=_('Customer'), to='shop.Customer', on_delete=models.CASCADE, null=False, blank=False, db_index=True)
    type = models.CharField(verbose_name=_('Type'), max_length=100, choices=EmailType, null=False, blank=False, default=EmailType.HOME)
    address = models.EmailField(verbose_name=_('Address'), max_length=100, null=False, blank=False, unique=True)

    def __str__(self):
        return f'{self.customer.name} <{self.address}>'

    class Meta:
        app_label = 'shop'
        verbose_name = _('Email')
        verbose_name_plural = _('Emails')

Code 18.4. File myproject/shop/tests/test_models_email.py
from django.test import TestCase
from shop.models import Customer, Email, EmailType


class EmailCreateTest(TestCase):
    def setUp(self):
        self.customer = Customer.objects.create(firstname='Mark', lastname='Watney')

    def test_email_with_type_default(self):
        Email.objects.create(customer=self.customer, address='mwatney@gmail.com')
        email = Email.objects.get(customer=self.customer)
        self.assertEqual(email.type, EmailType.HOME)

    def test_email_with_type_work(self):
        Email.objects.create(customer=self.customer, type=EmailType.WORK, address='mwatney@nasa.gov')
        email = Email.objects.get(customer=self.customer, type=EmailType.WORK)
        self.assertEqual(email.address, 'mwatney@nasa.gov')
        self.assertEqual(email.type, EmailType.WORK)

    def test_email_with_type_home(self):
        Email.objects.create(customer=self.customer, type=EmailType.HOME, address='mwatney@gmail.com')
        email = Email.objects.get(customer=self.customer, type=EmailType.HOME)
        self.assertEqual(email.address, 'mwatney@gmail.com')
        self.assertEqual(email.type, EmailType.HOME)


class EmailFunctionalityTest(TestCase):
    def setUp(self):
        self.customer = Customer.objects.create(firstname='Mark', lastname='Watney')
        self.email = Email.objects.create(customer=self.customer, address='mwatney@gmail.com')

    def test_email_str(self):
        self.assertEqual(str(self.email), 'Mark Watney <mwatney@gmail.com>')

18.4.3. Further Reading

18.4.4. Assignments

# doctest: +SKIP_FILE
# FIXME: Write tests

# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: Django Tests Shop
# - Difficulty: medium
# - Lines: 33
# - Minutes: 34

# %% English
# 0. Use `myproject.shop`
# 1. Create unit tests for model `Customer`

# %% Polish
# 0. Użyj `myproject.shop`
# 1. Stwórz testy jednostkowe do modelu `Customer`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 10), \
'Python 3.10+ required'
"""

...