DEV Community

Lina Rudashevski
Lina Rudashevski

Posted on

13 5

Writing Tests with pytest and pytest-factoryboy

Testing in python can be tricky so I've outlined what works for me. I like to use pytest with pytest-factoryboy to generate testing data. pytest-factoryboy is an easier way to test than having to maintain fixtures in a test database.

Define the factories. They look very similar to models.

# tests/factories.py 

import factory
from faker import Factory as FakerFactory

from django.contrib.auth.models import User
from django.utils.timezone import now

from app.models import Game, Message, GamePlayer, Round, Move

faker = FakerFactory.create()


class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    email = factory.Faker("safe_email")
    username = factory.LazyAttribute(lambda x: faker.name())

    @classmethod
    def _prepare(cls, create, **kwargs):
        password = kwargs.pop("password", None)
        user = super(UserFactory, cls)._prepare(create, **kwargs)
        if password:
            user.set_password(password)
            if create:
                user.save()
        return user


class GameFactory(factory.django.DjangoModelFactory):
    room_name = factory.LazyAttribute(lambda x: faker.name())
    game_status = "active"
    created_at = factory.LazyAttribute(lambda x: now())
    updated_at = factory.LazyAttribute(lambda x: now())
    round_started = False
    is_joinable = True

    class Meta:
        model = Game


class GamePlayerFactory(factory.django.DjangoModelFactory):
    followers = 0
    selfies = 3
    user = factory.SubFactory(UserFactory)
    started = False
    game = factory.SubFactory(GameFactory)

    class Meta:
        model = GamePlayer


class MessageFactory(factory.django.DjangoModelFactory):
    game = factory.SubFactory(GameFactory)
    username = None
    message = factory.LazyAttribute(lambda x: faker.sentence())
    created_at = factory.LazyAttribute(lambda x: now())
    message_type = None

    class Meta:
        model = Message

# etc 

In a conftest.py file I register the factories.

Fixtures defined in conftest.py file will be recognized in any test file. "Registering" the factories lets you inject them into your tests with a lowercase-underscore class name. In this same file, I'm defining fixtures with @pytest.fixture() which I'll use in my tests, and injecting them with the factories I just registered.

# tests/conftest.py 
import pytest
from pytest_factoryboy import register

from .factories import (
    UserFactory,
    GameFactory,
    GamePlayerFactory,
    MoveFactory,
    RoundFactory,
)

register(UserFactory)
register(GameFactory)
register(GamePlayerFactory)
register(MoveFactory)
register(RoundFactory)


@pytest.fixture()
def game(game_factory):
    return game_factory()


@pytest.fixture()
def rnd(game, round_factory):
    return round_factory(game=game, started=True)


@pytest.fixture()
def p_1(game, user_factory, game_player_factory):
    return game_player_factory(game=game, user=user_factory(), started=True)

#etc...more player fixtures defined

Write tests

Here I'm writing tests. I'm passing in my fixture data into my test method, and also passing in a factory called move because that's what I'm mainly testing here. In order to test player moves, I first needed to have created the players, the game, and the round, which is what I did in my conftest.py file. I only need to do this once, and I don't need to import the fixtures because anything declared in conftest.py is visible to the test files.

# tests/test_round_service.py

import pytest

from app.services.round_service import (
    RoundTabulation,
    LEAVE_COMMENT,
    CALL_IPHONE,
    DISLIKE,
    GO_LIVE,
    POST_SELFIE,
    DONT_POST,
    NO_MOVE,
    DISLIKE_DM,
    POST_SELFIE_PTS,
    POST_SELFIE_DM,
    GO_LIVE_DM,
    NO_MOVE_DM,
)
from app.services import round_service, message_service
from app.models import Move, Message, GamePlayer


@pytest.mark.django_db
def test_iphone_go_live_do_selfie(rnd, p_1, p_2, p_3, move_factory):
    """if a girl calls a girl who is trying to take a selfie, she sustains go
    live damage"""
    move_factory(round=rnd, action_type=GO_LIVE, player=p_1, victim=None)
    move_factory(round=rnd, action_type=CALL_IPHONE, player=p_2, victim=p_3)
    move_factory(round=rnd, action_type=POST_SELFIE, player=p_3, victim=None)
    tab = RoundTabulation(rnd).tabulate()
    assert tab[p_1.id] == 0
    assert tab[p_2.id] == GO_LIVE_DM
    assert tab[p_3.id] == GO_LIVE_DM


@pytest.mark.django_db
def test_go_live_with_call(rnd, p_1, p_2, p_3, p_4, p_5, move_factory):
    """message was created, points are corrected, player is removed from player
    points array, they have one less story"""
    move_factory(round=rnd, action_type=GO_LIVE, player=p_1, victim=None)
    move = move_factory(round=rnd, action_type=CALL_IPHONE, player=p_2, victim=p_1)
    move_factory(round=rnd, action_type=DISLIKE, player=p_3, victim=p_1)
    move_factory(round=rnd, action_type=DISLIKE, player=p_4, victim=p_1)
    move_factory(round=rnd, action_type=LEAVE_COMMENT, player=p_5, victim=p_1)
    tab = RoundTabulation(rnd).tabulate()
    assert message(rnd.game, p_2.user.username) in message_service.iphone_msg(
        move, p_1.user.username
    )
    p_1 = GamePlayer.objects.get(id=p_1.id)
    assert p_1.go_live == 1
    assert tab[p_1.id] == -50

Execute the tests

Now your tests can run with test data!

[17:59:32] (master) selfies
πŸ™‹ docker exec -it 5c93db3f3183 bash
root@5c93db3f3183:/selfies# pytest app/tests/test_round_service.py
======================================= test session starts ========================================
platform linux -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
Django settings: selfies.settings (from ini file)
rootdir: /selfies, inifile: pytest.ini
plugins: factoryboy-2.0.3, django-3.5.1
collected 15 items                                                                                 

app/tests/test_round_service.py ...............

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (0)

AWS GenAI LIVE image

How is generative AI increasing efficiency?

Join AWS GenAI LIVE! to find out how gen AI is reshaping productivity, streamlining processes, and driving innovation.

Learn more

πŸ‘‹ Kindness is contagious

Please leave a ❀️ or a friendly comment on this post if you found it helpful!

Okay