Multiply your Python Unit Test Cases with DDT
DDT (Data-Driven Tests) is a small python module that allows you to multiply your unit test cases for free.
The idea is pretty simple: you write a single test case and define some data samples, and DDT will generate a test case for each sample you provided.
You might ask: “Why is that useful?”
Consider the following example, a simple test case without using DDT.
import unittest
class TestWithoutDDT(unittest.TestCase):
def test_without_ddt(self):
for x in [1, -2, 3, 4, -5]:
self.assertGreater(x, 0)
If you run this test you will get the following output:
Failure
Traceback (most recent call last):
File "/home/jack/Repos/design-patterns/test_ddt.py", line 47, in test_without_ddt
self.assertGreater(x, 0)
AssertionError: -2 not greater than 0
The test failed as soon as it asserted that -2
is greater than 0
and then stopped. It didn’t consider 3
, 4
, or -5
, so you don’t don’t whether the test would have passed for those inputs or not.
Now take a look at a very similar test with DDT
.
import unittest
from ddt import ddt, data, idata, file_data, unpack
@ddt
class TestDDTData(unittest.TestCase):
@data(1, -2, 3, 4, -5)
def test_with_ddt_data(self, x):
self.assertGreater(x, 0)
If you run this test you will get two distinct failures, for -2
and -5
.
Failure
Traceback (most recent call last):
File "/home/jack/.virtualenvs/design-patterns/lib/python3.5/site-packages/ddt.py", line 139, in wrapper
return func(self, *args, **kwargs)
File "/home/jack/Repos/design-patterns/test_ddt.py", line 15, in test_with_ddt_data
self.assertGreater(x, 0)
AssertionError: -2 not greater than 0
Failure
Traceback (most recent call last):
File "/home/jack/.virtualenvs/design-patterns/lib/python3.5/site-packages/ddt.py", line 139, in wrapper
return func(self, *args, **kwargs)
File "/home/jack/Repos/design-patterns/test_ddt.py", line 15, in test_with_ddt_data
self.assertGreater(x, 0)
AssertionError: -5 not greater than 0
This means that all of the inputs were tested, and two of them failed. Now you know why DDT is so cool!
It takes less than 2 minutes to read the documentation, and the examples are great!
The main reason why I like DDT is that it’s very easy to use: just decorate a test class with the @ddt
decorator, and every test case you want with one of the decorators provided by this module. Here are the decorators available:
@data
: contains as many arguments as the values you want to feed to the test. This values can be numbers, strings, tuples, etc. In the case of tuples, a cool feature is that you can@unpack
them.@file_data
: loads the test data from a JSON or YAML file.@idata
: generates a new data sample from a generator function you defined somewhere in the code. (At this moment this decorator is not mentioned in the documentation).
Here is an example with a generator function and @idata
:
import unittest
from ddt import ddt, idata
def number_generator():
for x in [1, -2, 3, 4, -5]:
yield x
@ddt
class TestDDTGenerator(unittest.TestCase):
@idata(number_generator())
def test_with_ddt_idata(self, x):
self.assertGreater(x, 0)
And here an example where the data is stored in an external file (JSON):
mydatafile.json
[1, 2, 3, 4, 5]
import unittest
from ddt import ddt, file_data
@ddt
class TestDDTDataFile(unittest.TestCase):
@file_data('mydatafile.json')
def test_with_ddt_file_data(self, x):
self.assertGreater(x, 0)
Finally, an example where the data in unpacked:
import unittest
from ddt import ddt, data, unpack
@ddt
class TestDDTDataUnpack(unittest.TestCase):
@data(('hello', 3), ('answer', 42))
@unpack
def test_with_ddt_data_unpack(self, some_string, some_integer):
self.assertIsInstance(some_string, str)
self.assertIsInstance(some_integer, int)
Alternatives to DDT
The idea of test generators is not new, and there are at least two modules with similar capabilities: genty and data-provider. I opted for DDT because it seems better documented and more pythonic, but genty looks pretty good too. In particular, the @genty_repeat
decorator might be a nice feature that is not available in DDT(even if one could probably obtain the same functionality by using the retrying module).