0
# Helper Utilities
1
2
Utility functions for common testing tasks including safe imports, dictionary manipulation, monkey patching, and test organization helpers.
3
4
## Capabilities
5
6
### Safe Import Utilities
7
8
Functions for safely importing modules with fallback handling.
9
10
```python { .api }
11
def try_import(name, alternative=None, error_callback=None):
12
"""
13
Attempt to import a module, with a fallback.
14
15
Safely attempts to import a module or attribute, returning
16
an alternative value if the import fails. Useful for optional
17
dependencies and cross-version compatibility.
18
19
Args:
20
name (str): The name of the object to import (e.g., 'os.path.join')
21
alternative: The value to return if import fails (default: None)
22
error_callback (callable): Function called with ImportError if import fails
23
24
Returns:
25
The imported object or the alternative value
26
27
Example:
28
# Try to import optional dependency
29
numpy = try_import('numpy', alternative=None)
30
if numpy is not None:
31
# Use numpy functionality
32
pass
33
34
# Import specific function with fallback
35
json_loads = try_import('orjson.loads', json.loads)
36
"""
37
```
38
39
### Dictionary Manipulation
40
41
Utility functions for common dictionary operations in testing contexts.
42
43
```python { .api }
44
def map_values(function, dictionary):
45
"""
46
Map function across the values of a dictionary.
47
48
Applies a function to each value in a dictionary,
49
preserving the key-value structure.
50
51
Args:
52
function (callable): Function to apply to each value
53
dictionary (dict): Dictionary to transform
54
55
Returns:
56
dict: New dictionary with transformed values
57
58
Example:
59
data = {'a': 1, 'b': 2, 'c': 3}
60
doubled = map_values(lambda x: x * 2, data)
61
# {'a': 2, 'b': 4, 'c': 6}
62
"""
63
64
def filter_values(function, dictionary):
65
"""
66
Filter dictionary by its values using a predicate function.
67
68
Returns a new dictionary containing only the key-value pairs
69
where the value satisfies the predicate function.
70
71
Args:
72
function (callable): Predicate function returning bool
73
dictionary (dict): Dictionary to filter
74
75
Returns:
76
dict: Filtered dictionary
77
78
Example:
79
data = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
80
evens = filter_values(lambda x: x % 2 == 0, data)
81
# {'b': 2, 'd': 4}
82
"""
83
84
def dict_subtract(a, b):
85
"""
86
Return the part of dictionary a that's not in dictionary b.
87
88
Creates a new dictionary containing key-value pairs from 'a'
89
whose keys are not present in 'b'.
90
91
Args:
92
a (dict): Source dictionary
93
b (dict): Dictionary whose keys to exclude
94
95
Returns:
96
dict: Dictionary with keys from a not in b
97
98
Example:
99
a = {'x': 1, 'y': 2, 'z': 3}
100
b = {'y': 5, 'w': 6}
101
result = dict_subtract(a, b)
102
# {'x': 1, 'z': 3}
103
"""
104
105
def list_subtract(a, b):
106
"""
107
Return a list with elements of a not in b.
108
109
Creates a new list containing elements from 'a' that are
110
not present in 'b'. If an element appears multiple times
111
in 'a' and once in 'b', it will appear n-1 times in result.
112
113
Args:
114
a (list): Source list
115
b (list): List of elements to remove
116
117
Returns:
118
list: List with elements from a not in b
119
120
Example:
121
a = [1, 2, 3, 2, 4]
122
b = [2, 5]
123
result = list_subtract(a, b)
124
# [1, 3, 2, 4] (one instance of 2 removed)
125
"""
126
```
127
128
### Test Organization Utilities
129
130
Functions for working with test cases and test organization.
131
132
```python { .api }
133
def clone_test_with_new_id(test, new_id):
134
"""
135
Clone a test with a new test ID.
136
137
Creates a copy of a test case with a different identifier,
138
useful for parameterized testing or test case variations.
139
140
Args:
141
test: Original TestCase instance
142
new_id (str): New test identifier
143
144
Returns:
145
TestCase: Cloned test with new ID
146
147
Example:
148
original_test = MyTest('test_function')
149
variant_test = clone_test_with_new_id(original_test, 'test_function_variant')
150
"""
151
152
def iterate_tests(test_suite_or_case):
153
"""
154
Iterate through all individual tests in a test suite.
155
156
Recursively flattens test suites to yield individual
157
test cases, useful for test discovery and analysis.
158
159
Args:
160
test_suite_or_case: TestSuite or TestCase to iterate
161
162
Yields:
163
TestCase: Individual test cases
164
165
Example:
166
suite = TestSuite()
167
# ... add tests to suite ...
168
for test in iterate_tests(suite):
169
print(f"Found test: {test.id()}")
170
"""
171
172
def unique_text_generator():
173
"""
174
Generate unique text strings for test isolation.
175
176
Creates a generator that yields unique text strings,
177
useful for generating unique identifiers in tests.
178
179
Yields:
180
str: Unique text strings
181
182
Example:
183
generator = unique_text_generator()
184
unique1 = next(generator) # "0"
185
unique2 = next(generator) # "1"
186
unique3 = next(generator) # "2"
187
"""
188
```
189
190
### Monkey Patching
191
192
Classes and functions for runtime patching during tests.
193
194
```python { .api }
195
class MonkeyPatcher:
196
"""
197
Apply and remove multiple patches as a coordinated group.
198
199
Manages multiple monkey patches with automatic cleanup,
200
ensuring all patches are properly restored after use.
201
"""
202
203
def __init__(self):
204
"""Create a new MonkeyPatcher instance."""
205
206
def add_patch(self, obj, attribute, new_value):
207
"""
208
Add a patch to be applied.
209
210
Args:
211
obj: Object to patch
212
attribute (str): Attribute name to patch
213
new_value: New value for the attribute
214
"""
215
216
def patch(self):
217
"""
218
Apply all registered patches.
219
220
Applies all patches that have been registered with add_patch().
221
Should be called before the code that needs the patches.
222
"""
223
224
def restore(self):
225
"""
226
Restore all patched attributes to their original values.
227
228
Undoes all patches applied by patch(), restoring the
229
original attribute values.
230
"""
231
232
def __enter__(self):
233
"""
234
Context manager entry - applies patches.
235
236
Returns:
237
MonkeyPatcher: Self for context manager protocol
238
"""
239
self.patch()
240
return self
241
242
def __exit__(self, exc_type, exc_val, exc_tb):
243
"""
244
Context manager exit - restores patches.
245
246
Args:
247
exc_type: Exception type (if any)
248
exc_val: Exception value (if any)
249
exc_tb: Exception traceback (if any)
250
"""
251
self.restore()
252
253
def patch(obj, attribute, new_value):
254
"""
255
Apply a simple monkey patch.
256
257
Convenience function for applying a single patch.
258
For multiple patches, use MonkeyPatcher class.
259
260
Args:
261
obj: Object to patch
262
attribute (str): Attribute name to patch
263
new_value: New value for the attribute
264
265
Returns:
266
The original value that was replaced
267
268
Example:
269
# Patch a function temporarily
270
original = patch(os, 'getcwd', lambda: '/fake/path')
271
try:
272
# Code that uses os.getcwd()
273
pass
274
finally:
275
# Restore original
276
setattr(os, 'getcwd', original)
277
"""
278
```
279
280
### Standalone Assertions
281
282
Assertion functions that can be used outside of TestCase.
283
284
```python { .api }
285
def assert_that(matchee, matcher, message='', verbose=False):
286
"""
287
Assert that matchee matches the given matcher.
288
289
Standalone assertion function using testtools matchers,
290
can be used outside of TestCase instances for utility
291
functions and general-purpose assertions.
292
293
Args:
294
matchee: Object to be matched
295
matcher: Matcher instance to apply
296
message (str): Optional failure message
297
verbose (bool): Include detailed mismatch information
298
299
Raises:
300
MismatchError: If matcher does not match matchee
301
302
Example:
303
from testtools.assertions import assert_that
304
from testtools.matchers import Equals, GreaterThan
305
306
# Use in utility functions
307
def validate_config(config):
308
assert_that(config['timeout'], GreaterThan(0))
309
assert_that(config['host'], Contains('.'))
310
"""
311
```
312
313
### Compatibility Utilities
314
315
Functions for cross-Python version compatibility.
316
317
```python { .api }
318
def reraise(exc_type, exc_value, traceback):
319
"""
320
Re-raise an exception with its original traceback.
321
322
Properly re-raises exceptions preserving the original
323
traceback information across Python versions.
324
325
Args:
326
exc_type: Exception type
327
exc_value: Exception instance
328
traceback: Exception traceback
329
"""
330
331
def text_repr(obj):
332
"""
333
Get text representation with proper escaping.
334
335
Returns a properly escaped text representation of an object,
336
handling unicode and special characters correctly.
337
338
Args:
339
obj: Object to represent
340
341
Returns:
342
str: Escaped text representation
343
"""
344
345
def unicode_output_stream(stream):
346
"""
347
Get unicode-capable output stream.
348
349
Wraps a stream to ensure it can handle unicode output
350
properly across different Python versions and platforms.
351
352
Args:
353
stream: Original output stream
354
355
Returns:
356
Stream: Unicode-capable stream wrapper
357
"""
358
```
359
360
## Usage Examples
361
362
### Safe Import Patterns
363
364
```python
365
import testtools
366
from testtools.helpers import try_import
367
368
class MyTest(testtools.TestCase):
369
370
def setUp(self):
371
super().setUp()
372
373
# Try to import optional dependencies
374
self.numpy = try_import('numpy')
375
self.pandas = try_import('pandas')
376
self.requests = try_import('requests', error_callback=self._log_import_error)
377
378
def _log_import_error(self, error):
379
"""Log import errors for debugging."""
380
self.addDetail('import_error',
381
testtools.content.text_content(str(error)))
382
383
def test_with_optional_numpy(self):
384
if self.numpy is None:
385
self.skip("NumPy not available")
386
387
# Use numpy functionality
388
arr = self.numpy.array([1, 2, 3])
389
self.assertEqual(len(arr), 3)
390
391
def test_with_fallback_json(self):
392
# Use fast JSON library if available, fall back to standard
393
json_loads = try_import('orjson.loads', alternative=json.loads)
394
json_dumps = try_import('orjson.dumps', alternative=json.dumps)
395
396
data = {'test': True, 'value': 42}
397
serialized = json_dumps(data)
398
deserialized = json_loads(serialized)
399
400
self.assertEqual(deserialized, data)
401
```
402
403
### Dictionary Operations
404
405
```python
406
import testtools
407
from testtools.helpers import map_values, filter_values, dict_subtract
408
409
class DataProcessingTest(testtools.TestCase):
410
411
def test_data_transformation(self):
412
# Original data
413
raw_data = {
414
'temperature': 20,
415
'humidity': 65,
416
'pressure': 1013,
417
'wind_speed': 15
418
}
419
420
# Transform all values (Celsius to Fahrenheit)
421
fahrenheit_data = map_values(lambda c: c * 9/5 + 32,
422
{'temperature': raw_data['temperature']})
423
self.assertEqual(fahrenheit_data['temperature'], 68.0)
424
425
# Filter for specific conditions
426
high_values = filter_values(lambda x: x > 50, raw_data)
427
self.assertIn('humidity', high_values)
428
self.assertIn('pressure', high_values)
429
self.assertNotIn('temperature', high_values)
430
431
def test_configuration_merge(self):
432
# Base configuration
433
base_config = {
434
'host': 'localhost',
435
'port': 8080,
436
'debug': False,
437
'timeout': 30
438
}
439
440
# Environment-specific overrides
441
production_overrides = {
442
'host': 'prod.example.com',
443
'debug': False,
444
'ssl': True
445
}
446
447
# Get settings that are only in base config
448
base_only = dict_subtract(base_config, production_overrides)
449
self.assertEqual(base_only, {'port': 8080, 'timeout': 30})
450
451
# Merge configurations
452
final_config = {**base_config, **production_overrides}
453
self.assertEqual(final_config['host'], 'prod.example.com')
454
self.assertTrue(final_config['ssl'])
455
```
456
457
### Monkey Patching in Tests
458
459
```python
460
import testtools
461
from testtools.helpers import MonkeyPatcher
462
import os
463
import time
464
465
class MonkeyPatchTest(testtools.TestCase):
466
467
def test_with_context_manager(self):
468
# Use MonkeyPatcher as context manager
469
with MonkeyPatcher() as patcher:
470
patcher.add_patch(os, 'getcwd', lambda: '/fake/working/dir')
471
patcher.add_patch(time, 'time', lambda: 1234567890.0)
472
473
# Test code that uses patched functions
474
current_dir = os.getcwd()
475
current_time = time.time()
476
477
self.assertEqual(current_dir, '/fake/working/dir')
478
self.assertEqual(current_time, 1234567890.0)
479
480
# Outside context, original functions are restored
481
self.assertNotEqual(os.getcwd(), '/fake/working/dir')
482
483
def test_manual_patching(self):
484
patcher = MonkeyPatcher()
485
486
# Mock file system operations
487
patcher.add_patch(os.path, 'exists', lambda path: path == '/fake/file')
488
patcher.add_patch(os.path, 'isfile', lambda path: path.endswith('.txt'))
489
490
try:
491
patcher.patch()
492
493
# Test with mocked file system
494
self.assertTrue(os.path.exists('/fake/file'))
495
self.assertFalse(os.path.exists('/other/file'))
496
self.assertTrue(os.path.isfile('document.txt'))
497
self.assertFalse(os.path.isfile('directory'))
498
499
finally:
500
patcher.restore()
501
502
def test_method_patching(self):
503
# Patch methods on test objects
504
class MockService:
505
def get_data(self):
506
return "original data"
507
508
service = MockService()
509
patcher = MonkeyPatcher()
510
patcher.add_patch(service, 'get_data', lambda: "mocked data")
511
512
with patcher:
513
result = service.get_data()
514
self.assertEqual(result, "mocked data")
515
516
# Method restored after context
517
result = service.get_data()
518
self.assertEqual(result, "original data")
519
```
520
521
### Test Organization and Discovery
522
523
```python
524
import testtools
525
from testtools.helpers import iterate_tests, clone_test_with_new_id
526
527
class TestOrganizationExample(testtools.TestCase):
528
529
def test_suite_analysis(self):
530
# Create a test suite
531
suite = testtools.TestSuite()
532
suite.addTest(TestOrganizationExample('test_method_1'))
533
suite.addTest(TestOrganizationExample('test_method_2'))
534
535
# Analyze all tests in suite
536
test_count = 0
537
test_names = []
538
539
for test in iterate_tests(suite):
540
test_count += 1
541
test_names.append(test.id())
542
543
self.assertEqual(test_count, 2)
544
self.assertIn('TestOrganizationExample.test_method_1', test_names)
545
546
def test_method_1(self):
547
self.assertTrue(True)
548
549
def test_method_2(self):
550
self.assertTrue(True)
551
552
def create_parameterized_tests():
553
"""Create multiple test variants from a base test."""
554
base_test = TestOrganizationExample('test_method_1')
555
556
# Create variants with different parameters
557
variants = []
558
for i, param in enumerate(['param1', 'param2', 'param3']):
559
variant = clone_test_with_new_id(base_test, f'test_method_1_variant_{i}')
560
variant.test_param = param # Add parameter to test
561
variants.append(variant)
562
563
return variants
564
565
class TestWithParameters(testtools.TestCase):
566
567
def test_parameter_injection(self):
568
# Use parameter if available
569
param = getattr(self, 'test_param', 'default')
570
self.assertIsNotNone(param)
571
572
# Test behavior varies by parameter
573
if param == 'param1':
574
self.assertEqual(param, 'param1')
575
else:
576
self.assertNotEqual(param, 'param1')
577
```
578
579
### Standalone Assertions
580
581
```python
582
from testtools.assertions import assert_that
583
from testtools.matchers import Equals, GreaterThan, Contains, MatchesDict
584
585
def validate_user_data(user_data):
586
"""Validate user data using testtools assertions."""
587
# Use standalone assertions for validation
588
assert_that(user_data, MatchesDict({
589
'id': GreaterThan(0),
590
'name': Contains('@'), # Assuming email format
591
'age': GreaterThan(0)
592
}))
593
594
def process_configuration(config_file):
595
"""Process configuration with validation."""
596
config = load_config(config_file)
597
598
# Validate required fields
599
assert_that(config.get('database_url'), Contains('://'))
600
assert_that(config.get('port'), GreaterThan(1000))
601
assert_that(config.get('debug'), Equals(False))
602
603
return config
604
605
# Usage in non-test code
606
try:
607
user = {'id': 123, 'name': 'user@example.com', 'age': 25}
608
validate_user_data(user)
609
print("User data is valid")
610
except MismatchError as e:
611
print(f"Validation failed: {e}")
612
```
613
614
### Utility Integration
615
616
```python
617
import testtools
618
from testtools.helpers import *
619
from testtools.matchers import *
620
621
class IntegratedUtilityTest(testtools.TestCase):
622
623
def setUp(self):
624
super().setUp()
625
626
# Set up test environment with utilities
627
self.patcher = MonkeyPatcher()
628
self.unique_id_gen = unique_text_generator()
629
630
# Mock external dependencies
631
self.requests = try_import('requests')
632
if self.requests:
633
self.patcher.add_patch(self.requests, 'get', self._mock_http_get)
634
self.patcher.patch()
635
636
def tearDown(self):
637
if hasattr(self, 'patcher'):
638
self.patcher.restore()
639
super().tearDown()
640
641
def _mock_http_get(self, url):
642
"""Mock HTTP GET responses."""
643
class MockResponse:
644
def __init__(self, url):
645
self.url = url
646
self.status_code = 200
647
self.text = f"Response from {url}"
648
649
def json(self):
650
return {"url": self.url, "status": "ok"}
651
652
return MockResponse(url)
653
654
def test_integrated_workflow(self):
655
# Generate unique test data
656
unique_name = next(self.unique_id_gen)
657
test_data = {
658
'name': f'test_user_{unique_name}',
659
'email': f'user_{unique_name}@example.com'
660
}
661
662
# Process data with filtering
663
valid_data = filter_values(lambda v: '@' in v, test_data)
664
self.assertIn('email', valid_data)
665
666
# Transform data
667
formatted_data = map_values(str.upper,
668
{'name': test_data['name']})
669
670
# Make HTTP request (mocked)
671
if self.requests:
672
response = self.requests.get('http://api.example.com/users')
673
self.assertEqual(response.status_code, 200)
674
675
# Validate response with standalone assertion
676
assert_that(response.json(), MatchesDict({
677
'url': Contains('api.example.com'),
678
'status': Equals('ok')
679
}))
680
```