Skip to content

Commit

Permalink
Update documentation, add some API conveniences.
Browse files Browse the repository at this point in the history
  • Loading branch information
jamadden committed Sep 8, 2021
1 parent 309f10a commit fba58c0
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 60 deletions.
8 changes: 7 additions & 1 deletion CHANGES.rst
Expand Up @@ -2,14 +2,20 @@
Changes
=========


3.1.0 (unreleased)
==================

- Add support for Python 3.9.

- Drop support for Python 3.5.

- Add the module alias ``nti.testing.mock``, which is either the
standard library ``unittest.mock``, or the backport ``mock``. This
allows easy imports when backwards compatibility matters.

- Make ``mock``, ``mock.Mock`` and various other API attributes,
like ``is_true``, available directly from the ``nti.testing`` namespace.

3.0.0 (2020-06-16)
==================

Expand Down
62 changes: 34 additions & 28 deletions README.rst
Expand Up @@ -47,19 +47,23 @@ general-purpose matchers and matchers that are of use to users of
.. _zope.interface: https://pypi.python.org/pypi/zope.interface
.. _zope.schema: https://pypi.python.org/pypi/zope.schema

.. NOTE: We rely on the Sphinx 'default_role' to turn single back quotes into links,
while still being compatible with rendering with plain docutils/readme_renderer
for PyPI.
Matchers can be imported from the ``nti.testing.matchers`` module.
Matchers can be imported from the `nti.testing.matchers` module; the most commonly used matchers
can be directly imported from `nti.testing`.

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

``is_true`` and ``is_false`` check the ``bool`` value of a supplied
`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)::
is a variable, often of a more complex type):

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

Expand All @@ -69,7 +73,7 @@ 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::
implements it:

>>> from zope.interface import Interface, Attribute, implementer
>>> class IThing1(Interface):
Expand All @@ -82,17 +86,17 @@ implements it::
... class Thing(object):
... def __repr__(self): return "<object Thing>"

>>> from nti.testing.matchers import provides, implements
>>> from nti.testing 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
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::
specified by the interface are present as described:

>>> from nti.testing.matchers import verifiably_provides
>>> from nti.testing import verifiably_provides
>>> assert_that(Thing(), verifiably_provides(IThing2, IThing1))
>>> assert_that(Thing(), verifiably_provides(IThings))
Traceback (most recent call last):
Expand All @@ -103,7 +107,7 @@ specified by the interface are present as described::
<BLANKLINE>

If multiple attributes or methods are not provided, all such missing
information is reported::
information is reported:

>>> class IThingReceiver(IThings):
... def receive(some_thing):
Expand All @@ -124,7 +128,7 @@ information is reported::
``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::
`validly_provides` matcher:

>>> from zope.schema import Bool
>>> class IBoolThings(IThing1, IThing2):
Expand All @@ -133,9 +137,9 @@ values of the attributes, we can step up to ``zope.schema`` and the
... class BoolThing(object):
... def __repr__(self): return "<object BoolThing>"

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

>>> from nti.testing.matchers import validly_provides
>>> from nti.testing import validly_provides
>>> assert_that(BoolThing(), validly_provides(IThing1, IThing2))
>>> assert_that(BoolThing(), validly_provides(IBoolThings))
Traceback (most recent call last):
Expand All @@ -145,20 +149,21 @@ values of the attributes, we can step up to ``zope.schema`` and the
but: object verifiably providing <....IBoolThings> Using class <class 'BoolThing'> the object <object BoolThing> has failed to implement interface ....IBoolThings: The ....IBoolThings.got_that_thing_i_sent_you attribute was not provided.
<BLANKLINE>

For finer grained control, we can compare data against schema fields::
For finer grained control, we can compare data against schema fields
using `validated_by` and `not_validated_by`:

>>> from nti.testing.matchers import validated_by, not_validated_by
>>> from nti.testing 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::
The `aq_inContextOf` matcher uses the concepts from `Acquisition` to
check parent/child relationships:

>>> from nti.testing.matchers import aq_inContextOf
>>> from nti.testing import aq_inContextOf
>>> class Parent(object):
... pass
>>> class Child(object):
Expand All @@ -172,8 +177,8 @@ check parent/child relationships::
Test Fixtures
=============

Support for test fixtures can be found in ``nti.testing.base`` and
``nti.testing.layers``. The ``base`` package includes fully-fleshed
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.

Expand All @@ -185,16 +190,16 @@ case. They are established via ``setUp`` and torn down via
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.
extends from `nti.testing.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
The most important bases are `nti.testing.base.ConfiguringTestBase` and
`nti.testing.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``::
``set_up_packages`` and (if necessary) ``features``:

>>> from nti.testing.base import ConfiguringTestBase
>>> import zope.security
Expand All @@ -218,13 +223,13 @@ Time
====

Having a clock that's guaranteed to move in a positive increasing way
in every call to ``time.time`` is useful. ``nti.testing.time``
in every call to ``time.time`` is useful. `nti.testing.time`
provides a decorator to accomplish this that ensures values always are
at least the current time and always are increasing. (It is not thread
safe.) It can be applied to functions or methods, and optionally takes
a ``granularity`` argument::
a ``granularity`` argument:

>>> from nti.testing.time import time_monotonically_increases
>>> from nti.testing import time_monotonically_increases
>>> from nti.testing.time import reset_monotonic_time
>>> @time_monotonically_increases(0.1) # increment by 0.1
... def test():
Expand All @@ -238,4 +243,5 @@ a ``granularity`` argument::
And The Rest
============

There are some other assorted utilities. See the API documentation for details.
There are some other assorted utilities, including support for working with
ZODB in `nti.testing.zodb`. See the API documentation for details.
28 changes: 28 additions & 0 deletions docs/api.rst
@@ -0,0 +1,28 @@
===============
API Reference
===============

``nti.testing``
===============

.. automodule:: nti.testing
:member-order: bysource
:exclude-members: Mock

.. class:: Mock

This is an alias for :class:`unittest.mock.Mock` (or its
backport) made available here for convenience.

Submodules
==========

.. toctree::
:maxdepth: 2
:caption: Submodule details

base
layers
matchers
time
zodb
2 changes: 1 addition & 1 deletion docs/conf.py
Expand Up @@ -190,5 +190,5 @@
'members': None,
'show-inheritance': None,
}
autodoc_member_order = 'groupwise'
#autodoc_member_order = 'groupwise'
autoclass_content = 'both'
10 changes: 4 additions & 6 deletions docs/index.rst
@@ -1,15 +1,13 @@
.. currentmodule:: nti.testing


.. include:: ../README.rst

.. toctree::
:maxdepth: 2
:maxdepth: 3
:caption: Contents:

base
layers
matchers
time
zodb
api
changelog


Expand Down
69 changes: 65 additions & 4 deletions src/nti/testing/__init__.py
@@ -1,9 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
nti.testing.
The ``nti.testing`` module exposes the most commonly used API from the
submodules (for example, ``nti.testing.is_true`` is just an alias for
``nti.testing.matchers.is_true``). The submodules may contain other
functions, though, so be sure to look at their documentation.
Importing this module has side-effects when zope.testing is
Importing this module has side-effects when :mod:`zope.testing` is
importable:
- Add a zope.testing cleanup to ensure that transactions never
Expand All @@ -14,9 +17,23 @@
``ConnectionStateError: Cannot close a connection joined to a
transaction``.
- A zope.testing cleanup also ensures that the global transaction manager
is in its default implicit mode, at least for the current thread.
- A zope.testing cleanup also ensures that the global transaction
manager is in its default implicit mode, at least for the
current thread.
.. versionchanged:: 3.1.0
The :mod:`mock` module, or its backwards compatibility backport for
Python 2.7, is now available as an attribute of this module, and as
the module named ``nti.testing.mock``. Thus, for compatibility with
both Python 2 and Python 3, you can write ``from nti.testing import
mock`` or ``from nti.testing.mock import Mock``, or even just
``from nti.testing import Mock``.
.. versionchanged:: 3.1.0
Expose the most commonly used attributes of some submodules as API on this
module itself.
"""

from __future__ import absolute_import
Expand All @@ -26,9 +43,28 @@
import transaction
import zope.testing.cleanup

from . import mock
from .mock import Mock

from .matchers import is_true
from .matchers import is_false
from .matchers import provides
from .matchers import implements
from .matchers import verifiably_provides
from .matchers import validly_provides
from .matchers import validated_by
from .matchers import not_validated_by
from .matchers import aq_inContextOf

from .time import time_monotonically_increases


__docformat__ = "restructuredtext en"

def transactionCleanUp():
"""
Implement the transaction cleanup described in the module documentation.
"""
try:
transaction.abort()
except transaction.interfaces.NoTransaction:
Expand All @@ -43,3 +79,28 @@ def transactionCleanUp():


zope.testing.cleanup.addCleanUp(transactionCleanUp)

__all__ = [
# Things defined here we want to export
# if they do 'from nti.testing import *'
# This also defines what Sphinx documents for this module.
'transactionCleanUp',
'mock',
'Mock',
# API Convenience exports.
# * matchers
'is_true',
'is_false',
'provides',
'implements',
'verifiably_provides',
'validly_provides',
'validated_by',
'not_validated_by',
'aq_inContextOf',
# * time
'time_monotonically_increases',
# Sub-modules that should be imported with
# * imports as well. We generally don't want anything
# imported; it's better to use direct imports.
]
29 changes: 26 additions & 3 deletions src/nti/testing/matchers.py
Expand Up @@ -283,13 +283,15 @@ def not_validated_by(field, invalid=Invalid):
"""
return is_not(validated_by(field, invalid=invalid))

def _aq_inContextOf_NotImplemented(child, parent):
return False

try:
from Acquisition import aq_inContextOf as _aq_inContextOf
except ImportError: # pragma: no cover
# acquisition not installed
def _aq_inContextOf(child, parent):
return False
_aq_inContextOf = _aq_inContextOf_NotImplemented


class AqInContextOf(BaseMatcher):
def __init__(self, parent):
Expand All @@ -302,7 +304,28 @@ def _matches(self, item):
return _aq_inContextOf(item, self.parent) # not wrapped, but maybe __parent__ chain

def describe_to(self, description):
description.append_text('object in context of').append(repr(self.parent))
description.append_text('object in context of ')
description.append_description_of(self.parent)

def describe_mismatch(self, item, mismatch_description):
if _aq_inContextOf is _aq_inContextOf_NotImplemented:
mismatch_description.append_text('Acquisition was not installed.')
return

mismatch_description.append_description_of(item)
mismatch_description.append_text(' was not in the context of ')
mismatch_description.append_description_of(self.parent)
mismatch_description.append_text('; its lineage is ')
lineage = []
while item is not None:
try:
item = item.__parent__
except AttributeError:
item = None
if item is not None:
lineage.append(item)
mismatch_description.append_description_of(lineage)


def aq_inContextOf(parent):
"""
Expand Down

0 comments on commit fba58c0

Please sign in to comment.