Een Wagtail site testen met Factory Boy

Om een site te testen, moet deze enige inhoud bevatten. In deze tutorial testen we een Wagtail-site waar de content wordt gegenereerd met Factory Boy.

13 juli 2020 17:48
Thema's: Wagtail Streamfield opties Testen

In een eerdere tutorial hebben we een authenticatie-app getest. De opzet van de testgegevens bestond louter uit het definiëren van een aantal gebruikers. Bij een meer gecompliceerde app, zoals een Wagtail-site, moeten er meer gegevens worden ingesteld: gebruikers, pagina's, een menu, andere items die we willen testen. Sommige van deze elementen zijn van elkaar afhankelijk. Het is mogelijk om alle testinstanties expliciet te definiëren, zoals we hebben gedaan met de authenticatie-app, maar dat is niet erg flexibel. Een andere strategie is om fixtures te gebruiken: gegevens van een vorige of demo-site die in een bestand worden gedumpt en opnieuw worden geladen voor de test. Een nadeel kan zijn dat het fixtures-bestand groot is en niet gemakkelijk te begrijpen of te onderhouden, of dat het tijd kost om een ​​goed fixtures-bestand te creëren.

Een alternatief is om Factory Boy te gebruiken, een tool om testdata te genereren. Factory Boy zet 'fabrieken' op voor modelinstanties, waarin bepaalde eigenschappen, relaties of sequenties vooraf kunnen worden gedefinieerd, zoals ouder-kindrelaties, waarbij nieuwe instanties worden gegenereerd wanneer de fabriek meerdere keren wordt aangeroepen. Zodra de fabrieken zijn opgezet, wordt het definiëren van de testgegevens aanzienlijk eenvoudiger en veel flexibeler. Een andere optie zou zijn geweest om Wagtail Factories te gebruiken, dat een aantal kant-en-klare opties biedt.

Begin met het installeren van Factory Boy en plaats het in requirements.txt:

pip3 install factory_boy

Omdat we een behoorlijk aantal tests verwachten, maken we een directory /tests in onze app, waarin we al onze testbestanden zullen plaatsen. We moeten een __init__.py-bestand maken om de directory vindbaar te maken voor Django's testrunner. Maak nu een bestand factories.py waarin we al onze fabrieken zullen plaatsen.

Factory Boy heeft een speciale klasse voor Django-modellen. Het aanmaken van een fabriek voor een model gaat als volgt:

import factory
from ..models import Theme

class ThemeFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Theme

Het maken van een instantie van Theme is zo simpel als:

theme1 = ThemeFactory()

Ons model Theme heeft slechts één veld, genaamd name. We kunnen Factory Boy de opdracht geven om automatisch een nieuwe naam te genereren voor elke keer dat er een nieuwe Theme-instantie wordt gemaakt, door middel van sequences. Definieer voor de bovenstaande klasse ThemeFactory een veld name:

name = factory.Sequence(lambda n: 'Theme number {0}'.format(n))

De eerste instantie die wordt gemaakt, heeft de naam Theme number 0, de tweede Theme number 1, enz. We gebruiken de Python functie str.format() in plaats van de opmaak in printf-stijl.

Voor eenvoudige modellen werkt dit prima. Wagtail-pagina's hebben echter ouder-kindrelaties, dus om zoiets als een PageFactory te creëren, hebben we een manier nodig om dit automatisch vast te stellen. Dit kan worden gedaan door de _create-methode te overschrijven. Onze strategie is om een extra argument toe te voegen met key parent, dat verwijst naar de ouderpagina van de pagina die we willen maken. Onze fabriek is dan:

class PageFactory(factory.django.DjangoModelFactory):
    class Meta:
        abstract = True

    # override the _create method, to establish parent-child relationship between pages
    @classmethod
    def _create(cls, model_class, *args, **kwargs):

        try:
            parent = kwargs.pop('parent')
        except KeyError:
            # no parent, appending page to root
            parent = Page.get_first_root_node()

        page = model_class(*args, **kwargs)
        parent.add_child(instance=page)

        return page

We stellen abstract = True om aan te geven dat deze fabriek abstract is. In de _create-methode zoeken we naar een trefwoord parent in de argumenten, en als dat er is, dan voegen we de te maken pagina als kind toe aan deze parent. Als er geen parent argument is, dan voegen we de aan te maken pagina toe aan de root. Om de root te vinden, gebruiken we de methode get_first_root_node, beschikbaar via een voorouder van een Wagtail-pagina.

We creëren een fabriek voor homepages afgeleid van PageFactory:

from ..models import HomePage

class HomePageFactory(PageFactory):
    class Meta:
        model = HomePage

    title = factory.Sequence(lambda n: 'Home page {0}'.format(n))

en exact dezelfde code kan worden gebruikt om andere Page-klassen te maken, in ons geval een ThemePageFactory, ThemeIndexPageFactory, ArticleIndexPageFactory (vervang gewoon Home door Theme etc.). Voor een ArticlePageFactory hebben we twee extra dingen nodig. Allereerst zorgen we ervoor dat artikelpagina's verschillende velden first_published_at hebben, omdat we willen testen of de volgorde correct is. Dit wordt bereikt door opnieuw een sequence te gebruiken:

from datetime import timedelta
from django.utils import timezone

first_published_at = factory.Sequence(lambda n: timezone.now() - timedelta(days=365-n))

Ten tweede heeft ons ArticlePage-model een many-to-many relatie met het Theme-model. Deze kan worden gecreëerd met een post generation hook:

@factory.post_generation
def themes(self, create, extracted, **kwargs):
    if not create:
        # Simple build, do nothing.
        return

    if extracted:
        for theme in extracted:
            self.themes.add(theme)

Dit stelt ons in staat om een tupel van thema's toe te voegen aan de te creëren pagina als relaties met de specifieke pagina.

Dat zijn alle fabrieken die we nodig hebben om de tests te schrijven. Maak in de directory /tests een bestand test_pages.py aan. We definiëren onze initiële data met behulp van setUpTestData, net zoals we deden in de vorige tutorial. De eerste twee objecten die moeten worden gemaakt, zijn een request factory en een site:

from django.test import TestCase, RequestFactory
from wagtail.core.models import Page, Site

class TestPageModels(TestCase):

    @classmethod
    def setUpTestData(cls):
        cls.factory = RequestFactory()
        # Wagtail needs a site, e.g. to define url
        # Page.get_first_root_node() is used by Wagtail to find the root page
        cls.site = Site.objects.create(is_default_site=True, root_page=Page.get_first_root_node())

We hebben de request factory nodig in onze tests. Wagtail heeft ook een Site-instantie nodig, b.v. bij het construeren van de url van een pagina (zie Page- en Site-model). Nu zijn we klaar om pagina-instanties te bouwen met behulp van onze fabrieken. We maken een homepage, een themeindexpage en een articleindexpage (beide kinderen van de homepage), twee thema's en twee artikelpagina's als child van de articleindexpage. We definiëren ook enkele many-to-many relaties en stellen een paar velden in.

cls.homepage = HomePageFactory()
cls.articleindexpage = ArticleIndexPageFactory(parent=cls.homepage)
cls.themeindexpage = ThemeIndexPageFactory(parent=cls.homepage)
cls.theme1 = ThemeFactory()
cls.theme2 = ThemeFactory()
cls.themepage1 = ThemePageFactory(theme=cls.theme1, parent=cls.themeindexpage)
cls.themepage2 = ThemePageFactory(theme=cls.theme2, parent=cls.themeindexpage)
cls.articlepage1 = ArticlePageFactory(parent=cls.articleindexpage, themes=(cls.theme1, cls.theme2,), featured=True)
cls.articlepage2 = ArticlePageFactory(parent=cls.articleindexpage, themes=(cls.theme1,))

Zoals je ziet, kan het definiëren van instanties met slechts één regel nu we onze fabrieken hebben gedefinieerd. Als we meer instanties nodig hebben voor meer tests, kan dit eenvoudig. Laten we onze eerste test schrijven: controleer of de artikelen op een themapagina in omgekeerde chronologische volgorde staan:

def test_articles_on_themepage_are_in_antichronological_order(self):
    articlepageslist = self.themepage1.articlepages()
    publisheddatelist = [articlepage.first_published_at for articlepage in articlepageslist]
    self.assertTrue(publisheddatelist == sorted(publisheddatelist, reverse=True))

We gebruiken de methode articlepages() van een themapagina, extraheren de datums en controleren of ze in omgekeerde chronologische volgorde staan. We voeren de test uit met:

python3 manage.py test cms.tests.test_pages

Dezelfde test voor artikelen op de articleindexpage heeft bijna dezelfde code:

def test_articles_on_articleindexpage_are_in_antichronological_order(self):
    articlepageslist = self.articleindexpage.articlepages()
    publisheddatelist = [articlepage.first_published_at for articlepage in articlepageslist]
    self.assertTrue(publisheddatelist == sorted(publisheddatelist, reverse=True))

Nog enkele tests: de werking van het featured veld, de methode themepages() en de methode get_absolute_url() van een artikelpagina:

def test_featured_articles_on_articleindexpage_get_set(self):
    self.assertEqual(len(self.articleindexpage.featured_articlepages()), 1)

def test_themepages_of_articlepage(self):
    themepageslist = self.articlepage1.themepages()
    self.assertEqual(len(themepageslist), 2)
    self.assertEqual(themepageslist[0].url, self.themepage1.url)

def test_get_absolute_url_of_article_page(self):
    self.assertEqual(self.articlepage1.get_absolute_url(), self.articlepage1.url)

Onze laatste test betreft de serve() methode van de artikelpagina. Daarvoor hebben we een request nodig, dat we zullen maken met de request factory instantie die we hebben gedefinieerd. De serve() methode zal die request veranderen in een TemplateResponse-object, maar omdat de RequestFactory geen middleware ondersteunt, is niet alle functionaliteit van de respons beschikbaar; het status_code veld echter. wel Naar het voorbeeld in de Django-documentatie:

def test_article_page_is_served(self):
    # first create get request to page (serve method needs it)
    request = self.factory.get(self.articlepage1.url)
    # page serve method will return TemplateResponse object (but with only HttpResponse attributes, due to limitations of RequestFactory)
    response = self.articlepage1.serve(request)
    # this will have status code 200, if the page exists
    self.assertEqual(response.status_code, 200)

Laten we nu eens kijken hoeveel van onze code al wordt gedekt door de voorgaande tests. Net als in onze vorige tutorial gebruiken we daarvoor Coverage:

coverage run --source=cms manage.py test cms
coverage html

Als we index.html in onze browser openen zien we dat alle paginagerelateerde klassen in onze modellen behoorlijk goed worden gedekt. Tijd om verder te gaan met het testen van menu's en talen.

Reageer op dit artikel (log eerst in of bevestig hieronder met naam en email)