Data-Driven/Decorated Tests - A library to multiply test cases
npx @tessl/cli install tessl/pypi-ddt@1.7.0A Python library that allows you to multiply one test case by running it with different test data, making it appear as multiple test cases. DDT provides decorators that transform unittest test methods into data-driven tests, enabling systematic validation of functionality across diverse input conditions.
pip install ddtfrom ddt import ddt, data, file_data, unpack, idata, named_data, TestNameFormatOr using the module directly:
import ddtimport unittest
from ddt import ddt, data, file_data, unpack, TestNameFormat
@ddt
class MyTestCase(unittest.TestCase):
@data(1, 2, 3, 4)
def test_simple_values(self, value):
self.assertTrue(value > 0)
@data([1, 2], [3, 4], [5, 6])
@unpack
def test_with_unpack(self, a, b):
self.assertLess(a, b)
@file_data('test_data.json')
def test_with_file_data(self, value):
self.assertIsNotNone(value)
@ddt(testNameFormat=TestNameFormat.INDEX_ONLY)
class IndexOnlyTestCase(unittest.TestCase):
@data("hello", "world")
def test_with_index_only_names(self, value):
self.assertIsInstance(value, str)
if __name__ == '__main__':
unittest.main()Enables data-driven testing on unittest.TestCase subclasses.
def ddt(arg=None, **kwargs):
"""
Class decorator for test cases that enables data-driven testing.
Parameters:
- arg: unittest.TestCase subclass or None (for parameterized decorator usage)
- **kwargs: Optional configuration parameters
- testNameFormat: TestNameFormat enum value for controlling test name generation
Returns:
Decorated test class with generated test methods
"""Provides test data values directly to test methods.
def data(*values):
"""
Method decorator to provide test data values.
Parameters:
- *values: Variable number of test data values
Returns:
Decorated test method that runs once per data value
"""Provides test data from an iterable with optional index formatting.
def idata(iterable, index_len=None):
"""
Method decorator to provide test data from an iterable.
Parameters:
- iterable: Iterable of test data values
- index_len: Optional integer specifying width to zero-pad test indices
Returns:
Decorated test method that runs once per iterable item
"""Loads test data from JSON or YAML files.
def file_data(value, yaml_loader=None):
"""
Method decorator to load test data from files.
Parameters:
- value: Path to JSON/YAML file relative to test file directory
- yaml_loader: Optional custom YAML loader function
Returns:
Decorated test method that runs with file-based data
"""Unpacks tuple/list/dict test data as separate function arguments.
def unpack(func):
"""
Method decorator to unpack structured test data.
Parameters:
- func: Test method to be decorated
Returns:
Decorated test method that unpacks data arguments
"""Provides meaningful names to data-driven tests.
def named_data(*named_values):
"""
Method decorator to provide named test data with meaningful test names.
Parameters:
- *named_values: Sequences or dictionaries with 'name' key for test identification
Returns:
Decorated test method with named test cases
"""Generates test case names from original name, value, and index.
def mk_test_name(name, value, index=0, index_len=5, name_fmt=TestNameFormat.DEFAULT):
"""
Generate a new name for a test case.
Parameters:
- name: Original test method name
- value: Test data value
- index: Test case index (default: 0)
- index_len: Width for zero-padding indices (default: 5)
- name_fmt: TestNameFormat enum value for name formatting
Returns:
str: Generated test case name
"""Determines if a value is a simple/scalar type for name generation.
def is_trivial(value):
"""
Check if value is a trivial type for test naming.
Parameters:
- value: Value to check
Returns:
bool: True if value is trivial (scalar or simple container)
"""Controls how test names are generated.
class TestNameFormat(Enum):
"""
Enum to configure test name composition.
Values:
- DEFAULT (0): Include both index and value in test name
- INDEX_ONLY (1): Include only index in test name
"""
DEFAULT = 0
INDEX_ONLY = 1List subclass with name attribute for test naming (used internally by named_data).
class _NamedDataList(list):
"""
List subclass with name attribute for meaningful test names.
Parameters:
- name: Name for the test case
- *args: List elements
"""
def __init__(self, name, *args): ...
def __str__(self): ...Dict subclass with name attribute for test naming (used internally by named_data).
class _NamedDataDict(dict):
"""
Dict subclass with name attribute for meaningful test names.
Parameters:
- **kwargs: Dictionary items including required 'name' key
Raises:
- KeyError: If 'name' key is not provided
"""
def __init__(self, **kwargs): ...
def __str__(self): ...Package version string.
__version__ = "1.7.2"import unittest
from typing import Union, Any, List, Dict, Type, Callable
# Type aliases for documentation
TestData = Union[Any, List[Any], Dict[str, Any]]
TestMethod = Callable[..., None]
TestClass = Type[unittest.TestCase]@ddt
class TestCalculator(unittest.TestCase):
@data(2, 4, 6, 8)
def test_even_numbers(self, value):
self.assertEqual(value % 2, 0)@ddt
class TestMath(unittest.TestCase):
@data((2, 3, 5), (1, 1, 2), (0, 5, 5))
@unpack
def test_addition(self, a, b, expected):
self.assertEqual(a + b, expected)# test_data.json
[
{"input": "hello", "expected": 5},
{"input": "world", "expected": 5}
]
@ddt
class TestStringLength(unittest.TestCase):
@file_data('test_data.json')
def test_length(self, value):
input_str = value['input']
expected = value['expected']
self.assertEqual(len(input_str), expected)@ddt
class TestAPI(unittest.TestCase):
@named_data(
['valid_user', 'john@example.com', 200],
['invalid_email', 'invalid-email', 400],
['empty_email', '', 400]
)
def test_user_creation(self, email, expected_status):
response = create_user(email)
self.assertEqual(response.status_code, expected_status)import yaml
# Custom YAML loader for loading specific data types
class CustomLoader(yaml.SafeLoader):
pass
def construct_custom_object(loader, node):
return {'type': 'custom', 'value': loader.construct_scalar(node)}
CustomLoader.add_constructor('!custom', construct_custom_object)
@ddt
class TestYAMLData(unittest.TestCase):
@file_data('test_data.yaml', yaml_loader=CustomLoader)
def test_yaml_with_custom_loader(self, value):
self.assertIn('type', value)
self.assertEqual(value['type'], 'custom')Example test_data.yaml file:
- !custom "test_value_1"
- !custom "test_value_2"
- !custom "test_value_3"