DDT introduction
For the definition, let’s quote the author of the Python DDT library.
Let’s break down that definition with a simplistic example. We are testing aud_to_usd(value) method. That hypothetical method converts Australian dollars to USDs.
1.Multiply one test case by running it with different test data
You would for example have a test like test_aud_to_usd_ok()
with various data:
- AUD 1 => USD 0.76
- AUD 5 => USD 3.78
- AUD 0 => USD 0
- …
2. Make it appear as multiple test cases
Every single one of the above examples should run independently. In other words, for each case, one unique test should fail or pass.
Issues with using a forloop
The first requirement can easily be achieved via a forloop within a test. The code would look like this:
class ConvertionTestCase(unittest.TestCase):
def test_aud_to_usd_ok(self):
for value, result in (
(1, 0.76),
(1.00, 0.76),
(5, 3.78),
(5.00, 3.78),
(0, 0),
):
self.assertEquals(aud_to_usd(value), result)
Running it would gives me a single error or a single passing test. In case of failure, I wouldn’t know which test has failed (below, is it 1 or 1.00?) and if the following tests would have passed or not:
nosetests -v convert.py
test_aud_to_usd_ok (convert.ConvertionTestCase) ... FAIL
======================================================================
FAIL: test_aud_to_usd_ok (convert.ConvertionTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File ".../convert.py", line 21, in test_aud_to_usd_ok
self.assertEquals(aud_to_usd(value), result)
AssertionError: 0 != 0.76
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Let’s fix it with DDT
First, we need to install it by running:
pip install ddt
Then, all we need to do is to decorate or class with ddt
and move or testing tuple ((value, result),)
above the test method under the data
decorator - just like this:
import unittest
from ddt import ddt, data, file_data, unpack
@ddt
class ConvertionTestCase(unittest.TestCase):
@data(
(1, 0.76),
(1.00, 0.76),
(5, 3.78),
(5.00, 3.78),
(0, 0),
)
@unpack
def test_aud_to_usd_ok(self, value, result):
self.assertEquals(aud_to_usd(value), result)
nosetests -v convert.py
test_aud_to_usd_ok_1__1__0_76_ (convert.ConvertionTestCase) ... FAIL
test_aud_to_usd_ok_2__1_0__0_76_ (convert.ConvertionTestCase) ... FAIL
test_aud_to_usd_ok_3__5__3_78_ (convert.ConvertionTestCase) ... FAIL
test_aud_to_usd_ok_4__5_0__3_78_ (convert.ConvertionTestCase) ... FAIL
test_aud_to_usd_ok_5__0__0_ (convert.ConvertionTestCase) ... ok
FAIL: test_aud_to_usd_ok_1__1__0_76_ (convert.ConvertionTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "...python2.7/site-packages/ddt.py", line 144, in wrapper
return func(self, *args, **kwargs)
File ".../convert.py", line 22, in test_aud_to_usd_ok
self.assertEquals(aud_to_usd(value), result)
AssertionError: 0 != 0.76
....
As you can see, the DDT library has generated tests for me such as test_aud_to_usd_ok_2__1_0__0_76_
and I know exactly which ones have failed or passed. I can now easily fix my code. With the following code, I can satisfy my requirements:
def aud_to_usd(value):
return round(value * 0.75698, 2)
nosetests -v convert.py
test_aud_to_usd_ok_1__1__0_76_ (convert.ConvertionTestCase) ... ok
test_aud_to_usd_ok_2__1_0__0_76_ (convert.ConvertionTestCase) ... ok
test_aud_to_usd_ok_3__5__3_78_ (convert.ConvertionTestCase) ... ok
test_aud_to_usd_ok_4__5_0__3_78_ (convert.ConvertionTestCase) ... ok
test_aud_to_usd_ok_5__0__0_ (convert.ConvertionTestCase) ... ok
----------------------------------------------------------------------
Ran 5 tests in 0.001s
A concrete example built-in Django
Let’s pretend that you’re building a contact us form with an “enquiry type” dropdown. Depending what the message is about, it must go to a different service. Instead of writing a functional test per service, you can use DDT.
@ddt
class ContactUsTestCase(
@override_settings(FROM_EMAIL="Eat Some Code <no-reply@esc.com>")
@data(
(MESSAGE_OTHER, "info@esc.com"),
(MESSAGE_JOB, "jobs@esc.com")
)
@unpack
def test_contact_ok__email_sent(self, selected_service, email_to):
response = self.client.get(path="/contact-us/")
form = response.forms['contact_form']
form['name'] = "Robert"
form['email'] = "robert@gmail.com"
form['message'] = "Blablabla"
form['enquiry_type'] = selected_service # selected service as per above data
response = form.submit().follow()
self.assertContains(response, "Thank you")
# check the email is well sent
self.assertEquals(len(mail.outbox), 1)
email = mail.outbox[0]
self.assertEquals(email.from_email, 'ESC <no-reply@esc.com>')
self.assertEquals(email.to, [email_to]) # email_to as per above data
self.assertEquals(email.cc, [])
self.assertIn(u"Robert", email.body)
self.assertIn(u"robert@gmail.com", email.body)
self.assertIn(u"Blablabla", email.body)
The test, test_contact_ok__email_sent
will run twice: once with “Other” selected and once with “Job” selected. That way, you’re sure that the email is going to the right people in both cases. If the template is different per service, you’re also making sure that both templates are working.
DDT for Python - is an awesome library. I highly recommend it. If you know good alternatives, please let me know in the comments below.