Testing a view

Another base Plone feature that we can test is a View.

Create a new view

Create a new view with plonecli:

$ cd plonetraining.testing
$ plonecli add view

Follow the prompts and create a new view with the TestingItemView Python class and a matching template.

This new view will be automatically registered in our package.

Test the view

plonecli creates basic tests (in the test_view_testing_item_view.py file).

Inspecting this file, we see something like this:

class ViewsIntegrationTest(unittest.TestCase):

    layer = PLONETRAINING_TESTING_INTEGRATION_TESTING

    def setUp(self):
        self.portal = self.layer['portal']
        self.request = self.layer['request']
        setRoles(self.portal, TEST_USER_ID, ['Manager'])
        api.content.create(self.portal, 'Folder', 'other-folder')
        api.content.create(self.portal, 'Document', 'front-page')

    def test_testing_item_view_is_registered(self):
        view = getMultiAdapter(
            (self.portal['other-folder'], self.portal.REQUEST),
            name='testing-item-view'
        )
        self.assertTrue(view.__name__ == 'testing-item-view')
        # self.assertTrue(
        #     'Sample View' in view(),
        #     'Sample View is not found in testing-item-view'
        # )

    def test_testing_item_view_not_matching_interface(self):
        with self.assertRaises(ComponentLookupError):
            getMultiAdapter(
                (self.portal['front-page'], self.portal.REQUEST),
                name='testing-item-view',
            )

In setUp we are creating sample content that we will use in tests.

The first test (test_testing_item_view_is_registered) tries to call the view on a Folder and checks that everything works well and that we get the correct view.

The second test (test_testing_item_view_not_matching_interface) tries to call the view on a non-folderish content item (a Document) and checks that this raises an Exception.

If we take a look at the configure.zcml file where the view is registered, we can see that the view is registered only for folderish types. We want to test that this registration is correct.

We can test several things about a view:

  • If it is available only for a certain type of object

  • If it renders as we expect, by calling the view in an Integration test, or using the browser in a Functional test

  • If its methods return what we expect, by calling methods directly from the view instance

Exercise 1

We want to use this view only for our content type and not for all folderish ones (also because TestingItem isn’t a folderish type).

  • Change the view registration to be available only for our type

  • Update tests to check that we can call it only on a TestingItem content

  • The view template prints a string that is returned from its class. Write a test that checks this string.

Solution

TestingItem objects implements the ITestingItem interface, so we need to update the view registration like this:

<browser:page
    name="testing-item-view"
    for="plonetraining.testing.content.testing_item.ITestingItem"
    class=".testing_item_view.TestingItemView"
    template="testing_item_view.pt"
    permission="zope2.View"
    />

Then our test_view_testing_item_view will become like this:

    def setUp(self):
        self.portal = self.layer['portal']
        self.request = self.layer['request']
        setRoles(self.portal, TEST_USER_ID, ['Manager'])
        api.content.create(self.portal, 'Folder', 'other-folder')
        api.content.create(self.portal, 'Document', 'front-page')
        api.content.create(self.portal, 'TestingItem', 'foo')

    def test_testing_item_view_is_registered(self):
        view = getMultiAdapter(
            (self.portal['foo'], self.portal.REQUEST), name='testing-item-view'
        )
        self.assertTrue(view.__name__ == 'testing-item-view')
        # self.assertTrue(
        #     'Sample View' in view(),
        #     'Sample View is not found in testing-item-view'
        # )

    def test_testing_item_view_not_matching_interface(self):
        with self.assertRaises(ComponentLookupError):
            getMultiAdapter(
                (self.portal['front-page'], self.portal.REQUEST),
                name='testing-item-view',
            )
            getMultiAdapter(
                (self.portal['other-folder'], self.portal.REQUEST),
                name='testing-item-view',
            )

    def test_testing_item_view_show_text(self):
        view = api.content.get_view(
            name='testing-item-view',
            context=self.portal['foo'],
            request=self.request,
        )
        self.assertIn('A small message', view())

Note

plonecli uses getMultiAdapter to obtain a view and we use this for consistency with these pre-created tests, but the preferred way of obtaining a view is via plone.api.

Exercise 2

  • Add a method in the view that gets a parameter from the request (message) and returns it.

  • Check the method in the integration test

  • Update the template to print the value from that method

  • Test that calling the method from the view returns what we expect

  • Write a functional test to test browser integration

Solution

First, we need to implement that method in our view class in the views/testing_item_view.py file:

class TestingItemView(BrowserView):
    # If you want to define a template here, please remove the template from
    # the configure.zcml registration of this view.
    # template = ViewPageTemplateFile('testing_item_view.pt')

    def __call__(self):
        # Implement your own actions:
        self.msg = _(u'A small message')
        return self.index()

    def custom_msg(self):
        return self.request.form.get('message', '')

Then we add this message into the template in views/testing_item_view.pt

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:metal="http://xml.zope.org/namespaces/metal"
      xmlns:tal="http://xml.zope.org/namespaces/tal"
      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
      i18n:domain="plonetraining.testing"
      metal:use-macro="context/main_template/macros/master">
<body>
  <metal:block fill-slot="content-core">
      <h2 i18n:translate="">Sample View</h2>
      <p>This is the default message: ${view/msg}</p>
      <p tal:define="custom_msg view/custom_msg"
         tal:condition="custom_msg">This is the custom message: ${view/custom_msg}</p>
  </metal:block>
</body>
</html>

And finally we want to test everything, so let’s add an integration test for the method:

    def test_testing_custom_msg_without_parameter(self):
        view = api.content.get_view(
            name='testing-item-view',
            context=self.portal['foo'],
            request=self.request,
        )
        self.assertEqual('', view.custom_msg())

    def test_testing_custom_msg_with_parameter(self):
        view = api.content.get_view(
            name='testing-item-view',
            context=self.portal['foo'],
            request=self.request,
        )
        self.request.form['message'] = 'hello'
        self.assertEqual('hello', view.custom_msg())

Now we can create a functional test:

from plone.app.testing import SITE_OWNER_NAME
from plone.app.testing import SITE_OWNER_PASSWORD
from plone.testing.z2 import Browser
from transaction import commit


class ViewsFunctionalTest(unittest.TestCase):

    layer = PLONETRAINING_TESTING_FUNCTIONAL_TESTING

    def setUp(self):
        app = self.layer['app']
        self.portal = self.layer['portal']
        self.portal_url = self.portal.absolute_url()
        setRoles(self.portal, TEST_USER_ID, ['Manager'])
        api.content.create(self.portal, 'TestingItem', 'foo')
        # needed to "see" this content in the browser
        commit()

        self.browser = Browser(app)
        self.browser.handleErrors = False
        self.browser.addHeader(
            'Authorization',
            'Basic {username}:{password}'.format(
                username=SITE_OWNER_NAME,
                password=SITE_OWNER_PASSWORD),
        )

    def test_view_without_parameter(self):
        self.browser.open(self.portal_url + '/foo')
        self.assertNotIn(
            '<p>This is the default message: A small message</p>',
            self.browser.contents,
        )
        # because it's not the default view
        self.browser.open(self.portal_url + '/foo/testing-item-view')
        self.assertIn(
            '<p>This is the default message: A small message</p>',
            self.browser.contents,
        )
        self.assertNotIn(
            'This is the custom message',
            self.browser.contents,
        )

    def test_view_with_parameter(self):
        self.browser.open(self.portal_url + '/foo?message=hello')
        self.assertNotIn(
            '<p>This is the default message: A small message</p>',
            self.browser.contents,
        )
        self.assertNotIn(
            'This is the custom message',
            self.browser.contents,
        )
        # because it's not the default view
        self.browser.open(self.portal_url + '/foo/testing-item-view?message=hello')
        self.assertIn(
            '<p>This is the default message: A small message</p>',
            self.browser.contents,
        )
        self.assertIn(
            '<p>This is the custom message: hello</p>',