Search code examples
djangopytestdjango-managers

pytest: how to avoid repetition in custom User manager testing


I'm testing a custom user manager with pytest and factory_boy. I want to test those cases where information required to create a new user is incomplete, but I have different required parameters at the moment there are 3 (email, username, identification_number) but there may be more in the future.

Manager

class UserManager(BaseUserManager):
    """ Define a manager for custom User model. """

    def create_user(
        self,
        email: str,
        username: str,
        identification_number: str,
        password: Optional[str] = None,
        is_active: bool = True,
        is_staff: bool = False,
        is_admin: bool = False,
    ) -> User:
        """ Creates and saves a User. """
        if not email:
            raise ValueError(_("Users must have an email address."))
        if not username:
            raise ValueError(_("Users must have a username."))
        if not identification_number:
            raise ValueError(_("Users must have an identification number."))
        user = self.model(email=self.normalize_email(email))
        user.set_password(password)
        user.username = username
        user.identification_number = identification_number
        user.active = is_active
        user.staff = is_staff
        user.admin = is_admin
        user.save(using=self._db)
        return user

Tests

import pytest
from django.contrib.auth import get_user_model

from my_app.users.tests.factories import UserFactory

pytestmark = pytest.mark.django_db

class TestsUsersManagers:
    def test_user_with_no_email(self):
        proto_user = UserFactory.build()  # User created with factory boy 
        User = get_user_model()
        with pytest.raises(TypeError):
            User.objects.create_user()
        with pytest.raises(TypeError):
            User.objects.create_user(
                username=proto_user.username,
                identification_number=proto_user.identification_number,
                password=proto_user._password,
            )
        with pytest.raises(ValueError):
            User.objects.create_user(
                email="",
                username=proto_user.username,
                identification_number=proto_user.identification_number,
                password=proto_user._password,
            )

    def test_user_with_no_username(self):
        proto_user = UserFactory.build()
        User = get_user_model()
        with pytest.raises(TypeError):
            User.objects.create_user()
        with pytest.raises(TypeError):
            User.objects.create_user(
                email=proto_user.email,
                identification_number=proto_user.identification_number,
                password=proto_user._password,
            )
        with pytest.raises(ValueError):
            User.objects.create_user(
                email=proto_user.email,
                username="",
                identification_number=proto_user.identification_number,
                password=proto_user._password,
            )

    def test_user_with_no_identification_number(self):
        proto_user = UserFactory.build()
        User = get_user_model()
        with pytest.raises(TypeError):
            User.objects.create_user()
        with pytest.raises(TypeError):
            User.objects.create_user(
                email=proto_user.email,
                username=proto_user.username,
                password=proto_user._password,
            )
        with pytest.raises(ValueError):
            User.objects.create_user(
                email=proto_user.email,
                username=proto_user.username,
                identification_number="",
                password=proto_user._password,
            )

The problem

There is a lot of repeated code, and since the number of parameters required may increase, I should repeat the same test for those additional parameters over and over again.


Solution

  • You can use a helper method and a fixture to reduce the repeated code. Following code block is an example of this approach.

    import pytest
    from django.contrib.auth import get_user_model
    
    from my_app.users.tests.factories import UserFactory
    
    pytestmark = pytest.mark.django_db
    
    class TestsUsersManagers:
    
        @pytest.fixture
        def proto_user(self):
            return UserFactory.build()
    
        def helper(self, username, password, email, identification_number):
            User = get_user_model()
            with pytest.raises(TypeError):
                User.objects.create_user()
            params = {'password': password}
            if username:
                params['username'] = username
            if email:
                params['email'] = email
            if identification_number:
                params['identification_number'] = identification_number
            with pytest.raises(TypeError):
                User.objects.create_user(**params)
            with pytest.raises(ValueError):
                User.objects.create_user(
                    email=email,
                    username=username,
                    identification_number=identification_number,
                    password=password,
                )
    
        def test_user_with_no_email(self, proto_user):
            self.helper(proto_user.username, proto_user._password, "", proto_user.identification_number)
    
        def test_user_with_no_username(self, proto_user):
            self.helper("", proto_user._password, proto_user.email, proto_user.identification_number)
    
        def test_user_with_no_identification_number(self, proto_user):
            self.helper(proto_user.username, proto_user._password, proto_user.email, "")
    

    Or you can write parametrize test also. Like the following,

    import pytest
    from django.contrib.auth import get_user_model
    
    from my_app.users.tests.factories import UserFactory
    
    pytestmark = pytest.mark.django_db
    
    class TestsUsersManagers:
    
        def helper(self, username, password, email, identification_number):
            User = get_user_model()
            with pytest.raises(TypeError):
                User.objects.create_user()
            params = {'password': password}
            if username:
                params['username'] = username
            if email:
                params['email'] = email
            if identification_number:
                params['identification_number'] = identification_number
            with pytest.raises(TypeError):
                User.objects.create_user(**params)
            with pytest.raises(ValueError):
                User.objects.create_user(
                    email=email,
                    username=username,
                    identification_number=identification_number,
                    password=password,
                )
    
        @pytest.mark.parametrize("username,password,email,identification_number", [
            ("username", "password", "", "identification_number"),
            ("", "password", "email@email.com", "identification_number"),
            ("username", "password", "email@email.com", ""),
        ])
        def test_user(self, username, password, email, identification_number):
            self.helper(username, password, email, identification_number)
    

    Note: I don't have chance to run the code. So you may need to modify them.