Tester un site Wagtail avec Factory Boy

Pour tester un site, il doit avoir du contenu. Dans cette vidéo, on va tester un site Wagtail où le contenu est généré avec Factory Boy.

13 Juillet 2020 17:48
Thèmes: Options de Wagtail Streamfield Tester

Dans un didacticiel précédent, on a testé une application d'authentification. La configuration des données de test consistait simplement à définir quelques utilisateurs. Une application plus compliquée, comme un site Wagtail, nécessite de configurer plus de données: utilisateurs, pages, menu, autres éléments qu'on veut tester. Certains de ces éléments dépendent les uns des autres. La définition explicite de toutes les instances de test, comme on l'a fait avec l'application d'authentification, est possible mais n'est pas très flexible. Une autre stratégie consiste à utiliser des fixtures: les données d'un site précédent ou de démonstration qui sont sauvegardées dans un fichier et chargées à nouveau avant le test. Un inconvénient peut être que le fichier du fixture est volumineux et difficile à comprendre ou à maintenir, ou qu'il peut prendre un certain temps pour configurer un bon fichier de fixture.

Une alternative consiste à utiliser Factory Boy, un outil pour générer des données de test. Factory Boy configure des 'usines' pour les instances de modèle, dans lesquelles certaines propriétés, relations ou séquences peuvent être prédéfinies, telles que les relations parent-enfant, générant de nouvelles instances lors de l'appel multiple de l'usine, définissant des valeurs par défaut. Une fois les usines installées, la configuration des données de test devient beaucoup plus facile et flexible. Une autre option aurait été d'utiliser Wagtail Factories, qui propose un certain nombre d'options prêtes à l'emploi.

Commencez par installer Factory Boy et placez-le dans requirements.txt:

pip3 install factory_boy

Puisqu'on prévoie avoir un certain nombre de tests, créons un répertoire /tests dans notre application, où on mettra tous nos fichiers de test. On doit créer un fichier __init__.py pour rendre le fichier détectable pour le lanceur de test de Django. Créez maintenant un fichier factories.py où on mettra toutes nos usines.

Factory Boy a une classe spéciale pour les modèles Django. La création d'une usine pour un modèle se fait comme suit:

import factory
from ..models import Theme

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

La création d'une instance de Theme est aussi simple que:

theme1 = ThemeFactory()

Notre modèle Theme n'a qu'un seul champ, appelé name. On peut demander à Factory Boy de générer automatiquement un nouveau nom chaque fois qu'une nouvelle instance de Theme est créée, en utilisant des sequences. Pour la classe ThemeFactory ci-dessus, définissez un champ name:

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

La première instance qui sera créée aura un nom Theme number 0, le deuxième Theme number 1, etc. On utilise la fonction Python str.format() au lieu de la mise en forme de style printf.

Pour les modèles simples, cela fonctionne bien. Cependant, les pages Wagtail ont des relations parent-enfant, donc afin de créer quelque chose comme une PageFactory, on a besoin d'un moyen d'établir ça automatiquement. Cela peut être fait en remplaçant la méthode _create. Notre stratégie consiste à ajouter un argument supplémentaire avec le clé parent, qui pointera vers la page parent de la page qu'on veut créer. Notre usine est alors:

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

On définit abstract=True pour indiquer que cette usine est abstract. Dans la méthode _create, on recherche un parent dans les arguments, et s'il est là, on ajoute la page à créer en tant qu'enfant à ce parent. S'il n'y a pas d'argument parent, on ajoute la page à créer à la racine. Pour trouver la racine, on utilise la méthode get_first_root_node, disponible via un ancêtre d'une page Wagtail.

On crée une usine pour les pages d'accueil en sous-classant à partir de PageFactory:

from ..models import HomePage

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

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

et le même code exact peut être utilisé pour créer d'autres classes de Page, dans notre cas un ThemePageFactory, ThemeIndexPageFactory, ArticleIndexPageFactory (il suffit de remplacer Home par Theme, etc.). Pour une ArticlePageFactory, on a besoin de deux autres choses. Tout d'abord, on s'assurera que les pages d'articles ont des champs first_published_at différents, car on a l'intention de tester que la commande est correctement effectuée. Ceci est à nouveau réalisé en utilisant une séquence:

from datetime import timedelta
from django.utils import timezone

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

Deuxièmement, notre modèle ArticlePage a une relation plusieurs-à-plusieurs avec le modèle Theme. Cela peut être établi à l'aide d'un post generation hook, de la manière suivante:

@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)

Cela nous permet d'ajouter un tuple de thèmes à la page à créer en tant que relations avec la page spécifique.

Ce sont toutes les usines dont on a besoin pour commencer à écrire les tests. Dans le répertoire /tests, créez un fichier test_pages.py. On définit nos données initiales à l'aide de setUpTestData, comme on l'a fait dans le didacticiel précédent. Les deux premiers objets à créer sont un request factory et un 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())

On aura besoin du request factory dans nos tests. Wagtail a également besoin d'une instance de Site, par exemple lors de la construction de l'URL d'une page (voir modèle de Page et de Site). On est maintenant prêt à créer des instances de page à l'aide de nos usines. On crée une page d'accueil, une page d'index de thème et une page d'index d'article (les deux enfants de la page d'accueil), deux thèmes et deux pages d'article en tant qu'enfants de la page d'index d'article. On définit également des relations plusieurs-à-plusieurs et définit quelques champs.

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,))

Comme vous pouvez le voir, maintenant qu'on a défini nos usines, la définition des instances ne prend qu'une seule ligne. Si on avait besoin de plus d'instances pour plus de tests, cela peut facilement être fait. Écrivons notre premier test: vérifions si les articles sur une page de thème sont dans l'ordre chronologique inverse:

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))

On utilise la méthode articlepages() d'une page de thème, extrait les dates et vérifie si elles sont dans l'ordre chronologique inverse. On effectue le test avec:

python3 manage.py test cms.tests.test_pages

Le même test pour les articles sur l'articleindexpage a presque le même 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))

Quelques tests supplémentaires: le fonctionnement du champ featured, la méthode themepages() et la méthode get_absolute_url() d'une page d'article:

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)

Notre test final consiste à tester la méthode serve() de la page de l'article. Pour cela, on a besoin d'une request, qu'on créera avec l'instance de request factory qu'on a définie. La méthode serve() transformera cette request en un objet TemplateResponse, mais parce que RequestFactory ne prend pas en charge le middleware, pas toutes les fonctionnalités de la réponse sont disponibles; le champ status_code l'est cependant. En suivant l'exemple dans la documentation Django:

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)

Voyons maintenant quelle partie de notre code est déjà couverte par les tests précédents. Comme dans notre tutoriel précédent, on utilise Coverage pour cela:

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

Ouvrir index.html dans notre navigateur montre que toutes les classes liées aux pages de nos modèles sont assez bien couvertes. Il est temps de passer aux tests du menu et des langues.

Commentez cet article (connectez-vous d'abord ou confirmez par nom et email ci-dessous)