The DDT (Data-Driven Testing) library is a third-party package that enables parameterized testing in Python's unittest framework. Before proceeding, install the package using pip: pip install ddt.
DDT provides the @ddt class decorator along with two method decorators: @data for direct test data input and @unpack for decomposing compound data structures. Understanding how these decorators work together is essential for effective data-driven testing.
When using @data, individual entries become separate test cases. If the data contains multiple values arranged as tuples, lists, or dictionaries, you must either manually process the data within the test method or use @unpack to automatically decompose it into multiple parameters.
Understanding @data Decorator Behavior
The @data decorator accepts multiple arguments, and each argument generates a distinct test execution. For example, @data(a, b) creates two test cases—one using value a and another using value b. When @data receives a list or tuple as a single argument, that entire structure passes as one parameter unless @unpack is applied.
Consider the scenario where @data([[1, 2, 3]]) is used without @unpack. The list [1, 2, 3] arrives at the test method as a single parameter. Conversely, when @data([[3, 2, 1], [5, 3, 2], [10, 4, 6]]) pairs with @unpack, the nested lists decompose into individual arguments matching the method's parameter signature.
Basic Usage Example
The following demonstrates fundamental DDT patterns with various data structures:
import unittest
from ddt import ddt, data, unpack
@ddt
class CalculatorTests(unittest.TestCase):
def setUp(self):
print('Initializing test environment')
@data([10, 20, 30])
def test_single_parameter(self, numbers):
"""Receives entire list as one parameter"""
print(f"Received data: {numbers}")
self.assertIsInstance(numbers, list)
@data([15, 8, 7], [25, 12, 13], [40, 15, 25])
@unpack
def test_subtraction_operation(self, minuend, subtrahend, expected):
"""Decomposes list into separate parameters"""
result = int(minuend) - int(subtrahend)
expected_value = int(expected)
self.assertEqual(result, expected_value)
@data([100, 200], [300, 400])
def test_without_unpack_fails(self, first, second):
"""Fails without @unpack because two parameters expected but only one list received"""
self.assertGreater(first, second)
def tearDown(self):
print('Cleaning up test environment')
if __name__ == '__main__':
unittest.main(verbosity=2)
Output Analysis
The first test, test_single_parameter, executes successfully because the complete list [10, 20, 30] arrives as a single parameter named numbers. The output displays the entire list structure.
The second test, test_subtraction_operation, generates three separate test cases. Each case receives decomposed values: the first case uses 15, 8, 7 as minuend, subtrahend, and expected respectively. All three cases pass when the subtraction logic produces expected results.
The third test, test_without_unpack_fails, encounters errors because the decorator receives two list arguments [[100, 200], [300, 400]] but lacks @unpack. Each list passes as a single argument to the first parameter, leaving the second parameter unreceived. The unittest framework reports missing required positional arguments.
Working with Different Data Types
DDT supports tuples, lists, and dictionaries as data containers. Each type requires @unpack to decompose into individual parameters. The following example demonstrates these variations:
@ddt
class ComparisonTests(unittest.TestCase):
@data((50, 30), (45, 20), (100, 75))
@unpack
def test_tuple_decomposition(self, left_value, right_value):
"""Tuples decompose just like lists"""
self.assertGreater(left_value, right_value)
@data([80, 60], [90, 70], [120, 100])
@unpack
def test_list_decomposition(self, primary, secondary):
"""Lists decompose into matching parameters"""
self.assertGreater(primary, secondary)
@data({'x': 5, 'y': 10, 'z': 15},
{'x': 8, 'y': 12, 'z': 20})
@unpack
def test_dictionary_decomposition(self, x, y, z):
"""Dictionaries decompose using keys as parameter names"""
self.assertLess(x, y)
self.assertLess(y, z)
if __name__ == '__main__':
unittest.main(verbosity=2)
Dictionary decomposition requires that dictionary keys match parameter names exactly. The @unpack decorator maps each key to its corresponding parameter, enabling clean parameterization with named arguments.
Integrating CSV Data Sources
For test data stored in external files, DDT integrates smoothly with CSV readers. The following pattern demonstrates reading CSV content and feeding it into test methods:
import csv
def load_csv_records(filepath):
"""Read CSV file and return list of rows"""
records = []
with open(filepath, mode='r', encoding='utf-8') as file:
reader = csv.reader(file)
for row in reader:
records.append(tuple(row))
return records
@ddt
class CsvDataDrivenTests(unittest.TestCase):
@data(*load_csv_records('input_data.csv'))
@unpack
def test_csv_parameterized(self, column_a, column_b, column_c):
"""Each CSV row becomes test parameters"""
print(f"Column A: {column_a}")
print(f"Column B: {column_b}")
print(f"Column C: {column_c}")
The csv.reader produces lissts, which tuples convert to ensure hashability. The * operator unpacks the returned list into individual @data arguments, generating one test case per CSV row. The @unpack decorator then decomposes each row into the three expected parameters.
This approach enables test automation with external data sources, supporting scenarios like validation testing, boundary analysis, and combinatorial testing without modifying test logic. DDT also supports file-based data loading through the file_data decorator, which reads JSON or YAML configurations for test parameterization.