Testing your Wagtail site

Wagtail comes with some utilities that simplify writing tests for your site.

WagtailPageTestCase

class wagtail.test.utils.WagtailPageTestCase WagtailPageTestCase extends django.test.TestCase, adding a few new assert methods. You should extend this class to make use of its methods:

from wagtail.test.utils import WagtailPageTestCase
from myapp.models import MyPage

class MyPageTests(WagtailPageTestCase):
    def test_can_create_a_page(self):
        ...

assertPageIsRoutable(page, route_path=”/”, msg=None)

Asserts that page can be routed to without raising a Http404 error.

For page types with multiple routes, you can use route_path to specify an alternate route to test.

This assertion is great for getting coverage on custom routing logic for page types. Here is an example:

from wagtail.test.utils import WagtailPageTestCase
from myapp.models import EventListPage

class EventListPageRoutabilityTests(WagtailPageTestCase):
    @classmethod
    def setUpTestData(cls):
        # create page(s) for testing
        ...

    def test_default_route(self):
        self.assertPageIsRoutable(self.page)

    def test_year_archive_route(self):
        # NOTE: Despite this page type raising a 404 when no events exist for
        # the specified year, routing should still be successful
        self.assertPageIsRoutable(self.page, "archive/year/1984/")

assertPageIsRenderable(page, route_path=”/”, query_data=None, post_data=None, user=None, accept_404=False, accept_redirect=False, msg=None)

Asserts that page can be rendered without raising a fatal error.

For page types with multiple routes, you can use route_path to specify a partial path to be added to the page’s regular url.

When post_data is provided, the test makes a POST request with post_data in the request body. Otherwise, a GET request is made.

When supplied, query_data is always converted to a querystring and added to the request URL.

When user is provided, the test is conducted with them as the active user.

By default, the assertion will fail if the request to the page URL results in a 301, 302 or 404 HTTP response. If you are testing a page/route where a 404 response is expected, you can use accept_404=True to indicate this, and the assertion will pass when encountering a 404 response. Likewise, if you are testing a page/route where a redirect response is expected, you can use accept_redirect=True to indicate this, and the assertion will pass when encountering 301 or 302 response.

This assertion is great for getting coverage on custom rendering logic for page types. Here is an example:

def test_default_route_rendering(self):
    self.assertPageIsRenderable(self.page)

def test_year_archive_route_with_zero_matches(self):
    # NOTE: Should raise a 404 when no events exist for the specified year
    self.assertPageIsRenderable(self.page, "archive/year/1984/", accept_404=True)

def test_month_archive_route_with_zero_matches(self):
    # NOTE: Should redirect to year-specific view when no events exist for the specified month
    self.assertPageIsRenderable(self.page, "archive/year/1984/07/", accept_redirect=True)

assertPageIsEditable(page, post_data=None, user=None, msg=None)

Asserts that the page edit view works for page without raising a fatal error.

When user is provided, the test is conducted with them as the active user. Otherwise, a superuser is created and used for the test.

After a successful GET request, a POST request is made with field data in the request body. If post_data is provided, that will be used for this purpose. If not, this data will be extracted from the GET response HTML.

This assertion is great for getting coverage on custom fields, panel configuration and custom validation logic. Here is an example:

def test_editability(self):
    self.assertPageIsEditable(self.page)

def test_editability_on_post(self):
    self.assertPageIsEditable(
        self.page,
        post_data={
            "title": "Fabulous events",
            "slug": "events",
            "show_featured": True,
            "show_expired": False,
            "action-publish": "",
        }
    )

assertPageIsPreviewable(page, mode=””, post_data=None, user=None, msg=None)

Asserts that the page preview view can be loaded for page without raising a fatal error.

For page types that support different preview modes, you can use mode to specify the preview mode to be tested.

When user is provided, the test is conducted with them as the active user. Otherwise, a superuser is created and used for the test.

To load the preview, the test client needs to make a POST request including all required field data in the request body. If post_data is provided, that will be used for this purpose. If not, the method will attempt to extract this data from the page edit view.

This assertion is great for getting coverage on custom preview modes, or getting reassurance that custom rendering logic is compatible with Wagtail’s preview mode. Here is an example:

def test_general_previewability(self):
    self.assertPageIsPreviewable(self.page)

def test_archive_previewability(self):
    self.assertPageIsPreviewable(self.page, mode="year-archive")

assertCanCreateAt(parent_model, child_model, msg=None) Assert a particular child Page type can be created under a parent Page type. parent_model and child_model should be the Page classes being tested.

def test_can_create_under_home_page(self):
    # You can create a ContentPage under a HomePage
    self.assertCanCreateAt(HomePage, ContentPage)

assertCanNotCreateAt(parent_model, child_model, msg=None) Assert a particular child Page type can not be created under a parent Page type. parent_model and child_model should be the Page classes being tested.

def test_cant_create_under_event_page(self):
    # You can not create a ContentPage under an EventPage
    self.assertCanNotCreateAt(EventPage, ContentPage)

assertCanCreate(parent, child_model, data, msg=None, publish=True) Assert that a child of the given Page type can be created under the parent, using the supplied POST data.

parent should be a Page instance, and child_model should be a Page subclass. data should be a dict that will be POSTed at the Wagtail admin Page creation method.

publish specifies whether the page being created should be published or not - default is True.

from wagtail.test.utils.form_data import nested_form_data, streamfield

def test_can_create_content_page(self):
    # Get the HomePage
    root_page = HomePage.objects.get(pk=2)

    # Assert that a ContentPage can be made here, with this POST data
    self.assertCanCreate(root_page, ContentPage, nested_form_data({
        'title': 'About us',
        'body': streamfield([
            ('text', 'Lorem ipsum dolor sit amet'),
        ])
    }))

See Form data helpers for a set of functions useful for constructing POST data.

assertAllowedParentPageTypes(child_model, parent_models, msg=None) Test that the only page types that child_model can be created under are parent_models.

The list of allowed parent models may differ from those set in Page.parent_page_types, if the parent models have set Page.subpage_types.

def test_content_page_parent_pages(self):
    # A ContentPage can only be created under a HomePage
    # or another ContentPage
    self.assertAllowedParentPageTypes(
        ContentPage, {HomePage, ContentPage})

    # An EventPage can only be created under an EventIndex
    self.assertAllowedParentPageTypes(
        EventPage, {EventIndex})

assertAllowedSubpageTypes(parent_model, child_models, msg=None) Test that the only page types that can be created under parent_model are child_models.

The list of allowed child models may differ from those set in Page.subpage_types, if the child models have set Page.parent_page_types.

def test_content_page_subpages(self):
    # A ContentPage can only have other ContentPage children
    self.assertAllowedSubpageTypes(
        ContentPage, {ContentPage})

    # A HomePage can have ContentPage and EventIndex children
    self.assertAllowedSubpageTypes(
        HomePage, {ContentPage, EventIndex})

Form data helpers

The assertCanCreate method requires page data to be passed in the same format that the page edit form would submit. For complex page types, it can be difficult to construct this data structure by hand; the wagtail.test.utils.form_data module provides a set of helper functions to assist with this.

wagtail.test.utils.form_data.nested_form_data(data)

Translates a nested dict structure into a flat form data dict with hyphen-separated keys.

nested_form_data({
    'foo': 'bar',
    'parent': {
        'child': 'field',
    },
})
# Returns: {'foo': 'bar', 'parent-child': 'field'}
wagtail.test.utils.form_data.rich_text(value, editor='default', features=None)

Converts an HTML-like rich text string to the data format required by the currently active rich text editor.

Parameters:
  • editor – An alternative editor name as defined in WAGTAILADMIN_RICH_TEXT_EDITORS

  • features – A list of features allowed in the rich text content (see Limiting features in a rich text field)

self.assertCanCreate(root_page, ContentPage, nested_form_data({
    'title': 'About us',
    'body': rich_text('<p>Lorem ipsum dolor sit amet</p>'),
}))
wagtail.test.utils.form_data.streamfield(items)

Takes a list of (block_type, value) tuples and turns it in to StreamField form data. Use this within a nested_form_data() call, with the field name as the key.

nested_form_data({'content': streamfield([
    ('text', 'Hello, world'),
])})
# Returns:
# {
#     'content-count': '1',
#     'content-0-type': 'text',
#     'content-0-value': 'Hello, world',
#     'content-0-order': '0',
#     'content-0-deleted': '',
# }
wagtail.test.utils.form_data.inline_formset(items, initial=0, min=0, max=1000)

Takes a list of form data for an InlineFormset and translates it in to valid POST data. Use this within a nested_form_data() call, with the formset relation name as the key.

nested_form_data({'lines': inline_formset([
    {'text': 'Hello'},
    {'text': 'World'},
])})
# Returns:
# {
#     'lines-TOTAL_FORMS': '2',
#     'lines-INITIAL_FORMS': '0',
#     'lines-MIN_NUM_FORMS': '0',
#     'lines-MAX_NUM_FORMS': '1000',
#     'lines-0-text': 'Hello',
#     'lines-0-ORDER': '0',
#     'lines-0-DELETE': '',
#     'lines-1-text': 'World',
#     'lines-1-ORDER': '1',
#     'lines-1-DELETE': '',
# }

Creating Page objects within tests

If you want to create page objects within tests, you will need to go through some steps before actually creating the page you want to test.

  • Pages can’t be created directly with MyPage.objects.create() as you would do with a regular Django model, they need to be added as children to a parent page with parent.add_child(instance=child).

  • To start the page tree, you need a root page that can be created with Page.get_first_root_node().

  • You also need a Site set up with the correct hostname and a root_page.

from wagtail.models import Page, Site
from wagtail.rich_text import RichText
from wagtail.test.utils import WagtailPageTestCase

from home.models import HomePage, MyPage


class MyPageTest(WagtailPageTestCase):
    @classmethod
    def setUpTestData(cls):
        root = Page.get_first_root_node()
        Site.objects.create(
            hostname="testserver",
            root_page=root,
            is_default_site=True,
            site_name="testserver",
        )
        home = HomePage(title="Home")
        root.add_child(instance=home)
        cls.page = MyPage(
            title="My Page",
            slug="mypage",
        )
        home.add_child(instance=cls.page)

    def test_get(self):
        response = self.client.get(self.page.url)
        self.assertEqual(response.status_code, 200)

Working with Page content

You will likely want to test the content of your page. If it includes a StreamField, you will need to set its content as a list of tuples with the block’s name and content. For RichTextBlock, the content has to be an instance of RichText.

...
from wagtail.rich_text import RichText

class MyPageTest(WagtailPageTestCase):
    @classmethod
    def setUpTestData(cls):
        ...
        # Create page instance here
        cls.page.body.extend(
            [
                ("heading", "Just a CharField Heading"),
                ("paragraph", RichText("<p>First paragraph</p>")),
                ("paragraph", RichText("<p>Second paragraph</p>")),
            ]
        )
        cls.page.save()

    def test_page_content(self):
        response = self.client.get(self.page.url)
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Just a CharField Heading")
        self.assertContains(response, "<p>First paragraph</p>")
        self.assertContains(response, "<p>Second paragraph</p>")

Fixtures

Using dumpdata

Creating fixtures for tests is best done by creating content in a development environment, and using Django’s dumpdata command.

Note that by default dumpdata will represent content_type by the primary key; this may cause consistency issues when adding / removing models, as content types are populated separately from fixtures. To prevent this, use the --natural-foreign switch, which represents content types by ["app", "model"] instead.

Manual modification

You could modify the dumped fixtures manually, or even write them all by hand. Here are a few things to be wary of.

Custom Page models

When creating customized Page models in fixtures, you will need to add both a wagtailcore.page entry, and one for your custom Page model.

Let’s say you have a website module which defines a Homepage(Page) class. You could create such a homepage in a fixture with:

[
    {
        "model": "wagtailcore.page",
        "pk": 3,
        "fields": {
            "title": "My Customer's Homepage",
            "content_type": ["website", "homepage"],
            "depth": 2
        }
    },
    {
        "model": "website.homepage",
        "pk": 3,
        "fields": {}
    }
]

Treebeard fields

Filling in the path / numchild / depth fields is necessary for tree operations like get_parent() to work correctly. url_path is another field that can cause errors in some uncommon cases if it isn’t filled in.

The Treebeard docs might help in understanding how this works.