Skip to content

Commit

Permalink
More docs and tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
jamadden committed Jul 28, 2016
1 parent 971e6c9 commit b566c7e
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 3 deletions.
9 changes: 9 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@

Changes
=======


1.0.0 (unreleased)
------------------

- Add Python 3 support.
- Initial PyPI release.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
include .coveragerc
include CHANGES
include INSTALL
include README.rst
include LICENSE
include TODO
include nose2.cfg
Expand Down
170 changes: 170 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,173 @@

Support for writing tests, particularly in a Zope3/ZTK environment,
using either nose2 or zope.testing.

Installation
============

nti.testing can be installed using pip, either from the git repository
or from PyPI::

pip install nti.testing


PyHamcrest
==========

nti.testing provides a group of `PyHamcrest`_ matchers. There are both
general-purpose matchers and matchers that are of use to users of
`zope.interface`_ and `zope.schema`_.


.. _PyHamcrest: https://pyhamcrest.readthedocs.io/en/latest/
.. _zope.interface: https://pypi.python.org/pypi/zope.interface
.. _zope.schema: https://pypi.python.org/pypi/zope.schema


Matchers can be imported from the ``nti.testing.matchers`` module.

Basic Matchers
--------------

``is_true`` and ``is_false`` check the ``bool`` value of a supplied
object (we're using literals for explanation purposes, but it
obviously makes more sense, and reads better, when the matched object
is a variable, often of a more complex type)::

>>> from hamcrest import assert_that, is_
>>> from nti.testing.matchers import is_true, is_false
>>> assert_that("Hi", is_true())
>>> assert_that(0, is_false())

Interface Matchers
------------------

Next we come to matchers that support basic use of ``zope.interface``.

We can check that an object provides an interface and that a factory
implements it::

>>> from zope.interface import Interface, Attribute, implementer
>>> class IThing1(Interface):
... pass
>>> class IThing2(Interface):
... pass
>>> class IThings(IThing1, IThing2):
... got_that_thing_i_sent_you = Attribute("Did you get that thing?")
>>> @implementer(IThings)
... class Thing(object):
... pass

>>> from nti.testing.matchers import provides, implements
>>> assert_that(Thing(), provides(IThings))
>>> assert_that(Thing, implements(IThings))

The attentive reader will have noticed that ``IThings`` defines an
attribute that our implementation doesn't *actually* provide. This is
where the next stricter check comes in. ``verifiably_provides`` uses
the interface machinery to determine that all attributes and methods
specified by the interface are present as described::


>>> from nti.testing.matchers import verifiably_provides
>>> assert_that(Thing(), verifiably_provides(IThing2, IThing1))
>>> assert_that(Thing(), verifiably_provides(IThings))
Traceback (most recent call last):
...
AssertionError:...
Expected: object verifiably providing IThings
but: <class 'Thing'> failed to provide attribute "got_that_thing_i_sent_you" from IThings
<BLANKLINE>

``zope.interface`` can only check whether or not an attribute or
method is present. To place (arbitrary) tighter constraints on the
values of the attributes, we can step up to ``zope.schema`` and the
``validly_provides`` matcher::

>>> from zope.schema import Bool
>>> class IBoolThings(IThing1, IThing2):
... got_that_thing_i_sent_you = Bool()
>>> @implementer(IBoolThings)
... class BoolThing(object):
... pass

``validly_provides`` is a superset of ``verifiably_provides``::

>>> from nti.testing.matchers import validly_provides
>>> assert_that(BoolThing(), validly_provides(IThing1, IThing2))
>>> assert_that(BoolThing(), validly_provides(IBoolThings))
Traceback (most recent call last):
...
AssertionError:...
Expected: (object verifiably providing IBoolThings and object validly providing <InterfaceClass ....IBoolThings>)
but: object verifiably providing IBoolThings <class 'BoolThing'> failed to provide attribute "got_that_thing_i_sent_you" from IBoolThings
<BLANKLINE>

For finer grained control, we can compare data against schema fields::

>>> from nti.testing.matchers import validated_by, not_validated_by
>>> field = IBoolThings.get('got_that_thing_i_sent_you')
>>> assert_that(True, is_(validated_by(field)))
>>> assert_that(None, is_(not_validated_by(field)))

Parent/Child Relationships
--------------------------

The ``aq_inContextOf`` matcher uses the concepts from Acquisition to
check parent/child relationships::

>>> from nti.testing.matchers import aq_inContextOf
>>> class Parent(object):
... pass
>>> class Child(object):
... __parent__ = None
>>> parent = Parent()
>>> child = Child()
>>> child.__parent__ = parent

>>> assert_that(child, aq_inContextOf(parent))

Test Fixtures
=============

Support for test fixtures can be found in ``nti.testing.base`` and
``nti.testing.layers``. The ``base`` package includes fully-fleshed
out base classes for direct use, while the ``layers`` package includes
mixins that can be used to construct your own test layers.

The ``base`` package makes a distinction between "normal" and "shared"
fixtures. Normal fixtures are those that are used for a single test
case. They are established via ``setUp`` and torn down via
``tearDown``.

In contrast, shared fixtures are expected to endure for the duration
of all the tests in the class or all the tests in the layer. These are
best used when the fixture is expensive to create. Anything that
extends from ``base.AbstractSharedTestBase`` creates a shared fixture.
Through the magic of metaclasses, such a subclass can also be assigned
as the ``layer`` property of another class to be used as a test layer
that can be shared across more than one class.

The most important bases are ``base.ConfiguringTestBase`` and
``base.SharedConfiguringTestBase``. These are both fixtures for
configuring ZCML, either from existing packages or complete file
paths. To use these, subclass them and define class attributes
``set_up_packages`` and (if necessary) ``features``::

>>> from nti.testing.base import ConfiguringTestBase
>>> import zope.security
>>> class MyConfiguringTest(ConfiguringTestBase):
... set_up_packages = (
... 'zope.component', # the default configuration by name
... # a named file in a named package
... ('ftesting.zcml', 'zope.traversing.tests'),
... # an imported module
... zope.security,
... # Our own package; in a test, this will mean the parent
... # package
... ".")

We would then proceed to write our test methods. The packages that we
specified will be set up and torn down around every test method. In
addition, the ``zope.testing`` cleanup functions will also run around
every test method.
10 changes: 7 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@
'zope.testrunner',
]

def _read(fname):
with codecs.open(fname, encoding='utf-8') as f:
return f.read()

setup(
name = 'nti.testing',
version = VERSION,
author = 'Jason Madden',
author_email = 'jason@nextthought.com',
description = "Support for testing code",
long_description = codecs.open('README.rst', encoding='utf-8').read(),
license = 'Proprietary',
keywords = 'nose testing',
long_description = _read('README.rst') + _read('CHANGES'),
license = 'Apache',
keywords = 'nose2 testing zope3 ZTK hamcrest',
url = 'https://github.com/NextThought/nti.testing',
classifiers = [
'Intended Audience :: Developers',
Expand Down
36 changes: 36 additions & 0 deletions src/nti/testing/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,54 @@

import unittest
import doctest
import os
import re
from zope.testing import renormalizing


class TestImport(unittest.TestCase):
def test_import(self):
for name in ('base', 'layers', 'matchers', 'time'):
__import__('nti.testing.' + name)

checker = renormalizing.RENormalizing([
# Python 3 bytes add a "b".
(re.compile(r'b(".*?")'), r"\1"),
(re.compile(r"b('.*?')"), r"\1"),
# Windows shows result from 'u64' as long?
(re.compile(r"(\d+)L"), r"\1"),
# Python 3 adds module name to exceptions.
(re.compile("ZODB.POSException.ConflictError"), r"ConflictError"),
(re.compile("ZODB.POSException.POSKeyError"), r"POSKeyError"),
(re.compile("ZODB.POSException.ReadConflictError"), r"ReadConflictError"),
(re.compile("ZODB.POSException.Unsupported"), r"Unsupported"),
(re.compile("ZODB.interfaces.BlobError"), r"BlobError"),
# XXX document me
(re.compile(r'\%(sep)s\%(sep)s' % dict(sep=os.path.sep)), '/'),
(re.compile(r'\%(sep)s' % dict(sep=os.path.sep)), '/'),
])


def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestImport))
suite.addTest(doctest.DocFileSuite(
'test_component_cleanup_broken.txt'))

if os.path.exists('../../../../README.rst'):
suite.addTest(doctest.DocFileSuite(
'../../../../README.rst',
optionflags=doctest.ELLIPSIS,
checker=checker,
))
elif os.path.exists('README.rst'):
# tox
suite.addTest(doctest.DocFileSuite(
os.path.abspath("README.rst"),
module_relative=False,
optionflags=doctest.ELLIPSIS,
checker=checker,
))
return suite


Expand Down

0 comments on commit b566c7e

Please sign in to comment.