Testing a Dexterity content type

The most common thing that we could develop in Plone are content types. Let’s see how to test if a new content type works as expected.

Create a new content type

With plonecli we can add features to our package using the command line.

For example, let’s create a new TestingItem content type:

$ cd plonetraining.testing
$ plonecli add content_type

With this command, plonecli will ask you some questions and will register a new content type in our package.

Note

To follow this training, you need to answer questions like this:

  • Content type name (Allowed: _ a-z A-Z and whitespace) [Todo Task]: TestingItem

  • Content type description:

  • Use XML Model [y]: n

  • Dexterity base class (Container/Item) [Container]: Item

  • Should the content type globally addable? [y]: y

  • Create a content type class [y]: y

  • Activate default behaviors? [y]: y

Test the content type

If we now try to re-run the tests, we see that the number of tests has increased. This is because plonecli also creates a basic test for this new content type in the tests/test_ct_testing_item.py file:

class TestingItemIntegrationTest(unittest.TestCase):

    layer = PLONETRAINING_TESTING_INTEGRATION_TESTING

    def setUp(self):
        """Custom shared utility setup for tests."""
        self.portal = self.layer['portal']
        setRoles(self.portal, TEST_USER_ID, ['Manager'])
        self.parent = self.portal

    def test_ct_testing_item_schema(self):
        fti = queryUtility(IDexterityFTI, name='TestingItem')
        schema = fti.lookupSchema()
        schema_name = portalTypeToSchemaName('TestingItem')
        self.assertEqual(schema_name, schema.getName())

    def test_ct_testing_item_fti(self):
        fti = queryUtility(IDexterityFTI, name='TestingItem')
        self.assertTrue(fti)

    def test_ct_testing_item_factory(self):
        fti = queryUtility(IDexterityFTI, name='TestingItem')
        factory = fti.factory
        obj = createObject(factory)

    def test_ct_testing_item_adding(self):
        setRoles(self.portal, TEST_USER_ID, ['Contributor'])
        obj = api.content.create(
            container=self.portal, type='TestingItem', id='testing_item',
        )

        parent = obj.__parent__
        self.assertIn('testing_item', parent.objectIds())

        # check that deleting the object works too
        api.content.delete(obj=obj)
        self.assertNotIn('testing_item', parent.objectIds())

    def test_ct_testing_item_globally_addable(self):
        setRoles(self.portal, TEST_USER_ID, ['Contributor'])
        fti = queryUtility(IDexterityFTI, name='TestingItem')
        self.assertTrue(
            fti.global_allow, u'{0} is not globally addable!'.format(fti.id),
        )

We can see some interesting things:

  • We are using the PLONETRAINING_TESTING_INTEGRATION_TESTING layer, because we don’t need to write functional tests

  • We are using the setUp method to set some variables and add some roles to the testing user.

In these tests, we are testing some very basic things for our content type, for example if it’s correctly registered with factory type information (FTI), if we can create an item of our content type and if a user with a specific role (Contributor) can add it.

Note

plone.app.testing gives us a default user for tests. We could import its username in a variable called TEST_USER_ID and set different roles when needed.

By default, each test is run as that logged-in user. If we want to run tests as anonymous users or using a different user, we need to logout and login.

Exercise 1

  • Change the permissions for our new content type and only allow Manager role to add items of this type.

  • Fix old tests.

  • Create a new one test to verify that the Contributor role can’t add items of this type.

Solution

In rolemap.xml:

<permission name="plonetraining.testing: Add TestingItem" acquire="True">
    <role name="Manager"/>
</permission>

In the test case file we need to import a new Exception:

from AccessControl.unauthorized import Unauthorized

And then we update tests like this:

    def test_ct_testing_item_adding(self):
        obj = api.content.create(
            container=self.portal, type='TestingItem', id='testing_item',
        )

        parent = obj.__parent__
        self.assertIn('testing_item', parent.objectIds())

        # check that deleting the object works too
        api.content.delete(obj=obj)
        self.assertNotIn('testing_item', parent.objectIds())

    def test_ct_testing_item_globally_addable(self):
        fti = queryUtility(IDexterityFTI, name='TestingItem')
        self.assertTrue(
            fti.global_allow, u'{0} is not globally addable!'.format(fti.id),
        )

    def test_ct_testing_item_contributor_cant_add(self):
        setRoles(self.portal, TEST_USER_ID, ['Contributor'])
        with self.assertRaises(Unauthorized):
            api.content.create(
                container=self.portal, type='TestingItem', id='testing_item',
            )

Functional test

So far, we have written only ìntegration tests because we didn’t need to test browser integration or commit any transactions.

As we covered before, if we don’t need to create functional tests, it’s better to avoid them because they are slower than integration tests.

Note

It’s always better to avoid transactions in tests because they invalidate the isolation between tests in the same test case.

However, we will create a functional test to see how it works and how content type creation works in a browser.

Let’s create a new test case in the same file.

First, we need to import some new dependencies:

from plone.app.testing import SITE_OWNER_NAME
from plone.app.testing import SITE_OWNER_PASSWORD
from plone.testing.z2 import Browser
from plonetraining.testing.testing import PLONETRAINING_TESTING_FUNCTIONAL_TESTING

Now we can create a new test case:

class TestingItemFunctionalTest(unittest.TestCase):

    layer = PLONETRAINING_TESTING_FUNCTIONAL_TESTING

    def setUp(self):
        app = self.layer['app']
        self.portal = self.layer['portal']
        self.request = self.layer['request']
        self.portal_url = self.portal.absolute_url()

        # Set up browser
        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_add_testing_item(self):
        self.browser.open(self.portal_url + '/++add++TestingItem')
        self.browser.getControl(name='form.widgets.IBasic.title').value = 'Foo'
        self.browser.getControl('Save').click()
        self.assertTrue(
            '<h1 class="documentFirstHeading">Foo</h1>'
            in self.browser.contents
        )

        self.assertEqual('Foo', self.portal['foo'].title)

    def test_view_testing_item(self):
        setRoles(self.portal, TEST_USER_ID, ['Manager'])
        api.content.create(
            type='TestingItem',
            title='Bar',
            description='This is a description',
            container=self.portal,
        )

        import transaction

        transaction.commit()

        self.browser.open(self.portal_url + '/bar')

The first thing we see is the new layer used: PLONETRAINING_TESTING_FUNCTIONAL_TESTING.

In setUp we configure the test case to use the Zope web browser and we login as site owner.

In test_add_testing_item we simulate the user interaction of creating a new content in the browser with following actions:

  • Open the url for adding a new TestingItem content

  • Find the title field and compile it

  • Click to Save button

  • Check that after saving it, we can see its title.

In test_view_testing_item we are checking that, by accessing a content item directly using a URL, we can see its values.

Note

self.browser.contents shows the HTML of the last visited page.