Implementing Data-Driven Tests with Python unittest and ddt
Overview of ddt
When automating API tests, a single endpoint typically requires validation against multiple input combinations covering positive scenarios, boundary conditions, and error cases. Manually writing individual test methods for each permutation creates redundant code. The ddt (Data-Driven Tests) library integrates with Python's built-in unittest framwork to externalize test data, enabling a single test method to execute across diverse datasets.
The library provides four primary decorators:
@ddt: Class-level decorator enabling data-driven capabilities@data: Specifies the dataset for a test method@unpack: Distributes complex data structures (tuples, lists, dictionaries) into separate arguments@file_data: Loads test cases from external JSON or YAML files
Basic Parameterization Patterns
Single Value Iteration
Apply the @data decorator with individual values to run the same assertion logic against different scalar inputs:
import unittest
from ddt import ddt, data
@ddt
class ScalarTestSuite(unittest.TestCase):
@data(100, 200, 300)
def test_numeric_validation(self, input_value):
self.assertIsInstance(input_value, int)
print(f"Processing value: {input_value}")
if __name__ == '__main__':
unittest.main()
Structured Data Unpacking
For multi-parameter scenarios, pass tuples and utilize @unpack to distribute values across method arguments:
import unittest
from ddt import ddt, data, unpack
@ddt
class TupleUnpackingTests(unittest.TestCase):
@data((10, 20, 30), (40, 50, 60))
@unpack
def test_arithmetic_operations(self, x, y, expected_sum):
actual = x + y
self.assertEqual(actual, expected_sum)
print(f"Verified: {x} + {y} = {expected_sum}")
if __name__ == '__main__':
unittest.main()
Dictionary Parameter Mapping
When working with named parameters, dictionaries provide clarity. The @unpack decorator maps dictionary values to keyword arguments:
import unittest
from ddt import ddt, data, unpack
@ddt
class DictionaryMappingTests(unittest.TestCase):
@data([
{'username': 'admin', 'role': 'superuser'},
{'username': 'guest', 'role': 'readonly'}
])
@unpack
def test_user_permissions(self, username, role):
print(f"Validating access rights for {username} with {role} privileges")
self.assertIn(role, ['superuser', 'readonly', 'editor'])
if __name__ == '__main__':
unittest.main()
External Data Integration with Excel
Real-world test automation often requires managing large datasets in spreadsheets. The following implementation demonstrates reading Excel files using xlrd and parameterizing HTTP requests with requests and ddt:
import unittest
import requests
import ddt
import xlrd
def extract_excel_dataset(file_path):
"""Parse Excel workbook and return list of row values."""
dataset = []
workbook = xlrd.open_workbook(file_path)
worksheet = workbook.sheet_by_index(0)
for row_idx in range(1, worksheet.nrows):
row_data = list(worksheet.row_values(row_idx, 0, worksheet.ncols))
dataset.append(row_data)
return dataset
# Load test data from external file
test_scenarios = extract_excel_dataset('./test_data.xlsx')
# Authentication token (typically retrieved dynamically in production)
auth_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.sample.token"
def fetch_product_catalog(category_id, status_filter):
"""Retrieve filtered product listings from API."""
request_headers = {
'Authorization': f'Bearer {auth_token}',
'Content-Type': 'application/json',
'X-Request-ID': 'automated-test-suite'
}
query_params = {
"category": category_id,
"status": int(status_filter)
}
api_response = requests.get(
"https://api.example.com/v1/products/catalog",
headers=request_headers,
params=query_params,
verify=True
)
return api_response
@ddt.ddt
class ProductCatalogAPITests(unittest.TestCase):
@ddt.data(*test_scenarios)
@ddt.unpack
def test_catalog_filter_combinations(self, category_code, availability_status):
"""Validate catalog responses across multiple filter combinations."""
response = fetch_product_catalog(category_code, availability_status)
response_payload = response.json()
self.assertEqual(response.status_code, 200)
self.assertEqual(response_payload.get('status'), 200)
self.assertIn('products', response_payload)
if __name__ == '__main__':
unittest.main(verbosity=2)
This approach separates test logic from test data, allowing non-technical stakeholders to modify Excel spreadsheets without changing Python code. The verbosity=2 flag provides detailed execution traces for debugging complex paramter combinations.