nti.testing 4.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nti/__init__.py +0 -0
- nti/testing/__init__.py +107 -0
- nti/testing/base.py +574 -0
- nti/testing/layers/__init__.py +53 -0
- nti/testing/layers/cleanup.py +106 -0
- nti/testing/layers/postgres.py +693 -0
- nti/testing/layers/tests/__init__.py +0 -0
- nti/testing/layers/tests/test_postgres.py +17 -0
- nti/testing/layers/zope.py +122 -0
- nti/testing/matchers.py +370 -0
- nti/testing/mock.py +22 -0
- nti/testing/tests/__init__.py +0 -0
- nti/testing/tests/test_base.py +160 -0
- nti/testing/tests/test_component_cleanup_broken.txt +137 -0
- nti/testing/tests/test_layers.py +56 -0
- nti/testing/tests/test_main.py +47 -0
- nti/testing/tests/test_matchers.py +237 -0
- nti/testing/tests/test_time.py +157 -0
- nti/testing/tests/test_zodb.py +326 -0
- nti/testing/time.py +179 -0
- nti/testing/zodb.py +245 -0
- nti.testing-4.2.0.dist-info/LICENSE +11 -0
- nti.testing-4.2.0.dist-info/METADATA +423 -0
- nti.testing-4.2.0.dist-info/RECORD +27 -0
- nti.testing-4.2.0.dist-info/WHEEL +5 -0
- nti.testing-4.2.0.dist-info/top_level.txt +1 -0
- nti.testing-4.2.0.dist-info/zip-safe +1 -0
nti/testing/zodb.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Support for testing of ZODB applications.
|
|
4
|
+
|
|
5
|
+
.. versionadded:: 3.0.0
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import absolute_import
|
|
9
|
+
from __future__ import division
|
|
10
|
+
from __future__ import print_function
|
|
11
|
+
|
|
12
|
+
import gc
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
import transaction
|
|
17
|
+
from transaction.interfaces import NoTransaction
|
|
18
|
+
|
|
19
|
+
import ZODB
|
|
20
|
+
from ZODB.interfaces import IDatabase
|
|
21
|
+
from ZODB.DemoStorage import DemoStorage
|
|
22
|
+
from zope import component
|
|
23
|
+
from zope.exceptions import print_exception
|
|
24
|
+
from zope.exceptions import format_exception
|
|
25
|
+
|
|
26
|
+
from .layers import ZopeComponentLayer
|
|
27
|
+
|
|
28
|
+
PYPY = hasattr(sys, 'pypy_version_info')
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
'mock_db_trans',
|
|
32
|
+
'ZODBLayer',
|
|
33
|
+
'reset_db_caches',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# The exceptions are not expected to be caught. They indicate errors
|
|
37
|
+
# to fix.
|
|
38
|
+
class _BaseException(Exception):
|
|
39
|
+
default_message = 'Unknown'
|
|
40
|
+
def __init__(self, t, v, tb):
|
|
41
|
+
msg = self.default_message
|
|
42
|
+
if t:
|
|
43
|
+
msg = ''.join(format_exception(t, v, tb))
|
|
44
|
+
Exception.__init__(self, msg)
|
|
45
|
+
|
|
46
|
+
class _TransactionManagerModeChanged(_BaseException):
|
|
47
|
+
default_message = 'The transaction manager explicit mode changed'
|
|
48
|
+
|
|
49
|
+
class _TransactionChanged(_BaseException):
|
|
50
|
+
default_message = 'The body changed the active transaction'
|
|
51
|
+
|
|
52
|
+
class mock_db_trans(object):
|
|
53
|
+
"""
|
|
54
|
+
A context manager that begins and commits a database transaction.
|
|
55
|
+
|
|
56
|
+
This class may be subclassed; there are some extension points
|
|
57
|
+
to allow customization.
|
|
58
|
+
|
|
59
|
+
This class is not designed for concurrency.
|
|
60
|
+
|
|
61
|
+
Entering this context manager begins a transaction using the
|
|
62
|
+
thread-local global transaction manager and returns a connection
|
|
63
|
+
open to the database given in the *db* parameter. Exiting this
|
|
64
|
+
context manager commits the transaction (or aborts it if it is
|
|
65
|
+
doomed).
|
|
66
|
+
|
|
67
|
+
It is an error to enter this context manager with a transaction
|
|
68
|
+
already in progress. The global transaction may be committed or aborted
|
|
69
|
+
before exiting this transaction, but no new transaction may be opened. During
|
|
70
|
+
execution of the body, the transaction manager is in explicit mode;
|
|
71
|
+
it is an error if this is changed during the body.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
#: The connection that was opened. Valid after entering and before exiting.
|
|
75
|
+
conn = None
|
|
76
|
+
|
|
77
|
+
#: The file to which unexpected exceptions that cannot be raised are printed.
|
|
78
|
+
#: If `None`, exceptions will be written to `sys.stderr`
|
|
79
|
+
exc_file = None
|
|
80
|
+
|
|
81
|
+
def __init__(self, db=None):
|
|
82
|
+
"""
|
|
83
|
+
:param db: The :class:`ZODB.DB` to open. If none is given,
|
|
84
|
+
then the :attr:`ZODBLayer.db` will be used.
|
|
85
|
+
"""
|
|
86
|
+
self.db = db if db is not None else ZODBLayer.db
|
|
87
|
+
self.__txm_was_explicit = None
|
|
88
|
+
self.__current_transaction = None
|
|
89
|
+
|
|
90
|
+
def on_connection_opened(self, conn):
|
|
91
|
+
"""
|
|
92
|
+
Called when the connection to the DB has been opened.
|
|
93
|
+
|
|
94
|
+
Subclasses may override to perform initialization.
|
|
95
|
+
The DB may have been used before, so check its state and don't assume
|
|
96
|
+
a complete initialization must happen.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __enter__(self):
|
|
100
|
+
txm = transaction.manager
|
|
101
|
+
self.__txm_was_explicit = txm.explicit
|
|
102
|
+
txm.explicit = True
|
|
103
|
+
try:
|
|
104
|
+
self.__current_transaction = txm.begin()
|
|
105
|
+
self.conn = conn = self.db.open()
|
|
106
|
+
self.on_connection_opened(conn)
|
|
107
|
+
except:
|
|
108
|
+
# Could be several things:
|
|
109
|
+
# Already in a transaction is definitely a user error.
|
|
110
|
+
# Failure to open the DB: Probably also a user error.
|
|
111
|
+
# Failure to setup the DB: Possibly a user error, or error in a
|
|
112
|
+
# subclass.
|
|
113
|
+
txm.explicit = self.__txm_was_explicit
|
|
114
|
+
if self.__current_transaction:
|
|
115
|
+
self._clean_up_one_transaction(self.__current_transaction, True)
|
|
116
|
+
|
|
117
|
+
if self.conn is not None:
|
|
118
|
+
self.conn.close()
|
|
119
|
+
self.conn = None
|
|
120
|
+
self.__current_transaction = None
|
|
121
|
+
raise
|
|
122
|
+
return self.conn
|
|
123
|
+
|
|
124
|
+
def _clean_up_one_transaction(self, tx, abort_only):
|
|
125
|
+
try:
|
|
126
|
+
if abort_only or tx.isDoomed():
|
|
127
|
+
tx.abort()
|
|
128
|
+
else:
|
|
129
|
+
tx.commit()
|
|
130
|
+
except:
|
|
131
|
+
tx.abort() # idempotent with transaction 3
|
|
132
|
+
raise
|
|
133
|
+
|
|
134
|
+
def _report_exception(self, msg, t, v, tb):
|
|
135
|
+
f = self.exc_file or sys.stderr
|
|
136
|
+
print(msg, file=f)
|
|
137
|
+
print_exception(t, v, tb, file=f, with_filenames=False)
|
|
138
|
+
|
|
139
|
+
def _close_connection(self, conn, ignore_errors):
|
|
140
|
+
catch = Exception if ignore_errors else ()
|
|
141
|
+
try:
|
|
142
|
+
conn.close()
|
|
143
|
+
except catch: # pylint:disable=broad-exception-caught
|
|
144
|
+
self._report_exception("Unexpected error closing connection; ignored.", *sys.exc_info())
|
|
145
|
+
|
|
146
|
+
def __exit__(self, t, v, tb):
|
|
147
|
+
# Returning a True value from __exit__ suppresses any
|
|
148
|
+
# exception raised in the body.
|
|
149
|
+
body_raised = t is not None
|
|
150
|
+
abort_only = body_raised # Should we commit our transaction, or only abort it?
|
|
151
|
+
error_in_body = None # Did the body do something wrong?
|
|
152
|
+
# Assuming we're still on the same thread and this hasn't been reset.
|
|
153
|
+
txm = transaction.manager
|
|
154
|
+
tx = self.__current_transaction
|
|
155
|
+
if not txm.explicit:
|
|
156
|
+
abort_only = True
|
|
157
|
+
error_in_body:Exception = _TransactionManagerModeChanged(t, v, tb)
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
if txm.get() is not tx:
|
|
161
|
+
# Remember, .get() could raise.
|
|
162
|
+
self._clean_up_one_transaction(txm.get(), abort_only=True)
|
|
163
|
+
error_in_body = _TransactionChanged(t, v, tb)
|
|
164
|
+
except NoTransaction:
|
|
165
|
+
abort_only = True # It's already gone, so just abort it
|
|
166
|
+
error_in_body = _TransactionChanged(t, v, tb)
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
self._clean_up_one_transaction(tx, abort_only)
|
|
170
|
+
except Exception: # pylint:disable=broad-except
|
|
171
|
+
if not body_raised:
|
|
172
|
+
# The body had no issue, but the commit did. Raise the commit error,
|
|
173
|
+
# but let the finally block know not to shadow it.
|
|
174
|
+
body_raised = True
|
|
175
|
+
raise
|
|
176
|
+
self._report_exception(
|
|
177
|
+
"Failed to cleanup trans, but body raised exception too. "
|
|
178
|
+
"This exception will be ignored.",
|
|
179
|
+
*sys.exc_info())
|
|
180
|
+
# So let the body exception propagate
|
|
181
|
+
finally:
|
|
182
|
+
txm.explicit = self.__txm_was_explicit
|
|
183
|
+
self._close_connection(self.conn, ignore_errors=body_raised)
|
|
184
|
+
self.conn = self.__current_transaction = None
|
|
185
|
+
reset_db_caches(self.db)
|
|
186
|
+
if error_in_body:
|
|
187
|
+
raise error_in_body # pylint:disable=raising-bad-type
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def reset_db_caches(db=None, collect=False):
|
|
192
|
+
"""
|
|
193
|
+
Minimize the caches of all connections found in the *db*.
|
|
194
|
+
|
|
195
|
+
If the *db* is not given, then the one from the :class:`ZODBLayer`
|
|
196
|
+
is used.
|
|
197
|
+
|
|
198
|
+
On PyPy, or if *collect* is true, this will invoke
|
|
199
|
+
:func:`gc.collect` to help remove any weak references to objects
|
|
200
|
+
that were ejected from the cache.
|
|
201
|
+
"""
|
|
202
|
+
result = -1
|
|
203
|
+
if db is None:
|
|
204
|
+
db = ZODBLayer.db
|
|
205
|
+
if db is not None:
|
|
206
|
+
for conn in db.pool:
|
|
207
|
+
conn.cacheMinimize()
|
|
208
|
+
if PYPY or collect:
|
|
209
|
+
result = gc.collect()
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
class ZODBLayer(ZopeComponentLayer):
|
|
213
|
+
"""
|
|
214
|
+
Test layer that creates a ZODB database using
|
|
215
|
+
:class:`ZODB.DemoStorage.DemoStorage` and registers it as the
|
|
216
|
+
no-name :class:`ZODB.interfaces.IDatabase` in the global component
|
|
217
|
+
registry. It is also available in the :attr:`db` attribute of this
|
|
218
|
+
object.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
#: The DB that was created.
|
|
222
|
+
db = None
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
def setUp(cls):
|
|
226
|
+
db = cls.db = ZODB.DB(DemoStorage())
|
|
227
|
+
component.getGlobalSiteManager().registerUtility(db, IDatabase)
|
|
228
|
+
|
|
229
|
+
@classmethod
|
|
230
|
+
def tearDown(cls):
|
|
231
|
+
db = cls.db
|
|
232
|
+
cls.db = None
|
|
233
|
+
if db is not None:
|
|
234
|
+
db.close()
|
|
235
|
+
reg_db = component.getGlobalSiteManager().queryUtility(IDatabase)
|
|
236
|
+
if reg_db is db:
|
|
237
|
+
component.getGlobalSiteManager().unregisterUtility(db, IDatabase)
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def testSetUp(cls):
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
@classmethod
|
|
244
|
+
def testTearDown(cls):
|
|
245
|
+
pass
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
2
|
+
you may not use this software except in compliance with the License.
|
|
3
|
+
You may obtain a copy of the License at
|
|
4
|
+
|
|
5
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
|
|
7
|
+
Unless required by applicable law or agreed to in writing, software
|
|
8
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
9
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
10
|
+
See the License for the specific language governing permissions and
|
|
11
|
+
limitations under the License.
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: nti.testing
|
|
3
|
+
Version: 4.2.0
|
|
4
|
+
Summary: Support for testing code
|
|
5
|
+
Home-page: https://github.com/OpenNTI/nti.testing
|
|
6
|
+
Author: Jason Madden
|
|
7
|
+
Author-email: jason@nextthought.com
|
|
8
|
+
License: Apache
|
|
9
|
+
Project-URL: Documentation, https://ntitesting.readthedocs.io/en/latest/
|
|
10
|
+
Keywords: nose2 testing zope3 ZTK hamcrest
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
23
|
+
Classifier: Topic :: Software Development :: Testing
|
|
24
|
+
Classifier: Framework :: Zope3
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: ZODB>=5.6.0
|
|
28
|
+
Requires-Dist: zope.interface>=5.4.0
|
|
29
|
+
Requires-Dist: pyhamcrest
|
|
30
|
+
Requires-Dist: six
|
|
31
|
+
Requires-Dist: transaction
|
|
32
|
+
Requires-Dist: zope.component
|
|
33
|
+
Requires-Dist: zope.configuration
|
|
34
|
+
Requires-Dist: zope.dottedname
|
|
35
|
+
Requires-Dist: zope.exceptions
|
|
36
|
+
Requires-Dist: zope.schema
|
|
37
|
+
Requires-Dist: zope.testing
|
|
38
|
+
Provides-Extra: docs
|
|
39
|
+
Requires-Dist: Sphinx; extra == "docs"
|
|
40
|
+
Requires-Dist: furo; extra == "docs"
|
|
41
|
+
Provides-Extra: test
|
|
42
|
+
Requires-Dist: Acquisition; extra == "test"
|
|
43
|
+
Requires-Dist: zope.site; extra == "test"
|
|
44
|
+
Requires-Dist: zope.testrunner; extra == "test"
|
|
45
|
+
Requires-Dist: testgres>=1.10; extra == "test"
|
|
46
|
+
Requires-Dist: psycopg2-binary; platform_python_implementation != "PyPy" and extra == "test"
|
|
47
|
+
Provides-Extra: testgres
|
|
48
|
+
Requires-Dist: testgres>=1.10; extra == "testgres"
|
|
49
|
+
Requires-Dist: psycopg2-binary; platform_python_implementation != "PyPy" and extra == "testgres"
|
|
50
|
+
|
|
51
|
+
=============
|
|
52
|
+
nti.testing
|
|
53
|
+
=============
|
|
54
|
+
|
|
55
|
+
.. image:: https://img.shields.io/pypi/v/nti.testing.svg
|
|
56
|
+
:target: https://pypi.python.org/pypi/nti.testing/
|
|
57
|
+
:alt: Latest release
|
|
58
|
+
|
|
59
|
+
.. image:: https://img.shields.io/pypi/pyversions/nti.testing.svg
|
|
60
|
+
:target: https://pypi.org/project/nti.testing/
|
|
61
|
+
:alt: Supported Python versions
|
|
62
|
+
|
|
63
|
+
.. image:: https://github.com/NextThought/nti.testing/actions/workflows/tests.yml/badge.svg
|
|
64
|
+
:target: https://github.com/NextThought/nti.testing/actions/workflows/tests.yml
|
|
65
|
+
:alt: Test Status
|
|
66
|
+
|
|
67
|
+
.. image:: https://coveralls.io/repos/github/NextThought/nti.testing/badge.svg
|
|
68
|
+
:target: https://coveralls.io/github/NextThought/nti.testing
|
|
69
|
+
|
|
70
|
+
.. image:: http://readthedocs.org/projects/ntitesting/badge/?version=latest
|
|
71
|
+
:target: http://ntitesting.readthedocs.io/en/latest/?badge=latest
|
|
72
|
+
:alt: Documentation Status
|
|
73
|
+
|
|
74
|
+
Support for writing tests, particularly in a Zope3/ZTK environment,
|
|
75
|
+
using zope.testing (nose2 may also work, but is not recommended).
|
|
76
|
+
|
|
77
|
+
Complete documentation is hosted at https://ntitesting.readthedocs.io/
|
|
78
|
+
|
|
79
|
+
Installation
|
|
80
|
+
============
|
|
81
|
+
|
|
82
|
+
nti.testing can be installed using pip, either from the git repository
|
|
83
|
+
or from PyPI::
|
|
84
|
+
|
|
85
|
+
pip install nti.testing[testgres]
|
|
86
|
+
|
|
87
|
+
Use the ``testgres`` extra to be able to use `nti.testing.layers.postgres`.
|
|
88
|
+
|
|
89
|
+
PyHamcrest
|
|
90
|
+
==========
|
|
91
|
+
|
|
92
|
+
nti.testing provides a group of `PyHamcrest`_ matchers. There are both
|
|
93
|
+
general-purpose matchers and matchers that are of use to users of
|
|
94
|
+
`zope.interface`_ and `zope.schema`_.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
.. _PyHamcrest: https://pyhamcrest.readthedocs.io/en/latest/
|
|
98
|
+
.. _zope.interface: https://pypi.python.org/pypi/zope.interface
|
|
99
|
+
.. _zope.schema: https://pypi.python.org/pypi/zope.schema
|
|
100
|
+
|
|
101
|
+
.. NOTE: We rely on the Sphinx 'default_role' to turn single back quotes into links,
|
|
102
|
+
while still being compatible with rendering with plain docutils/readme_renderer
|
|
103
|
+
for PyPI.
|
|
104
|
+
|
|
105
|
+
Matchers can be imported from the `nti.testing.matchers` module; the most commonly used matchers
|
|
106
|
+
can be directly imported from `nti.testing`.
|
|
107
|
+
|
|
108
|
+
Basic Matchers
|
|
109
|
+
--------------
|
|
110
|
+
|
|
111
|
+
`is_true` and `is_false` check the `bool` value of a supplied
|
|
112
|
+
object (we're using literals for explanation purposes, but it
|
|
113
|
+
obviously makes more sense, and reads better, when the matched object
|
|
114
|
+
is a variable, often of a more complex type):
|
|
115
|
+
|
|
116
|
+
>>> from hamcrest import assert_that, is_
|
|
117
|
+
>>> from nti.testing import is_true, is_false
|
|
118
|
+
>>> assert_that("Hi", is_true())
|
|
119
|
+
>>> assert_that(0, is_false())
|
|
120
|
+
|
|
121
|
+
Interface Matchers
|
|
122
|
+
------------------
|
|
123
|
+
|
|
124
|
+
Next we come to matchers that support basic use of ``zope.interface``.
|
|
125
|
+
|
|
126
|
+
We can check that an object provides an interface and that a factory
|
|
127
|
+
implements it:
|
|
128
|
+
|
|
129
|
+
>>> from zope.interface import Interface, Attribute, implementer
|
|
130
|
+
>>> class IThing1(Interface):
|
|
131
|
+
... pass
|
|
132
|
+
>>> class IThing2(Interface):
|
|
133
|
+
... pass
|
|
134
|
+
>>> class IThings(IThing1, IThing2):
|
|
135
|
+
... got_that_thing_i_sent_you = Attribute("Did you get that thing?")
|
|
136
|
+
>>> @implementer(IThings)
|
|
137
|
+
... class Thing(object):
|
|
138
|
+
... def __repr__(self): return "<object Thing>"
|
|
139
|
+
|
|
140
|
+
>>> from nti.testing import provides, implements
|
|
141
|
+
>>> assert_that(Thing(), provides(IThings))
|
|
142
|
+
>>> assert_that(Thing, implements(IThings))
|
|
143
|
+
|
|
144
|
+
The attentive reader will have noticed that ``IThings`` defines an
|
|
145
|
+
attribute that our implementation doesn't *actually* provide. This is
|
|
146
|
+
where the next stricter check comes in. `verifiably_provides` uses
|
|
147
|
+
the interface machinery to determine that all attributes and methods
|
|
148
|
+
specified by the interface are present as described:
|
|
149
|
+
|
|
150
|
+
>>> from nti.testing import verifiably_provides
|
|
151
|
+
>>> assert_that(Thing(), verifiably_provides(IThing2, IThing1))
|
|
152
|
+
>>> assert_that(Thing(), verifiably_provides(IThings))
|
|
153
|
+
Traceback (most recent call last):
|
|
154
|
+
...
|
|
155
|
+
AssertionError:...
|
|
156
|
+
Expected: object verifiably providing <...IThings>
|
|
157
|
+
but: Using class <class 'Thing'> the object <object Thing> has failed to implement interface ....IThings: The ....IThings.got_that_thing_i_sent_you attribute was not provided.
|
|
158
|
+
<BLANKLINE>
|
|
159
|
+
|
|
160
|
+
If multiple attributes or methods are not provided, all such missing
|
|
161
|
+
information is reported:
|
|
162
|
+
|
|
163
|
+
>>> class IThingReceiver(IThings):
|
|
164
|
+
... def receive(some_thing):
|
|
165
|
+
... """Get the thing"""
|
|
166
|
+
>>> @implementer(IThingReceiver)
|
|
167
|
+
... class ThingReceiver(object):
|
|
168
|
+
... def __repr__(self): return "<object ThingReceiver>"
|
|
169
|
+
>>> assert_that(ThingReceiver(), verifiably_provides(IThingReceiver))
|
|
170
|
+
Traceback (most recent call last):
|
|
171
|
+
...
|
|
172
|
+
AssertionError:...
|
|
173
|
+
Expected: object verifiably providing <...IThingReceiver>
|
|
174
|
+
but: Using class <class 'ThingReceiver'> the object <object ThingReceiver> has failed to implement interface ....IThingReceiver:
|
|
175
|
+
The ....IThings.got_that_thing_i_sent_you attribute was not provided
|
|
176
|
+
The ....IThingReceiver.receive(some_thing) attribute was not provided
|
|
177
|
+
<BLANKLINE>
|
|
178
|
+
|
|
179
|
+
``zope.interface`` can only check whether or not an attribute or
|
|
180
|
+
method is present. To place (arbitrary) tighter constraints on the
|
|
181
|
+
values of the attributes, we can step up to ``zope.schema`` and the
|
|
182
|
+
`validly_provides` matcher:
|
|
183
|
+
|
|
184
|
+
>>> from zope.schema import Bool
|
|
185
|
+
>>> class IBoolThings(IThing1, IThing2):
|
|
186
|
+
... got_that_thing_i_sent_you = Bool(required=True)
|
|
187
|
+
>>> @implementer(IBoolThings)
|
|
188
|
+
... class BoolThing(object):
|
|
189
|
+
... def __repr__(self): return "<object BoolThing>"
|
|
190
|
+
|
|
191
|
+
`validly_provides` is a superset of `verifiably_provides`:
|
|
192
|
+
|
|
193
|
+
>>> from nti.testing import validly_provides
|
|
194
|
+
>>> assert_that(BoolThing(), validly_provides(IThing1, IThing2))
|
|
195
|
+
>>> assert_that(BoolThing(), validly_provides(IBoolThings))
|
|
196
|
+
Traceback (most recent call last):
|
|
197
|
+
...
|
|
198
|
+
AssertionError:...
|
|
199
|
+
Expected: (object verifiably providing <...IBoolThings> and object validly providing ....IBoolThings)
|
|
200
|
+
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.
|
|
201
|
+
<BLANKLINE>
|
|
202
|
+
|
|
203
|
+
For finer grained control, we can compare data against schema fields
|
|
204
|
+
using `validated_by` and `not_validated_by`:
|
|
205
|
+
|
|
206
|
+
>>> from nti.testing import validated_by, not_validated_by
|
|
207
|
+
>>> field = IBoolThings.get('got_that_thing_i_sent_you')
|
|
208
|
+
>>> assert_that(True, is_(validated_by(field)))
|
|
209
|
+
>>> assert_that(None, is_(not_validated_by(field)))
|
|
210
|
+
|
|
211
|
+
Parent/Child Relationships
|
|
212
|
+
--------------------------
|
|
213
|
+
|
|
214
|
+
The `aq_inContextOf` matcher uses the concepts from `Acquisition` to
|
|
215
|
+
check parent/child relationships:
|
|
216
|
+
|
|
217
|
+
>>> from nti.testing import aq_inContextOf
|
|
218
|
+
>>> class Parent(object):
|
|
219
|
+
... pass
|
|
220
|
+
>>> class Child(object):
|
|
221
|
+
... __parent__ = None
|
|
222
|
+
>>> parent = Parent()
|
|
223
|
+
>>> child = Child()
|
|
224
|
+
>>> child.__parent__ = parent
|
|
225
|
+
|
|
226
|
+
>>> assert_that(child, aq_inContextOf(parent))
|
|
227
|
+
|
|
228
|
+
Test Fixtures
|
|
229
|
+
=============
|
|
230
|
+
|
|
231
|
+
Support for test fixtures can be found in `nti.testing.base` and
|
|
232
|
+
`nti.testing.layers`. The ``base`` package includes fully-fleshed
|
|
233
|
+
out base classes for direct use, while the ``layers`` package mostly includes
|
|
234
|
+
mixins that can be used to construct your own test layers.
|
|
235
|
+
|
|
236
|
+
The ``base`` package makes a distinction between "normal" and "shared"
|
|
237
|
+
fixtures. Normal fixtures are those that are used for a single test
|
|
238
|
+
case. They are established via ``setUp`` and torn down via
|
|
239
|
+
``tearDown``.
|
|
240
|
+
|
|
241
|
+
In contrast, shared fixtures are expected to endure for the duration
|
|
242
|
+
of all the tests in the class or all the tests in the layer. These are
|
|
243
|
+
best used when the fixture is expensive to create. Anything that
|
|
244
|
+
extends from `nti.testing.base.AbstractSharedTestBase` creates a shared fixture.
|
|
245
|
+
Through the magic of metaclasses, such a subclass can also be assigned
|
|
246
|
+
as the ``layer`` property of another class to be used as a test layer
|
|
247
|
+
that can be shared across more than one class.
|
|
248
|
+
|
|
249
|
+
The most important bases are `nti.testing.base.ConfiguringTestBase` and
|
|
250
|
+
`nti.testing.base.SharedConfiguringTestBase`. These are both fixtures for
|
|
251
|
+
configuring ZCML, either from existing packages or complete file
|
|
252
|
+
paths. To use these, subclass them and define class attributes
|
|
253
|
+
``set_up_packages`` and (if necessary) ``features``:
|
|
254
|
+
|
|
255
|
+
>>> from nti.testing.base import ConfiguringTestBase
|
|
256
|
+
>>> import zope.security
|
|
257
|
+
>>> class MyConfiguringTest(ConfiguringTestBase):
|
|
258
|
+
... set_up_packages = (
|
|
259
|
+
... 'zope.component', # the default configuration by name
|
|
260
|
+
... # a named file in a named package
|
|
261
|
+
... ('ftesting.zcml', 'zope.traversing.tests'),
|
|
262
|
+
... # an imported module
|
|
263
|
+
... zope.security,
|
|
264
|
+
... # Our own package; in a test, this will mean the parent
|
|
265
|
+
... # package
|
|
266
|
+
... ".")
|
|
267
|
+
|
|
268
|
+
We would then proceed to write our test methods. The packages that we
|
|
269
|
+
specified will be set up and torn down around every test method. In
|
|
270
|
+
addition, the ``zope.testing`` cleanup functions will also run around
|
|
271
|
+
every test method.
|
|
272
|
+
|
|
273
|
+
Time
|
|
274
|
+
====
|
|
275
|
+
|
|
276
|
+
Having a clock that's guaranteed to move in a positive increasing way
|
|
277
|
+
in every call to ``time.time`` is useful. `nti.testing.time`
|
|
278
|
+
provides a decorator to accomplish this that ensures values always are
|
|
279
|
+
at least the current time and always are increasing. (It is not thread
|
|
280
|
+
safe.) It can be applied to functions or methods, and optionally takes
|
|
281
|
+
a ``granularity`` argument:
|
|
282
|
+
|
|
283
|
+
>>> from nti.testing import time_monotonically_increases
|
|
284
|
+
>>> from nti.testing.time import reset_monotonic_time
|
|
285
|
+
>>> @time_monotonically_increases(0.1) # increment by 0.1
|
|
286
|
+
... def test():
|
|
287
|
+
... import time
|
|
288
|
+
... t1 = time.time()
|
|
289
|
+
... t2 = time.time()
|
|
290
|
+
... assert t2 == t1 + 0.1, (t2, t1)
|
|
291
|
+
|
|
292
|
+
>>> test()
|
|
293
|
+
|
|
294
|
+
And The Rest
|
|
295
|
+
============
|
|
296
|
+
|
|
297
|
+
There are some other assorted utilities, including support for working with
|
|
298
|
+
ZODB in `nti.testing.zodb`. See the API documentation for details.
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
=========
|
|
302
|
+
Changes
|
|
303
|
+
=========
|
|
304
|
+
|
|
305
|
+
4.2.0 (2024-11-06)
|
|
306
|
+
==================
|
|
307
|
+
|
|
308
|
+
- Add support for Python 3.13.
|
|
309
|
+
- Use native namespace packages.
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
4.1.0 (2024-04-10)
|
|
313
|
+
==================
|
|
314
|
+
|
|
315
|
+
- Add support for, and require, testgres 1.10. This is needed because
|
|
316
|
+
they changed the signature for ``get_pg_version``.
|
|
317
|
+
- Drop support for Python 3.8 and 3.9.
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
4.0.0 (2023-10-24)
|
|
321
|
+
==================
|
|
322
|
+
|
|
323
|
+
- Add support for Python 3.10, 3.11 and 3.12.
|
|
324
|
+
- Drop support for Python 2 and Python 3.6 and 3.7.
|
|
325
|
+
- Add a layer for working with a ``testgres`` Postgres instance.
|
|
326
|
+
- Add methods to the test base classes to support ``unittest.mock`` patches.
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
3.1.0 (2021-09-08)
|
|
330
|
+
==================
|
|
331
|
+
|
|
332
|
+
- Add support for Python 3.9.
|
|
333
|
+
|
|
334
|
+
- Drop support for Python 3.5.
|
|
335
|
+
|
|
336
|
+
- Add the module alias ``nti.testing.mock``, which is either the
|
|
337
|
+
standard library ``unittest.mock``, or the backport ``mock``. This
|
|
338
|
+
allows easy imports when backwards compatibility matters.
|
|
339
|
+
|
|
340
|
+
- Make ``mock``, ``mock.Mock`` and various other API attributes,
|
|
341
|
+
like ``is_true``, available directly from the ``nti.testing`` namespace.
|
|
342
|
+
|
|
343
|
+
3.0.0 (2020-06-16)
|
|
344
|
+
==================
|
|
345
|
+
|
|
346
|
+
- Add support for Python 3.8.
|
|
347
|
+
|
|
348
|
+
- Require zope.interface 5.1. This lets the interface matchers produce
|
|
349
|
+
much more informative error messages.
|
|
350
|
+
|
|
351
|
+
- Add ``nti.testing.zodb`` with helpers for dealing with ZODB. This
|
|
352
|
+
makes ZODB 5.6 or above a required dependency.
|
|
353
|
+
|
|
354
|
+
2.2.1 (2019-09-10)
|
|
355
|
+
==================
|
|
356
|
+
|
|
357
|
+
- Make transaction cleanup safer if the default transaction manager
|
|
358
|
+
has been made explicit.
|
|
359
|
+
|
|
360
|
+
Also, reset the default transaction manager to implicit.
|
|
361
|
+
|
|
362
|
+
See `issue 17 <https://github.com/NextThought/nti.testing/issues/17>`_.
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
2.2.0 (2018-08-24)
|
|
366
|
+
==================
|
|
367
|
+
|
|
368
|
+
- Add support for Python 3.7.
|
|
369
|
+
|
|
370
|
+
- Make ``time_monotonically_increases`` also handle ``time.gmtime``
|
|
371
|
+
and add a helper for using it in layers.
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
2.1.0 (2017-10-23)
|
|
375
|
+
==================
|
|
376
|
+
|
|
377
|
+
- Make ``Acquisition`` an optional dependency. If it is not installed,
|
|
378
|
+
the ``aq_inContextOf`` matcher will always return False.
|
|
379
|
+
|
|
380
|
+
- Remove dependency on ``fudge``. Instead, we now use ``unittest.mock`` on
|
|
381
|
+
Python 3, or its backport ``mock`` on Python 2. See `issue 11
|
|
382
|
+
<https://github.com/NextThought/nti.testing/issues/11>`_.
|
|
383
|
+
|
|
384
|
+
- Refactor ZCML configuration support to share more code and
|
|
385
|
+
documentation. See `issue 10
|
|
386
|
+
<https://github.com/NextThought/nti.testing/issues/10>`_.
|
|
387
|
+
|
|
388
|
+
- The layer ``ConfiguringLayerMixin`` and the base class
|
|
389
|
+
``SharedConfiguringTestBase`` now default to running
|
|
390
|
+
configuration in the package the subclass is defined in, just as
|
|
391
|
+
subclasses of ``ConfiguringTestBase`` did.
|
|
392
|
+
|
|
393
|
+
2.0.1 (2017-10-18)
|
|
394
|
+
==================
|
|
395
|
+
|
|
396
|
+
- The validation matchers (``validated_by`` and ``not_validated_by``)
|
|
397
|
+
now consider it a failure (by default) if the validate method raises
|
|
398
|
+
anything other than ``zope.interface.exceptions.Invalid`` (which
|
|
399
|
+
includes the ``zope.schema`` exceptions like ``WrongType``).
|
|
400
|
+
Previously, they accepted any exception as meaning the object was
|
|
401
|
+
invalid, but this could hide bugs in the actual validation method
|
|
402
|
+
itself. You can supply the ``invalid`` argument to the matchers to
|
|
403
|
+
loosen or tighten this as desired. (Giving ``invalid=Exception``
|
|
404
|
+
will restore the old behaviour.)
|
|
405
|
+
See `issue 7 <https://github.com/NextThought/nti.testing/issues/7>`_.
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
2.0.0 (2017-04-12)
|
|
409
|
+
==================
|
|
410
|
+
|
|
411
|
+
- Add support for Python 3.6.
|
|
412
|
+
|
|
413
|
+
- Remove ``unicode_literals``.
|
|
414
|
+
|
|
415
|
+
- Substantially rework ``time_monotonically_increases`` for greater
|
|
416
|
+
safety. Fixes `issue 5 <https://github.com/NextThought/nti.testing/issues/5>`_.
|
|
417
|
+
|
|
418
|
+
1.0.0 (2016-07-28)
|
|
419
|
+
==================
|
|
420
|
+
|
|
421
|
+
- Add Python 3 support.
|
|
422
|
+
|
|
423
|
+
- Initial PyPI release.
|