Extensions to the Python standard library unit testing framework
Utility functions for common testing tasks including safe imports, dictionary manipulation, monkey patching, and test organization helpers.
Functions for safely importing modules with fallback handling.
def try_import(name, alternative=None, error_callback=None):
"""
Attempt to import a module, with a fallback.
Safely attempts to import a module or attribute, returning
an alternative value if the import fails. Useful for optional
dependencies and cross-version compatibility.
Args:
name (str): The name of the object to import (e.g., 'os.path.join')
alternative: The value to return if import fails (default: None)
error_callback (callable): Function called with ImportError if import fails
Returns:
The imported object or the alternative value
Example:
# Try to import optional dependency
numpy = try_import('numpy', alternative=None)
if numpy is not None:
# Use numpy functionality
pass
# Import specific function with fallback
json_loads = try_import('orjson.loads', json.loads)
"""Utility functions for common dictionary operations in testing contexts.
def map_values(function, dictionary):
"""
Map function across the values of a dictionary.
Applies a function to each value in a dictionary,
preserving the key-value structure.
Args:
function (callable): Function to apply to each value
dictionary (dict): Dictionary to transform
Returns:
dict: New dictionary with transformed values
Example:
data = {'a': 1, 'b': 2, 'c': 3}
doubled = map_values(lambda x: x * 2, data)
# {'a': 2, 'b': 4, 'c': 6}
"""
def filter_values(function, dictionary):
"""
Filter dictionary by its values using a predicate function.
Returns a new dictionary containing only the key-value pairs
where the value satisfies the predicate function.
Args:
function (callable): Predicate function returning bool
dictionary (dict): Dictionary to filter
Returns:
dict: Filtered dictionary
Example:
data = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
evens = filter_values(lambda x: x % 2 == 0, data)
# {'b': 2, 'd': 4}
"""
def dict_subtract(a, b):
"""
Return the part of dictionary a that's not in dictionary b.
Creates a new dictionary containing key-value pairs from 'a'
whose keys are not present in 'b'.
Args:
a (dict): Source dictionary
b (dict): Dictionary whose keys to exclude
Returns:
dict: Dictionary with keys from a not in b
Example:
a = {'x': 1, 'y': 2, 'z': 3}
b = {'y': 5, 'w': 6}
result = dict_subtract(a, b)
# {'x': 1, 'z': 3}
"""
def list_subtract(a, b):
"""
Return a list with elements of a not in b.
Creates a new list containing elements from 'a' that are
not present in 'b'. If an element appears multiple times
in 'a' and once in 'b', it will appear n-1 times in result.
Args:
a (list): Source list
b (list): List of elements to remove
Returns:
list: List with elements from a not in b
Example:
a = [1, 2, 3, 2, 4]
b = [2, 5]
result = list_subtract(a, b)
# [1, 3, 2, 4] (one instance of 2 removed)
"""Functions for working with test cases and test organization.
def clone_test_with_new_id(test, new_id):
"""
Clone a test with a new test ID.
Creates a copy of a test case with a different identifier,
useful for parameterized testing or test case variations.
Args:
test: Original TestCase instance
new_id (str): New test identifier
Returns:
TestCase: Cloned test with new ID
Example:
original_test = MyTest('test_function')
variant_test = clone_test_with_new_id(original_test, 'test_function_variant')
"""
def iterate_tests(test_suite_or_case):
"""
Iterate through all individual tests in a test suite.
Recursively flattens test suites to yield individual
test cases, useful for test discovery and analysis.
Args:
test_suite_or_case: TestSuite or TestCase to iterate
Yields:
TestCase: Individual test cases
Example:
suite = TestSuite()
# ... add tests to suite ...
for test in iterate_tests(suite):
print(f"Found test: {test.id()}")
"""
def unique_text_generator():
"""
Generate unique text strings for test isolation.
Creates a generator that yields unique text strings,
useful for generating unique identifiers in tests.
Yields:
str: Unique text strings
Example:
generator = unique_text_generator()
unique1 = next(generator) # "0"
unique2 = next(generator) # "1"
unique3 = next(generator) # "2"
"""Classes and functions for runtime patching during tests.
class MonkeyPatcher:
"""
Apply and remove multiple patches as a coordinated group.
Manages multiple monkey patches with automatic cleanup,
ensuring all patches are properly restored after use.
"""
def __init__(self):
"""Create a new MonkeyPatcher instance."""
def add_patch(self, obj, attribute, new_value):
"""
Add a patch to be applied.
Args:
obj: Object to patch
attribute (str): Attribute name to patch
new_value: New value for the attribute
"""
def patch(self):
"""
Apply all registered patches.
Applies all patches that have been registered with add_patch().
Should be called before the code that needs the patches.
"""
def restore(self):
"""
Restore all patched attributes to their original values.
Undoes all patches applied by patch(), restoring the
original attribute values.
"""
def __enter__(self):
"""
Context manager entry - applies patches.
Returns:
MonkeyPatcher: Self for context manager protocol
"""
self.patch()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""
Context manager exit - restores patches.
Args:
exc_type: Exception type (if any)
exc_val: Exception value (if any)
exc_tb: Exception traceback (if any)
"""
self.restore()
def patch(obj, attribute, new_value):
"""
Apply a simple monkey patch.
Convenience function for applying a single patch.
For multiple patches, use MonkeyPatcher class.
Args:
obj: Object to patch
attribute (str): Attribute name to patch
new_value: New value for the attribute
Returns:
The original value that was replaced
Example:
# Patch a function temporarily
original = patch(os, 'getcwd', lambda: '/fake/path')
try:
# Code that uses os.getcwd()
pass
finally:
# Restore original
setattr(os, 'getcwd', original)
"""Assertion functions that can be used outside of TestCase.
def assert_that(matchee, matcher, message='', verbose=False):
"""
Assert that matchee matches the given matcher.
Standalone assertion function using testtools matchers,
can be used outside of TestCase instances for utility
functions and general-purpose assertions.
Args:
matchee: Object to be matched
matcher: Matcher instance to apply
message (str): Optional failure message
verbose (bool): Include detailed mismatch information
Raises:
MismatchError: If matcher does not match matchee
Example:
from testtools.assertions import assert_that
from testtools.matchers import Equals, GreaterThan
# Use in utility functions
def validate_config(config):
assert_that(config['timeout'], GreaterThan(0))
assert_that(config['host'], Contains('.'))
"""Functions for cross-Python version compatibility.
def reraise(exc_type, exc_value, traceback):
"""
Re-raise an exception with its original traceback.
Properly re-raises exceptions preserving the original
traceback information across Python versions.
Args:
exc_type: Exception type
exc_value: Exception instance
traceback: Exception traceback
"""
def text_repr(obj):
"""
Get text representation with proper escaping.
Returns a properly escaped text representation of an object,
handling unicode and special characters correctly.
Args:
obj: Object to represent
Returns:
str: Escaped text representation
"""
def unicode_output_stream(stream):
"""
Get unicode-capable output stream.
Wraps a stream to ensure it can handle unicode output
properly across different Python versions and platforms.
Args:
stream: Original output stream
Returns:
Stream: Unicode-capable stream wrapper
"""import testtools
from testtools.helpers import try_import
class MyTest(testtools.TestCase):
def setUp(self):
super().setUp()
# Try to import optional dependencies
self.numpy = try_import('numpy')
self.pandas = try_import('pandas')
self.requests = try_import('requests', error_callback=self._log_import_error)
def _log_import_error(self, error):
"""Log import errors for debugging."""
self.addDetail('import_error',
testtools.content.text_content(str(error)))
def test_with_optional_numpy(self):
if self.numpy is None:
self.skip("NumPy not available")
# Use numpy functionality
arr = self.numpy.array([1, 2, 3])
self.assertEqual(len(arr), 3)
def test_with_fallback_json(self):
# Use fast JSON library if available, fall back to standard
json_loads = try_import('orjson.loads', alternative=json.loads)
json_dumps = try_import('orjson.dumps', alternative=json.dumps)
data = {'test': True, 'value': 42}
serialized = json_dumps(data)
deserialized = json_loads(serialized)
self.assertEqual(deserialized, data)import testtools
from testtools.helpers import map_values, filter_values, dict_subtract
class DataProcessingTest(testtools.TestCase):
def test_data_transformation(self):
# Original data
raw_data = {
'temperature': 20,
'humidity': 65,
'pressure': 1013,
'wind_speed': 15
}
# Transform all values (Celsius to Fahrenheit)
fahrenheit_data = map_values(lambda c: c * 9/5 + 32,
{'temperature': raw_data['temperature']})
self.assertEqual(fahrenheit_data['temperature'], 68.0)
# Filter for specific conditions
high_values = filter_values(lambda x: x > 50, raw_data)
self.assertIn('humidity', high_values)
self.assertIn('pressure', high_values)
self.assertNotIn('temperature', high_values)
def test_configuration_merge(self):
# Base configuration
base_config = {
'host': 'localhost',
'port': 8080,
'debug': False,
'timeout': 30
}
# Environment-specific overrides
production_overrides = {
'host': 'prod.example.com',
'debug': False,
'ssl': True
}
# Get settings that are only in base config
base_only = dict_subtract(base_config, production_overrides)
self.assertEqual(base_only, {'port': 8080, 'timeout': 30})
# Merge configurations
final_config = {**base_config, **production_overrides}
self.assertEqual(final_config['host'], 'prod.example.com')
self.assertTrue(final_config['ssl'])import testtools
from testtools.helpers import MonkeyPatcher
import os
import time
class MonkeyPatchTest(testtools.TestCase):
def test_with_context_manager(self):
# Use MonkeyPatcher as context manager
with MonkeyPatcher() as patcher:
patcher.add_patch(os, 'getcwd', lambda: '/fake/working/dir')
patcher.add_patch(time, 'time', lambda: 1234567890.0)
# Test code that uses patched functions
current_dir = os.getcwd()
current_time = time.time()
self.assertEqual(current_dir, '/fake/working/dir')
self.assertEqual(current_time, 1234567890.0)
# Outside context, original functions are restored
self.assertNotEqual(os.getcwd(), '/fake/working/dir')
def test_manual_patching(self):
patcher = MonkeyPatcher()
# Mock file system operations
patcher.add_patch(os.path, 'exists', lambda path: path == '/fake/file')
patcher.add_patch(os.path, 'isfile', lambda path: path.endswith('.txt'))
try:
patcher.patch()
# Test with mocked file system
self.assertTrue(os.path.exists('/fake/file'))
self.assertFalse(os.path.exists('/other/file'))
self.assertTrue(os.path.isfile('document.txt'))
self.assertFalse(os.path.isfile('directory'))
finally:
patcher.restore()
def test_method_patching(self):
# Patch methods on test objects
class MockService:
def get_data(self):
return "original data"
service = MockService()
patcher = MonkeyPatcher()
patcher.add_patch(service, 'get_data', lambda: "mocked data")
with patcher:
result = service.get_data()
self.assertEqual(result, "mocked data")
# Method restored after context
result = service.get_data()
self.assertEqual(result, "original data")import testtools
from testtools.helpers import iterate_tests, clone_test_with_new_id
class TestOrganizationExample(testtools.TestCase):
def test_suite_analysis(self):
# Create a test suite
suite = testtools.TestSuite()
suite.addTest(TestOrganizationExample('test_method_1'))
suite.addTest(TestOrganizationExample('test_method_2'))
# Analyze all tests in suite
test_count = 0
test_names = []
for test in iterate_tests(suite):
test_count += 1
test_names.append(test.id())
self.assertEqual(test_count, 2)
self.assertIn('TestOrganizationExample.test_method_1', test_names)
def test_method_1(self):
self.assertTrue(True)
def test_method_2(self):
self.assertTrue(True)
def create_parameterized_tests():
"""Create multiple test variants from a base test."""
base_test = TestOrganizationExample('test_method_1')
# Create variants with different parameters
variants = []
for i, param in enumerate(['param1', 'param2', 'param3']):
variant = clone_test_with_new_id(base_test, f'test_method_1_variant_{i}')
variant.test_param = param # Add parameter to test
variants.append(variant)
return variants
class TestWithParameters(testtools.TestCase):
def test_parameter_injection(self):
# Use parameter if available
param = getattr(self, 'test_param', 'default')
self.assertIsNotNone(param)
# Test behavior varies by parameter
if param == 'param1':
self.assertEqual(param, 'param1')
else:
self.assertNotEqual(param, 'param1')from testtools.assertions import assert_that
from testtools.matchers import Equals, GreaterThan, Contains, MatchesDict
def validate_user_data(user_data):
"""Validate user data using testtools assertions."""
# Use standalone assertions for validation
assert_that(user_data, MatchesDict({
'id': GreaterThan(0),
'name': Contains('@'), # Assuming email format
'age': GreaterThan(0)
}))
def process_configuration(config_file):
"""Process configuration with validation."""
config = load_config(config_file)
# Validate required fields
assert_that(config.get('database_url'), Contains('://'))
assert_that(config.get('port'), GreaterThan(1000))
assert_that(config.get('debug'), Equals(False))
return config
# Usage in non-test code
try:
user = {'id': 123, 'name': 'user@example.com', 'age': 25}
validate_user_data(user)
print("User data is valid")
except MismatchError as e:
print(f"Validation failed: {e}")import testtools
from testtools.helpers import *
from testtools.matchers import *
class IntegratedUtilityTest(testtools.TestCase):
def setUp(self):
super().setUp()
# Set up test environment with utilities
self.patcher = MonkeyPatcher()
self.unique_id_gen = unique_text_generator()
# Mock external dependencies
self.requests = try_import('requests')
if self.requests:
self.patcher.add_patch(self.requests, 'get', self._mock_http_get)
self.patcher.patch()
def tearDown(self):
if hasattr(self, 'patcher'):
self.patcher.restore()
super().tearDown()
def _mock_http_get(self, url):
"""Mock HTTP GET responses."""
class MockResponse:
def __init__(self, url):
self.url = url
self.status_code = 200
self.text = f"Response from {url}"
def json(self):
return {"url": self.url, "status": "ok"}
return MockResponse(url)
def test_integrated_workflow(self):
# Generate unique test data
unique_name = next(self.unique_id_gen)
test_data = {
'name': f'test_user_{unique_name}',
'email': f'user_{unique_name}@example.com'
}
# Process data with filtering
valid_data = filter_values(lambda v: '@' in v, test_data)
self.assertIn('email', valid_data)
# Transform data
formatted_data = map_values(str.upper,
{'name': test_data['name']})
# Make HTTP request (mocked)
if self.requests:
response = self.requests.get('http://api.example.com/users')
self.assertEqual(response.status_code, 200)
# Validate response with standalone assertion
assert_that(response.json(), MatchesDict({
'url': Contains('api.example.com'),
'status': Equals('ok')
}))Install with Tessl CLI
npx tessl i tessl/pypi-testtools