Behaviors

« Return to page index

How to create re-usable behaviors for Dexterity types

1. Introduction

About this manual

Behaviors are re-usable bundles of functionality that can be enabled or disabled on a per-content type basis. Examples might include:

  • A set of form fields (on standard add and edit forms)
  • Enabling particular event handler
  • Enabling one or more views, viewlets or other UI components
  • Anything else which may be expressed in code via an adapter and/or marker interface

You would typically not write a behavior as a one-off. Behaviors are normally used when:

  • You want to share fields and functionality across multiple types easily. Behaviors allow you to write a schema and associated components (e.g. adapters, event handlers, views, viwelets) once and re-use them easily.
  • A more experienced developer is making functionality available for re-use by less experienced integrators. For example, a behavior can be packaged up and release as an add-on product. Integators can then install that product and use the behavior in their own types, either in code or through-the-web. 

This manual is aimed at developers who want to write new behaviors. This is a slightly more advanced topic than the writing of custom content types, and we will assume that you are familiar with buildout, know how to create a custom package, understand interfaces and have a basic understanding of Zope's adapter concept.

Behaviors are not tied to Dexterity, but Dexterity provides behavior support for its types via the behaviors FTI property. In fact, if you've used Dexterity before, you've probably used some behaviors. Take a look at the Dexterity Developer Manual for more information about how to enable behaviors on a type and for a list of standard behaviors.

To learn more about how behaviors are implemented, see the plone.behavior package. This manual should teach you everything you need to know to write your own behaviors, but not how to integrate them into another framework.

2. Behavior basics

The fundamental concepts behind behaviors

Before we dive into the practical examples, we need to explain a few of the concepts that underpin behaviors.

At the most basic level, a behavior is like a 'conditional' adapter. For a Dexterity content type, the condition is, "is this behavior listed in the behaviors property in the FTI?" When a behavior is enabled for a particular object, it will be possible to adapt that object to the behavior's interface. If the behavior is disabled, adaptation will fail.

A behavior consist at the very least of an interface and some metadata, namely a title and a description. In most cases, there is also a factory, akin to an adapter factory, which will be invoked to get an appropriate adapter when requested. This is usually just a class that looks like any other adapter factory, although it will tend to be applicable to Interface, IContentish or a similarly broad context.

In some cases, behaviors specify a marker interface, which will be directly provided by instances for which the behavior is enabled. This is useful if you want to conditionally enable event handlers or view components, which are registered for this marker interface. Some behaviors have no factory. In this case, the behavior interface and the marker interface must be one and the same.

Behaviors are registered globally, using the <plone.behavior /> ZCML directive. This results in, among other things, a named utility providing plone.behavior.interfaces.IBehavior being registered. This utility contains various information about the behavior, such as its name, title, interface and (optional) marker interface. The utility name is the full dotted name to the behavior interface.

3. Creating and registering behaviors

How to create a basic behavior that provides form fields

The following example is based on the collective.gtags product, which comes with a behavior that adds a tags field to the "Categorization" fieldset, storing the actual tags in the Dublin Core Subject field.

collective.gtags is a standard package, with a configure.zcml, a GenericSetup profile, and a number of modules. We won't describe those here, though, since we are only interested in the behavior.

First, there are a few dependencies in setup.py:

      install_requires=[
          ...,
          'plone.behavior',
          'plone.directives.form',
          'zope.schema',
          'zope.interface',
          'zope.component',
          'rwproperty',
      ],

The dependency on plone.directives.form is there to support form fields. If your behavior does not require form fields, you can skip this dependency. The rwproperty dependency provides some convenience decorators that are used in the behavior adapter factory class.

Next, we have behaviors.zcml, which is included from configure.zcml and contains all necessary configuration to set up the behaviors. It looks like this:

<configure
    xmlns="http://namespaces.zope.org/zope"
    xmlns:plone="http://namespaces.plone.org/plone"
    xmlns:grok="http://namespaces.zope.org/grok"
    i18n_domain="collective.gtags">

    <include package="plone.behavior" file="meta.zcml" />
    
    <include package="plone.directives.form" file="meta.zcml" />
    <include package="plone.directives.form" />

    <grok:grok package=".behaviors" />

    <plone:behavior
        title="GTags"
        description="Use the Dublin Core Subject (keywords) field for Google Code like tags."
        provides=".behaviors.ITags"
        factory=".behaviors.Tags"
        />

</configure>

We first include the plone.behavior meta.zcml file, so that we get access to the <plone:behavior /> ZCML directive.

The next three lines include plone.directives.form and its meta.zcml file, and then invoke the grok action on the behaviors module. This is not directly related to the behavior, but rather to the configuration of a schema interface that provides form fields and display hints to plone.autoform (and thus Dexterity's standard add and edit forms). If your behavior is not a form field provider, you can omit these lines. Similarly, if you have grokked the entire package elsewhere with <grok:grok package="." />, you can omit the <grok:grok package=".behaviors" /> line. Otherwise, adjust it to reflect the module or package where your behaviors are kept.

The behavior itself is registered with the <plone:behavior /> directive. We set a title and a description, and then speicfy the behavior interface with the provides attribute. This attribute is required, and is used to construct the unique name for the behavior. In this case, the behavior name is collective.gtags.behaviors.ITags, the full dotted name to the behavior interface. When the behavior is enabled for a type, it will be possible to adapt instances of that type to ITags. That adaptation will invoke the factory specified by the factory attribute.

The behaviors.py module looks like this:

"""Behaviours to assign tags (to ideas).

Includes a form field and a behaviour adapter that stores the data in the
standard Subject field.
"""

from rwproperty import getproperty, setproperty

from zope.interface import implements, alsoProvides
from zope.component import adapts

from plone.directives import form
from collective.gtags.field  import Tags

from Products.CMFCore.interfaces import IDublinCore

from collective.gtags import MessageFactory as _

class ITags(form.Schema):
    """Add tags to content
    """
    
    form.fieldset(
            'categorization',
            label=_(u'Categorization'),
            fields=('tags',),
        )
    
    tags = Tags(
            title=_(u"Tags"),
            description=_(u"Applicable tags"),
            required=False,
            allow_uncommon=True,
        )

alsoProvides(ITags, form.IFormFieldProvider)

class Tags(object):
    """Store tags in the Dublin Core metadata Subject field. This makes
    tags easy to search for.
    """
    implements(ITags)
    adapts(IDublinCore)

    def __init__(self, context):
        self.context = context
    
    @getproperty
    def tags(self):
        return set(self.context.Subject())
    @setproperty
    def tags(self, value):
        if value is None:
            value = ()
        self.context.setSubject(tuple(value))

We first define the ITags interface, which is also the behavior interface. Here, we define a single attribute, tags, but we could also have added methods and additional fields if required. Naturally, these need to be implemented by the behavior adapter.

Since we want this behavior to provide form fields, we derive the behavior interface from form.Schema and set form hints using plone.directives.form (remember that these will only take effect if the package is grokked). We also mark the ITags interface with IFormFieldProvider to signal that it should be processed for form fields by the standard forms. See the Dexterity Developer Manual for more information about setting form hints in schema interfaces.

If your behavior does not provide form fields, you can just derive from zope.interface.Interface and omit the alsoProvides() line.

Next, we write the class that implements the behavior adapter and acts the adapter factory. Notice how it implements the behavior interface (ITags), and adapts a broad interface (IDublinCore). The behavior cannot be enabled on types not supporting this interface. In many cases, you will omit the adapts() line, provided your behavior is generic enough to work on any context.

The adapter is otherwise identical to any other adapter. It implements the interface, here by storing values in the Subject field. The use of getproperty and setproperty from the rwproperty package is for convenience only.

4. Providing marker interfaces

How to use behaviors to set marker interfaces on instances of a given type.

Sometimes, it is useful for objects that provide a particular behavior to also provide a specific marker interface. For example, you can register a viewlet for a particular marker and use a behavior to enable that marker on all instances of a particular content type. The viewlet will then only show up when the behavior is enabled. The same principle can be applied to event handlers, views and other components.

There is usually no need to use markers to enable a custom adapter since a standard behavior is already a conditional adapter. However, in certain cases, you may want to provide one or more adapters to an interface that is not the behavior interface, e.g. to use a particular extension point provided by another component. In this case, it may easier to set a marker interface and provide an adapter from this marker.

plone.behavior's marker support can be used in two ways:

  • As the behavior interface itself. In this case, there is no behavior adapter factory. The behavior interface and the marker interface are one and the same.
  • As a supplement to a standard behavior adapter. In this case, a factory is provided, and the behavior interface (which the behavior adapter factory implements) is different to the marker interface.

Primary marker behaviors

In the first case, where the behavior interface and the marker interface are the same, you can simply use the <plone:behavior /> directive without a factory. For example:

    <plone:behavior
        title="Pony viewlet"
        description="Shows a pony next to the content"
        provides=".behaviors.IWantAPony"
        />

One could imagine a viewlet based on plone.pony registered for the IWantAPony marker interface. If the behavior is enabled for a particular object, IWantAPony.providedBy(object) would be true.

Supplementary marker behaviors

In the second case, we want to provide a behavior interface with a behavior adapter factory as usual (e.g. with some form fields and a custom storage or a few methods implemented in an adapter), but we also need a custom marker. Here, we use both the provides and  marker attributes to <plone:behavior /> to reference the two interfaces, as well as a factory.

To show a slightly more interesting example, here is a behavior from a project that lets content authors with particular permissions (iz.EditOfficialReviewers and iz.EditUnofficialReviewers), nominate the "official" and any "unofficial" reviewers for a given content item. The behavior provides the necessary form fields to support this, but it also sets a marker interface that enables an ILocalRoleProvider adapter to automatically grant local roles to the chosen reviewers, as well as a custom indexer that lists the reviewers.

The ZCML registration looks like this:

    <plone:behavior
        title="Reviewers"
        description="The ability to assign a list of official and/or unofficial reviewers to an item, granting those users special powers."
        provides=".reviewers.IReviewers"
        factory="plone.behavior.AnnotationStorage"
        marker=".reviewers.IReviewersMarker"
        />

Notice the use of the AnnotationStorage factory. This is a re-usable factory that can be used to easily create behaviors from schema interfaces that store their values in annotations. We'll describe this in more detail later. We could just as easily have provided our own factory in this example.

This whole package is grokked, so in configure.zcml we have:

    <grok:grok package="." />

The reviewers.py module contains the following:

"""Behavior to enable certain users to nominate reviewers

Includes form fields, an indexer to make it easy to find the items with
specific reviewers, and a local role provider to grant the Reviewer and
OfficialReviewer roles appropriately.
"""

from five import grok

from zope.interface import alsoProvides, Interface

from plone.directives import form
from zope import schema

from plone.formwidget.autocomplete.widget import AutocompleteMultiFieldWidget

from borg.localrole.interfaces import ILocalRoleProvider
from plone.indexer.interfaces import IIndexer
from Products.ZCatalog.interfaces import IZCatalog

from iz.behaviors import MessageFactory as _

class IReviewers(form.Schema):
    """Support for specifying official and unofficial reviewers
    """
    
    form.fieldset(
            'ownership',
            label=_(u'Ownership'),
            fields=('official_reviewers', 'unofficial_reviewers'),
        )

    form.widget(official_reviewers=AutocompleteMultiFieldWidget)
    form.write_permission(official_reviewers='iz.EditOfficialReviewers')
    official_reviewers = schema.Tuple(
            title=_(u'Official reviewers'),
            description=_(u'People or groups who may review this item in an official capacity.'),
            value_type=schema.Choice(title=_(u"Principal"), source="plone.principalsource.Principals"),
            required=False,
            missing_value=(), # important!
        )
    
    form.widget(unofficial_reviewers=AutocompleteMultiFieldWidget)
    form.write_permission(unofficial_reviewers='iz.EditUnofficialReviewers')
    unofficial_reviewers = schema.Tuple(
            title=_(u'Unofficial reviewers'),
            description=_(u'People or groups who may review this item in a supplementary capacity'),
            value_type=schema.Choice(title=_(u"Principal"), source="plone.principalsource.Principals"),
            required=False,
            missing_value=(), # important!
        )

alsoProvides(IReviewers, form.IFormFieldProvider)

class IReviewersMarker(Interface):
    """Marker interface that will be provided by instances using the
    IReviewers behavior. The ILocalRoleProvider adapter is registered for
    this marker.
    """

class ReviewerLocalRoles(grok.Adapter):
    """Grant local roles to reviewers when the behavior is used.
    """
    
    grok.implements(ILocalRoleProvider)
    grok.context(IReviewersMarker)
    grok.name('iz.behaviors.reviewers')
    
    def getRoles(self, principal_id):
        """If the user is in the list of reviewers for this item, grant
        the Reader, Editor and Contributor local roles.
        """
        
        c = IReviewers(self.context, None)
        if c is None or (not c.official_reviewers and not c.unofficial_reviewers):
            return ()
        
        if principal_id in c.official_reviewers:
            return ('Reviewer', 'OfficialReviewer',)
        elif principal_id in c.unofficial_reviewers:
            return ('Reviewer',)
        
        return ()
        
    def getAllRoles(self):
        """Return a list of tuples (principal_id, roles), where roles is a
        list of roles for the given user id.
        """
        
        c = IReviewers(self.context, None)
        if c is None or (not c.official_reviewers and not c.unofficial_reviewers):
            return
        
        seen = set ()
        
        for principal_id in c.official_reviewers:
            seen.add(principal_id)
            yield (principal_id, ('Reviewer', 'OfficialReviewer'),)
            
        for principal_id in c.unofficial_reviewers:
            if principal_id not in seen:
                yield (principal_id, ('Reviewer',),)

class ReviewersIndexer(grok.MultiAdapter):
    """Catalog indexer for the 'reviewers' index.
    """
    
    grok.implements(IIndexer)
    grok.adapts(IReviewersMarker, IZCatalog)
    grok.name('reviewers')
    
    def __init__(self, context, catalog):
        self.reviewers = IReviewers(context)
    
    def __call__(self):
        official = self.reviewers.official_reviewers or ()
        unofficial = self.reviewers.unofficial_reviewers or ()
        return tuple(set(official + unofficial))

Note that the iz.EditOfficialReviewers and iz.EditUnofficialReviewers permissions are defined and granted elsewhere.

This is quite a complex behavior, but hopefully you can see what's going on:

  • There is a standard schema interface, which is grokked for form hints using plone.directives.form and marked as an IFormFieldProvider. It uses plone.formwidget.autocomplete and plone.principalsource to implement the fields.
  • We define a marker interface (IReviewersMarker) and register this with the marker attribute of the <plone:behavior /> directive.
  • We define an adapter from this marker to ILocalRoles from borg.localrole. Here, we have chosen to use grokcore.component (via five.grok) to register the adapter. We could have used an <adapter /> ZCML statement as well, of course.
  • Similarly, we define a multi-adapter to IIndexer, as provided by plone.indexer. Again, we've chosen to use convention-over-configuration via five.grok to register this.

Although this behavior provides a lot of functionality, it is no more difficult for integrators to use than any other: they would simply list the behavior interface (iz.behaviors.reviewers.IReviewers in this case) in the FTI, and all this functionality comes to life. This is the true power of behaviors: developers can bundle up complex functionality into re-usable behaviors, which can then be enabled on a per-type basis by integrators (or the same developers in lazier moments).

5. Schema-only behaviors using annotations or attributes

Writing behaviors that provide schema fields

Oftentimes, we simply want a behavior to be a reusable collection of form fields. Integrators can then compose their types by combining different schemata. Writing the behavior schema is no different to writing any other schema interface. But how and where do we store the values? By default, plone.behavior provides two alternatives.

Using annotations

Annotations, as provided by the zope.annotation package, are a standard means of storing of key/value pairs on objects. In the default implementation (so-called attribute annotation), the values are stored in a BTree on the object called __annotations__. The raw annotations API involves adapting the object to the IAnnotations interface, which behaves like a dictionary, and storing values under unique keys here. plone.behavior comes with a special type of factory that means you can simply adapt an object to its behavior interface to get an adapter providing this interface, on which you can get and set values, which are eventually stored in annotations.

We've already seen an example of this factory:

    <plone:behavior
        title="Reviewers"
        description="The ability to assign a list of official and/or unofficial reviewers to an item, granting those users special powers."
        provides=".reviewers.IReviewers"
        factory="plone.behavior.AnnotationStorage"
        marker=".reviewers.IReviewersMarkere"
        />

Here, plone.behavior.AnnotationStorage is a behavior factory that can be used by any behavior with an interface that consists entirely of zope.schema fields. It simply stores those items in object annotations, saving you the trouble of writing your own annotation storage adapter. If you adapt an object for which the behavior is enabled to the behavior interface, you will be able to read and write values off the resultant adapter as normal.

Storing attributes

This approach is convenient, but there is another approach that is even more convenient, and, contrary to what you may think, may be more efficient: simply store the attributes of the schema interface directly on the content object.

As an example, here's the standard IRelatedItems behavior from plone.app.dexerity:

    <plone:behavior
        title="Related items"
        description="Adds the ability to assign related items"
        provides=".related.IRelatedItems"
        for="plone.dexterity.interfaces.IDexterityContent"
        />

The IRelatedItems schema looks like this:

from zope.interface import alsoProvides

from z3c.relationfield.schema import RelationChoice, RelationList

from plone.formwidget.contenttree import ObjPathSourceBinder
from plone.directives import form

class IRelatedItems(form.Schema):
    """Behavior interface to make a type support related items.
    """

    form.fieldset('categorization', label=u"Categorization",
                  fields=['relatedItems'])

    relatedItems = RelationList(
        title=u"Related Items",
        default=[],
        value_type=RelationChoice(title=u"Related",
                      source=ObjPathSourceBinder()),
        required=False,
        )

alsoProvides(IRelatedItems, form.IFormFieldProvider)

This is a standard schema using plone.directives.form (the package is also grokked). However, notice the lack of a behavior factory. This is a directly provided "marker" interface, except that it has attributes, and so it is not actually a marker interface. The result is that the relatedItems attribute will be stored directly onto a content object when first set (usually in the add form).

This approach has a few advantages:

  • There is no need to write or use a separate factory, so it is a little easier to use.
  • The attribute is available on the content object directly, so you can write context/relatedItems in a TAL expression, for example. This does require that it has been set at least once, though! If the schema is used in the type's add form, that will normally suffice, but old instances of the same type may not have the attribute and could raise an AttributeError.
  • If the value is going to be used frequently, and especially if it is read when viewing the content object, storing it in an attribute is more efficient than storing it in an annotation. (This is because the __annotations__ BTree is a separate persistent object which has to be loaded into memory, and may push something else out of the ZODB cache.)

The possible disadvantages are:

  • The attribute name may collide with another attribute on the object, either from its class, its base schema, or another behavior. Whether this is a problem in practice depends largely on whether the name is likely to be unique. In most cases, it will probably be sufficiently unique.
  • If the attribute stores a large value, it will increase memory usage, as it will be loaded into memory each time the object is fetched from the ZODB. However, you should use BLOBs or BTrees to store large values anyway. Loading an object with a BLOB or BTree does not mean loading the entire BLOB or Btree, so the memory overhead does not occur unless the whole BLOB or BTree is actually used.

"The moral of this story? BTrees do not always make things more efficient!" ~ Laurence Rowe

6. Testing behaviors

How to write unit tests for behaviors

Behaviors, like any other code, should be tested. If you are writing a behavior with just a marker interface or schema interface, it is probably not necessary to test the interface. However, any actual code, such as a behavior adapter factory, ought to be tested.

Writing a behavior integration test is not very difficult if you are happy to depend on Dexterity in your test. You can create a dummy type by instantiating a Dexterty FTI in portal_types and enable your behavior by adding its interface name to the behaviors property.

In many cases, however, it is better not to depend on Dexterity at all. It is not too difficult to mock what Dexterity does to enable behaviors on its types. The following example is taken from collective.gtags and tests the ITags behavior we saw on the first page of this manual.

Behaviors
=========

This package provides a behavior called `collective.gtags.behaviors.ITags`.
This adds a `Tags` field called `tags` to the "Categorization" fieldset, with
a behavior adapter that stores the chosen tags in the Subject metadata field.

To learn more about the `Tags` field and how it works, see `tagging.txt`.

Test setup
----------

Before we can run these tests, we need to load the collective.gtags
configuration. This will configure the behavior.

    >>> configuration = """\
    ... <configure
    ...      xmlns="http://namespaces.zope.org/zope"
    ...      i18n_domain="collective.gtags">
    ...      
    ...     <include package="Products.Five" file="meta.zcml" />
    ...     <include package="collective.gtags" file="behaviors.zcml" />
    ...     
    ... </configure>
    ... """

    >>> from StringIO import StringIO
    >>> from zope.configuration import xmlconfig
    >>> xmlconfig.xmlconfig(StringIO(configuration))

This behavior can be enabled for any `IDublinCore`. For the purposes of
testing, we will use the CMFDefault Document type and a custom
IBehaviorAssignable adapter to mark the behavior as enabled.

    >>> from Products.CMFDefault.Document import Document

    >>> from plone.behavior.interfaces import IBehaviorAssignable
    >>> from collective.gtags.behaviors import ITags
    >>> from zope.component import adapts
    >>> from zope.interface import implements
    >>> class TestingAssignable(object):
    ...     implements(IBehaviorAssignable)
    ...     adapts(Document)
    ...     
    ...     enabled = [ITags]
    ...     
    ...     def __init__(self, context):
    ...         self.context = context
    ...     
    ...     def supports(self, behavior_interface):
    ...         return behavior_interface in self.enabled
    ...     
    ...     def enumerate_behaviors(self):
    ...         for e in self.enabled:
    ...             yield queryUtility(IBehavior, name=e.__identifier__)

    >>> from zope.component import provideAdapter
    >>> provideAdapter(TestingAssignable)

Behavior installation
---------------------

We can now test that the behavior is installed when the ZCML for this package
is loaded.

    >>> from zope.component import getUtility
    >>> from plone.behavior.interfaces import IBehavior
    >>> tags_behavior = getUtility(IBehavior, name='collective.gtags.behaviors.ITags')
    >>> tags_behavior.interface
    <InterfaceClass collective.gtags.behaviors.ITags>

We also expect this behavior to be a form field provider. Let's verify that.

    >>> from plone.directives.form import IFormFieldProvider
    >>> IFormFieldProvider.providedBy(tags_behavior.interface)
    True

Using the behavior
------------------

Let's create a content object that has this behavior enabled and check that
it works.

    >>> doc = Document('doc')
    >>> tags_adapter = ITags(doc, None)
    >>> tags_adapter is not None
    True

We'll check that the `tags` set is built from the `Subject()` field:

    >>> doc.setSubject(['One', 'Two'])
    >>> doc.Subject()
    ('One', 'Two')
    
    >>> tags_adapter.tags == set(['One', 'Two'])
    True
    
    >>> tags_adapter.tags = set(['Two', 'Three'])
    >>> doc.Subject() == ('Two', 'Three')
    True

This test tries to prove that the behavior is correctly installed and works as intended on a suitable content class. It is not a true unit test, of course. For that, we would simply test the Tags adapter directly on a dummy context, but that is not terribly interesting, since all it does is convert sets to tuples. 

First, we configure the package. To keep the test small, we limit ourselves to the behaviors.zcml file, which in this case will suffice. We still need to include a minimal set of ZCML from Five.

Next, we implement an IBehaviorAssignable adapter. This is a low-level component used by plone.behavior to determine if a behavior is enabled on a particular object. Dexterity provides an implementation that checks the type's FTI. Our test version is much simpler - it hardcodes the supported behaviors.

With this in place, we first check that the IBehavior utility has been correctly registered. This is essentially a test to show that we've used the <plone:behavior /> directive as intended. We also verify that our schema interface is an IFormFieldsProvider. For a non-form behavior, we'd obviously omit this.

Finally, we test the behavior. We've chosen to use CMFDefault's Document type for our test, as the behavior adapter requires an object providing IDublinCore. If we were less lazy, we'd write our own class and implement IDublinCore directly. However, in many cases, the types from CMFDefault are going to provide convenient test fodder.

Obviously, if our behavior was more complex, we'd add more intricate tests. By the last section of the doctest, we have enough context to test the adapter factory.

To run the test, we need a test suite. In tests.py, we have:

import doctest
import unittest
from zope.testing import doctestunit
from zope.app.testing import setup

def setUp(test):
    pass
        
def tearDown(test):
    setup.placefulTearDown()

def test_suite():
    return unittest.TestSuite((
        doctestunit.DocFileSuite(
            'behaviors.txt',
            setUp=setUp, tearDown=tearDown,
            optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS),
        ))

This runs the behaviors.txt doctest from the same directory as the tests.py file. To run the test, we can use the usual test runner:

$ ./bin/instance test -s collective.gtags

A note about marker interfaces

Note that marker interface support depends on code that is implemented in Dexterity and is non-trivial to reproduce in a test. If you need a marker interface in a test, set it manually with zope.interface.alsoProvides, or write an integration test with Dexterity content.