Testing a navigation menu in Wagtail with Factory Boy

A navigation menu depends on the content of a site. In this tutorial we create the content of a Wagtail site with Factory Boy and with it test the navigation menu.

July 14, 2020, 8:43 p.m.
Themes: Navigation Testing

In an earlier tutorial we have created a navigation menu which can handle Wagtail pages as well as pages outside of the Wagtail tree, is multilingual and editable via admin. In another tutorial we have set up a factory for testing Wagtail pages. We are going to bring this together to test the navigation menu. Since we have already set up a page factory we can create some content for the site. Let's set up some content for our tests: a site, a homepage, an articleindexpage and two articlepages. Note that we set the show_in_menus fields to True. In our /tests directory create a file test_menus.py:

from .factories import HomePageFactory, ArticlePageFactory, ArticleIndexPageFactory
from django.test import TestCase
from wagtail.core.models import Page, Site

class TestMenus(TestCase):

    @classmethod
    def setUpTestData(cls):
        cls.site = Site.objects.create(is_default_site=True, root_page=Page.get_first_root_node())
        cls.homepage = HomePageFactory()
        cls.articleindexpage = ArticleIndexPageFactory(parent=cls.homepage)
        cls.articlepage1 = ArticlePageFactory(parent=cls.articleindexpage, show_in_menus=True)
        cls.articlepage2 = ArticlePageFactory(parent=cls.articleindexpage, show_in_menus=True)

Before we can create a menu and menu items we need to create a factory for them, in our file /tests/factories.py. The factory for the menu is simple:

from ..models import Menu

class MenuFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Menu

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

We already explained the use of Sequence to automatically generate a title. The menu item factory can be defined as follows:

from ..models import MenuItem

class MenuItemFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = MenuItem

    menu = factory.SubFactory(MenuFactory)

The foreign key relationship between the menu and the menu item is established through a SubFactory. In the test setup in the file test_menus.py we create a menu and three menu items with a few fields set to different values:

cls.menu = MenuFactory()
# for the menu-items we need to establish a sort order (class Orderable)
cls.menuitem_ordinary = MenuItemFactory(menu=cls.menu, sort_order=1, title="Ordinary",
                                        link_url='/ordinary/', show_when='always')
cls.menuitem_guest = MenuItemFactory(menu=cls.menu, sort_order=2, title="Guest", link_url='/guest/',
                                     show_when='not_logged_in')
cls.menuitem_articlepage1 = MenuItemFactory(menu=cls.menu, sort_order=3, link_page=cls.articlepage1)

Since the model MenuItem is subclassed from the class Orderable, we manually add a sort_order. Furthermore we set a title, a link_url and different values for the field show_when, to be used in our tests. Time to write our first test: check whether our function get_menu correctly generates a menu from a page which has children:

def test_get_menu_of_page(self):
    menu = get_menu(slug=None, page=self.articleindexpage, logged_in=True)
    self.assertEqual(menu[0]['title'], self.articlepage1.title)
    self.assertEqual(menu[0]['url'], self.articlepage1.url)
    self.assertEqual(menu[0]['page'].title, self.articlepage1.title)
    self.assertEqual(len(menu), 2)

We can run the test with:

python3 manage.py test cms.tests.test_menus

So far so good. Now let's test a handmade menu:

from django.utils import translation

def test_get_handmade_menu(self):
    menu = get_menu(self.menu.slug, None, True)
    self.assertEqual(menu[0]['title'], 'Ordinary')
    # the expected url is in the current language
    expected_url = '/' + translation.get_language() + '/ordinary/'
    self.assertEqual(menu[0]['url'], expected_url)

The get_menu function generates the menu from the slug of the menu instance, which is automatically generated from the title in our model. The first menu item should then have the title of the first menu item. Since our site is multilingual, we expect the url to be prefixed with the current language code, so we use the function get_language() to check this. Testing whether the function get_menu picks up the logged_in parameter is straightforward:

def test_get_menu_logged_in_or_not(self):
    menu = get_menu(self.menu.slug, None, True)
    # menu should only have two items
    self.assertEqual(len(menu), 2)
    menu = get_menu(self.menu.slug, None, False)
    self.assertEqual(len(menu), 3)

We want to add some more tests on how get_menu handles languages. For that we introduce a foreign language code and a page in a foreign language in our setup:

from django.conf import settings
from wagtailtrans.models import Language, TranslatablePage

cls.foreign_language_code = [code for code, lang in settings.LANGUAGES if code != settings.LANGUAGE_CODE][0]
# note: Wagtailtrans automatically creates a language tree for every language that is defined
cls.foreign_language = Language.objects.get_or_create(code=cls.foreign_language_code)[0]
cls.foreign_articlepage1 = TranslatablePage.objects.get(language=cls.foreign_language, canonical_page=cls.articlepage1)

We retrieve the foreign language code from our settings. Wagtailtrans has a special Language model, of which we need an instance. When a page is created (in our case articlepage1), Wagtailtrans automatically generates a page for every language in settings. Therefore we are able to retrieve a foreign language version of that page by using the fields language and canonical_page. Now we are able to test the trans_page() and trans_url() method of the MenuItem model:

def test_menuitem_trans_page_for_foreign_language(self):
self.assertEqual(self.menuitem_articlepage1.trans_page(self.foreign_language_code).url, self.foreign_articlepage1.url)

def test_menuitem_trans_page_for_canonical_language(self):
self.assertEqual(self.menuitem_articlepage1.trans_page(settings.LANGUAGE_CODE).url, self.articlepage1.url)

def test_menuitem_trans_url_method(self):
self.assertEqual(self.menuitem_articlepage1.trans_url(self.foreign_language_code), self.foreign_articlepage1.url)

Run coverage to check how much of the code we have covered thus far:

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

Our template tags are covered well enough. Time to move on to testing the language view in views.py.

Comment on this article (sign in first or confirm by name and email below)