Builder pattern for creating Plone objects in tests
Project description
ftw.builder
Create Plone objects in tests with the Builder Pattern.
The builder pattern simplifies constructing objects. In tests we often need to create Plone objects, sometimes a single object, sometimes a whole graph of objects. Using the builder pattern allows us to do this in a DRY way, so that we do not repeat this over and over.
from ftw.builder import create
from ftw.builder import Builder
def test_foo(self):
folder = create(Builder('folder')
.titled('My Folder')
.in_state('published'))
Installation
Add ftw.builder as (test-) dependency to your package in setup.py:
tests_require = [
'ftw.builder',
]
setup(name='my.package',
tests_require=tests_require,
extras_require={'tests': tests_require})
Usage
Setup builder session in your testcase
class TestPerson(unittest2.TestCase):
def setUp(self):
session.current_session = session.factory()
def tearDown(self):
session.current_session = None
In plone projects you can use the BUILDER_LAYER which your testing layer should base on. So the the session management is handled by the BUILDER_LAYER:
from ftw.builder.testing import BUILDER_LAYER
class MyPackageLayer(PloneSandboxLayer):
defaultBases = (PLONE_FIXTURE, BUILDER_LAYER)
Use the builder for creating objects in your tests:
from ftw.builder import Builder
from ftw.builder import create
from my.package.testing import MY_PACKAGE_INTEGRATION_TESTING
from unittest2 import TestCase
class TestMyFeature(TestCase)
layer = MY_PACKAGE_INTEGRATION_TESTING
def test_folder_is_well_titled(self):
folder = create(Builder('folder')
.titled('My Folder')
.in_state('published'))
self.assertEquals('My Folder', folder.Title())
Session
The BuilderSession keeps configuration for multiple builders. It is set up and destroyed by the BUILDER_LAYER and can be configured or replaced by a custom session with set_builder_session_factory.
Auto commit
When having a functional testing layer (plone.app.testing.FunctionalTesting) and doing browser tests it is necessary that the new objects are committed in the ZODB. When using a IntegrationTesting on the other hand it is essential that nothing is comitted, since this would break test isolation.
The session provides the auto_commit option (dislabed by default), which commits to the ZODB after creating an object. Since it is disabled by default you need to enable it in functional test cases.
A default session factory functional_session_factory that enables the auto-commit feature is provided:
def functional_session_factory():
sess = BuilderSession()
sess.auto_commit = True
return sess
You can use set_builder_session_factory to replace the default session factory in functional tests. Make sure to also base your fixture on the BUILDER_LAYER fixture:
from ftw.builder.session import BuilderSession
from ftw.builder.testing import BUILDER_LAYER
from ftw.builder.testing import functional_session_factory
from ftw.builder.testing import set_builder_session_factory
from plone.app.testing import FunctionalTesting
from plone.app.testing import IntegrationTesting
from plone.app.testing import PLONE_FIXTURE
from plone.app.testing import PloneSandboxLayer
class MyPackageLayer(PloneSandboxLayer):
defaultBases = (PLONE_FIXTURE, BUILDER_LAYER)
MY_PACKAGE_FIXTURE = MyPackageLayer()
MY_PACKAGE_INTEGRATION_TESTING = IntegrationTesting(
bases=(MY_PACKAGE_FIXTURE, ),
name="MyPackage:Integration")
MY_PACKAGE_FUNCTIONAL_TESTING = FunctionalTesting(
bases=(MY_PACKAGE_FIXTURE,
set_builder_session_factory(functional_session_factory)),
name="MyPackage:Integration")
Plone object builders
For creating Plone objects (Archetypes or Dexterity) there are some methods for setting basic options:
within(container) - tell the builder where to create the object
titled(title) - name the object
having(field=value) - set the value of any field on the object
in_state(review_state) - set the object into any review state of the workflow configured for this type
providing(interface1, interface2, ...) - let the object provide interfaces
Default builders
The ftw.builder ships with some builders for some default Plone (Archetypes) content types, but the idea is that you can easily craft your own builders for your types or extend existing builders.
The built-in builders are:
folder - creates an Archetypes folder
page (or Document) - creates an Archetypes page (alias Document)
file - creates a File
image - creates an Archetypes Image
Attaching files
The default Archetypes file builder let’s you attach a file or create the file with dummy content. The archetypes image builder provides a real image (1x1 px GIF):
file1 = create(Builder('file')
.with_dummy_content())
file2 = create(Builder('file')
.attach_file_containing('File content', name='filename.pdf')
image1 = create(Builder('image')
.with_dummy_content())
Users builder
There is a “user” builder registered by default.
By default the user is named John Doe:
john = create(Builder('user'))
john.getId() == "john.doe"
john.getProperty('fullname') == "Doe John"
john.getProperty('email') == "john@doe.com"
john.getRoles() == ['Member', 'Authenticated']
Changing the name of the user changes also the userid and the email address. You can also configure all the other necessary things:
folder = create(Builder('folder'))
hugo = create(Builder('user')
.named('Hugo', 'Boss')
.with_roles('Contributor')
.with_roles('Editor', on=folder))
hugo.getId() == 'hugo.boss'
hugo.getProperty('fullname') == 'Boss Hugo'
hugo.getProperty('email') == 'hugo@boss.com'
hugo.getRoles() == ['Contributor', 'Authenticated']
hugo.getRolesInContext(folder) == ['Contributor', 'Authenticated', 'Editor']
Groups builder
The “group” bilder helps you create groups:
folder = create(Builder('folder'))
user = create(Builder('user'))
group = create(Builder('group')
.titled('Administrators')
.with_roles('Site Administrator')
.with_roles('Editor', on=folder)
.with_members(user))
Creating new builders
The idea is that you create your own builders for your application. This might be builders creating a single Plone object (Archetypes or Dexterity) or builders creating a set of objects using other builders.
Creating python builders
Define a simpe builder class for your python object and register them in the builder registry
class PersonBuilder(object):
def __init__(self, session):
self.session = session
self.children_names = []
self.arguments = {}
def of_age(self):
self.arguments['age'] = 18
return self
def with_children(self, children_names):
self.children_names = children_names
return self
def having(self, **kwargs):
self.arguments.update(kwargs)
return self
def create(self, **kwargs):
person = Person(
self.arguments.get('name'),
self.arguments.get('age'))
for name in self.children_names:
person.add_child(
create(Builder('person').having(name=name, age=5))
)
return person
builder_registry.register('person', PersonBuilder)
Creating Archetypes builders
Use the ArchetypesBuilder base class for creating new Archetypes builders. Set the portal_type and your own methods.
from ftw.builder.archetypes import ArchetypesBuilder
from ftw.builder import builder_registry
class NewsBuilder(ArchetypesBuilder):
portal_type = 'News Item'
def containing(self, text):
self.arguments['text'] = text
return self
builder_registry.register('news', NewsBuilder)
Creating Dexterity builders
Use the DexterityBuilder base class for creating new Dexterity builders. Set the portal_type and your own methods.
from ftw.builder.dexterity import DexterityBuilder
from ftw.builder import builder_registry
class DocumentBuilder(DexterityBuilder):
portal_type = 'dexterity.document'
def with_dummy_content(self):
self.arguments["file"] = NamedBlobFile(data='Test data', filename='test.doc')
return self
Events
You can do things before and after creating the object:
class MyBuilder(ArchetypesBuilder):
def before_create(self):
super(NewsBuilder, self).before_create()
do_something()
def after_create(self):
do_something()
super(NewsBuilder, self).after_create()
Overriding existing builders
Sometimes it is necessary to override an existing builder. For re-registering an existing builder you can use the force flag:
builder_registry.register('file', CustomFileBuilder, force=True)
Other builders
Python package builder
The Python package builder builds a python package on the file system.
creates a setup.py
namespace packages are supported
builds the egg-info
creates a configure.zcml on demand
Example:
>>> import tempfile
>>> tempdir = tempfile.mkdtemp()
>>> package = create(Builder('python package')
... .at_path(tempdir)
... .named('my.package')
...
... .with_root_directory('docs')
... .with_root_file('docs/HISTORY.txt', 'CHANGELOG...')
... .with_file('resources/print.css', 'body {}', makedirs=True)
...
... .with_subpackage(Builder('subpackage')
... .named('browser')))
>>>
>>> with package.imported() as module:
... print module
...
<module 'my.package' from '...../tmpcAZhM2/my/package/__init__.py'>
It is also possible to create / load ZCML, all you need is a stacked configuration context. Plone’s testing layers provide a configuration context, but be aware that the component registry is not isolated. You may want to isolate the component registry with plone.testing.zca.pushGlobalRegistry.
package = create(
Builder('python package')
.named('the.package')
.at_path(self.layer['temp_directory'])
.with_subpackage(
Builder('subpackage')
.named('browser')
.with_file('hello_world.pt', '"Hello World"')
.with_zcml_node('browser:page',
**{'name': 'hello-world.json',
'template': 'hello_world.pt',
'permission': 'zope2.View',
'for': '*'})))
with package.zcml_loaded(self.layer['configurationContext']):
self.assertEqual('"Hello World"',
self.layer['portal'].restrictedTraverse('hello-world.json')())
Generic Setup profile builder
The “genericsetup profile” builder helps building a profile within a python package:
create(Builder('python package')
.named('the.package')
.at_path(self.layer['temp_directory'])
.with_profile(Builder('genericsetup profile')
.with_fs_version('3109')
.with_dependencies('collective.foo:default')
.with_file('types/MyType.xml', '<object></object>',
makedirs=True)))
Plone upgrade step builder
Builds a Generic Setup upgrade step for a package:
create(Builder('python package')
.named('the.package')
.at_path(self.layer['temp_directory'])
.with_profile(Builder('genericsetup profile')
.with_upgrade(Builder('plone upgrade step')
.upgrading('1000', '1001')
.titled('Add some actions...')
.with_description('Some details...'))))
ZCML file builder
The ZCML builder builds a ZCML file:
create(Builder('zcml')
.at_path('/path/to/my/package/configure.zcml')
.with_i18n_domain('my.package')
.include('.browser')
.include('Products.GenericSetup', file='meta.zcml')
.include(file='profiles.zcml')
.with_node('i18n:registerTranslations', directory='locales'))
Development / Tests
$ git clone https://github.com/4teamwork/ftw.builder.git
$ cd ftw.builder
$ ln -s development.cfg buildout.cfg
$ python2.7 bootstrap.py
$ ./bin/buildout
$ ./bin/test
Links
Continuous integration: https://jenkins.4teamwork.ch/search?q=ftw.builder
Copyright
This package is copyright by 4teamwork.
ftw.builder is licensed under GNU General Public License, version 2.
Changelog
1.6.2 (2015-05-20)
Package builder: make package version configurable. [jone]
1.6.1 (2015-05-20)
Package builder: update pkg_resources working set when loading package. [jone]
Add creation date setter to builder. [mbaechtold]
1.6.0 (2014-12-31)
Add more default builders:
a “zcml” builder for creating ZCML files
a “python package” builder for creating python package on the file system
a “namespace package” builder, used internally by the “python package” builder
a “subpackage” builder for extending a python package with nested packages
a “genericsetup profile” builder
a “plone upgrade step” builder for building Generic Setup upgrade steps
[jone]
1.5.2 (2014-12-06)
File builder: fix default filename encoding for AT. This was a regression in 1.5.0, where filenames were changed to unicode because of the consolidation of Archetypes and Dexterity builders. [jone]
1.5.1 (2014-12-03)
Fix NamedBlobFile import issue for Plone <= 4.2 where blobs are optional. [jone]
1.5.0 (2014-12-03)
Restore Plone 4.1 compatibility by making any DX imports conditional. [lgraf]
Plone 5 support: default content builders are switched to the dexterity implementation by default for Plone >= 5. The builder classes were moved from archetypes module to content module. [jone]
1.4.0 (2014-09-04)
Implement collection builder. [jone]
Fixed default value setter for different “owners” field. [phgross]
1.3.4 (2014-08-29)
DxBuilder: Fix encoding problem when filling default value for “owners” field. [jone]
1.3.3 (2014-06-05)
DxBuilder: Fix check if field is present (to determine if default values should be set). [lgraf]
DxBuilder: Make sure default values are set before adding content to container. [lgraf]
1.3.2 (2014-05-29)
Update object_provides in catalog when using provides(). [jone]
1.3.1 (2014-03-26)
Provide a real image (1x1 px GIF) for the Archetypes ImageBuilder. [mathias.leimgruber]
1.3.0 (2014-03-25)
Implement ATImage builder. [jone]
Reindex object security after setting local roles for a principal. [mathias.leimgruber]
Support “on” keyword argument for with_roles method in group builder. [mathias.leimgruber]
1.2.0 (2014-01-31)
Add providing() method to Plone builder, letting the object provide interfaces. [jone]
Don’t use IDNormalizer for Mail. It handles Umlauts weird. [tschanzt]
1.1.0 (2013-09-13)
Add groups builder. [jone]
Add users builder. [jone]
Added modification date setter for PloneObject Builders. [phgross]
1.0.0 (2013-08-12)
Added dexterity support. [phgross]
Initial implementation [jone]
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file ftw.builder-1.6.2.zip
.
File metadata
- Download URL: ftw.builder-1.6.2.zip
- Upload date:
- Size: 61.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 12e15f571c7919e9254477e85cd6fb2d7e10095500cde76bb00f1cc82878896e |
|
MD5 | f3d33ee87e698a594dbb99dfbba0e238 |
|
BLAKE2b-256 | 9cb6d5a8177e39346f4ab5b9d22d6a840cc1ceb208117229bbbc832ba65d5594 |