pythondjangopython-unittestdjango-testingdjango-unittest

A more elegant approach to writing Django’s unit tests


I am currently writing tests using Django’s unit tests (based on Python standard library module: unittest). I have written this test for my Contact model which passes:

class ContactTestCase(TestCase):
    def setUp(self):
        """Create model objects."""
        Contact.objects.create(
            name='Jane Doe',
            email='janedoe@gmail.com',
            phone='+2348123940567',
            subject='Sample Subject',
            message='This is my test message for Contact object.'
        )


    def test_user_can_compose_message(self):
        """ Test whether a user can compose a messsage in the contact form."""
        test_user = Contact.objects.get(name='Jane Doe')
        self.assertEqual(test_user.email, 'janedoe@gmail.com')
        self.assertEqual(test_user.phone, '+2348123940567')
        self.assertEqual(test_user.subject, 'Sample Subject')
        self.assertEqual(test_user.message, 'This is my test message for Contact object.')
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.005s

OK
Destroying test database for alias 'default'...

However, I had to use the assertEqual method 4 times in this test (could be more when testing models with more fields). It seems like this doesn't follow the DRY principle.

I know from the docs that assertEqual(first, second, msg=None) tests that first and second are equal. If the values do not compare equal, the test will fail.

Is there a workaround or a more elegant approach to writing such tests?


Solution

  • The perfect question! Welcome to community of the quality tests!

    1. Create a list/dict with testcases.
    2. Go through the list and call assert in the context of the subtest.
    3. Enjoy.

    Example here:

    def test_user_can_compose_message(self):
        """ Test whether a user can compose a messsage in the contact form."""
        test_user = Contact.objects.get(name='Jane Doe')
        test_cases = {'email': 'janedoe@gmail.com', 'phone': '+2348123940567', 'subject': 'Sample Subject', 'message': 'This is my test message for Contact object.'}
        for field, value in test_cases.items():
            with self.subTest("wrong user subtest", field=field, ):
                self.assertEqual(getattr(test_user, field), value)
    

    Subtest allows you to mark a section of your test as a separate test using a context manager. The canonical example is testing in a loop.

    Without the subTest, test would fail immediately on the first wrong assert, reporting that test has failed. With the Subtest context manager the failures in each subTest's assert don't cause the test to exit. The result of this test is that you'll see successes reported for some loop-steps with failures reported for other. You can setup Subtest message, they will be reported as part of the test failure.

    More here: https://docs.python.org/3/library/unittest.html#distinguishing-test-iterations-using-subtests

    Also i like this article: https://blog.ganssle.io/articles/2020/04/subtests-in-python.html

    By the way, don't forget to use factory boy and try to avoid hardcoded constants. Instead you can use django.utils.crypto.render_string etc.