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 ...............
Top comments (0)