Fabryki Factory Boy w testach Django

Factory Boy to narzędzie zastępującego Django fixtures, czy ręczne tworzenie danych w testach. Ułatwia tworzenie danych potrzebnych do testów, jak i łatwe utrzymanie ich wraz z rozwojem projektu i struktury modeli.

Factory Boya znajdziemy na githubie. Dostępna jest też dokumentacja, czy prezentacja na PyWaw.

Fabryki Factory Boy w testach

Instalacja jest standardowa:
pip install factory_boy

Po czym możemy zabrać się za tworzenie fabryk. Można w katalogu aplikacji stworzyć plik factories.py i importować fabryki w pliku z testami (tests.py).

Na potrzeby tego artykułu wykorzystam aplikację użytą w artykule o widokach opartych o klasy. Mamy tam model "Category":

class Category(models.Model):
    title = models.CharField(max_length=150)
Fabryka dla tego modelu wyglądałaby tak:
from demo.models import *
import factory

class CategoryFactory(factory.Factory):
    FACTORY_FOR = Category

    title = 'abc'

Ważny element to FACTORY_FOR - wskazuje model, którego dotyczy. Kolejna część fabryki to wartości jakie zostaną przypisane do poszczególnych atrybutów modelu. Powyżej tytuł każdej tworzonej kategorii ustawiony zostanie na "abc".

Teraz wykorzystajmy fabrykę do prostego testu. Kategorie są wyświetlane na stronie głównej, więc jeżeli stworzymy jedną to jej tytuł powinien znaleźć się w treści strony głównej. Oto tests.py:

from django.test import TestCase

import demo.factories

class DemoAppCategoryTest(TestCase):
    def test_if_category_is_displayed(self):
        category = demo.factories.CategoryFactory.create()

        response = self.client.get('/')
        self.assertTrue(category.title in response.content)

Metoda create tworzy i zwraca obiekt (kategorii). Dalej self.client wykonuje żądanie głównej strony a w assercie sprawdzane jest czy tytuł stworzonej kategorii występuje w zwróconej treści (testy można odpalić za pomocą polecenia python manage.py test NAZWA_APLIKACJI).

Fabryki potrafią więcej. Zazwyczaj dane takie jak nazwa kategorii byłaby unikalna. Dla każdej tworzonej kategorii trzeba by dostarczyć unikalną nazwę/tytuł. Factory Boy ma na to rozwiązanie - sekwencje:

class CategoryFactory(factory.Factory):
    FACTORY_FOR = Category

    title = factory.Sequence(lambda n: 'Category ' + n)

Pierwsza stworzona kategoria dostanie nazwę "Category 0", druga "Category 1" itd. My piszemy sobie testy, a fabryka odwala czarną robotę unikalności nazwy.

W bardziej życiowych modelach będą jakieś relacje, np model "wpisu":

class Entry(models.Model):
    category = models.ForeignKey(Category)
    user = models.ForeignKey(User)
    user_number = models.IntegerField()
    system_number = models.IntegerField()
Fabryki Factory Boy obsługują podfabryki, które można tutaj użyć:
from django.contrib.auth.models import User

class UserFactory(factory.Factory):
    FACTORY_FOR = User

    username = factory.Sequence(lambda n: 'user' + n)


class EntryFactory(factory.Factory):
    FACTORY_FOR = Entry

    category = factory.SubFactory(CategoryFactory)
    system_number = 1
    user_number = 12345
    user = factory.SubFactory(UserFactory)
W tym przykładzie fabryka "wpisu" ma zapewnione wszystkie potrzebne dane - choć nie zawsze takie fabryki będziemy mieć. Dane mogą być także przekazywane w metodzie create. Prosty test dla tej fabryki:
    def test_if_entry_is_displayed(self):
        entry = demo.factories.EntryFactory.create()

        response = self.client.get(reverse('entry-list', kwargs={'category_id': entry.category.id}))
        phrase = '<b>user number</b>: %s' % entry.user_number
        self.assertTrue(phrase in response.content)
Żeby móc logować użytkowników tworzonych przez fabryki potrzebna jest taka oto fabryka użytkownika:
class UserFactory(factory.Factory):
    FACTORY_FOR = User

    username = factory.Sequence(lambda n: 'user' + n)
    password = 'abc'

    @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
Każdy tworzony użytkownik dostanie hasło "abc", co pozwoli ich logować:
user = demo.factories.UserFactory.create()
self.client.login(username=user.username, password='abc')
O samych testach w Django można poczytać w dokumentacji, czy też zobaczyć prezentację na dstegelman-conf-notes.readthedocs.org.
RkBlog

Django, 10 June 2012

Comment article
Comment article RkBlog main page Search RSS Contact