lukeaus
lukeaus

Reputation: 12265

A DRY way of writing similar unit tests in python

I have some similar unit tests in python. There are so similar that only one argument is changing.

class TestFoo(TestCase):
    def test_typeA(self):
        self.assertTrue(foo(bar=TYPE_A))

    def test_typeB(self):
        self.assertTrue(foo(bar=TYPE_B))

    def test_typeC(self):
        self.assertTrue(foo(bar=TYPE_C))

    ...

Obviously this is not very DRY, and if you have even 4-5 different options the code is going to be very repetitive

Now I could do something like this

class TestFoo(TestCase):
    BAR_TYPES = (
        TYPE_A,
        TYPE_B,
        TYPE_C,
        ...
    )

    def _foo_test(self, bar_type):
        self.assertTrue(foo(bar=bar_type))

    def test_foo_bar_type(self):
        for bar_type in BAR_TYPES:
            _foo_test(bar=bar_type))

Which works, however when an exception gets raised, how will I know whether _foo_test failed with argument TYPE_A, TYPE_B or TYPE_C ?

Perhaps there is a better way of structuring these very similar tests?

Upvotes: 3

Views: 367

Answers (2)

J0HN
J0HN

Reputation: 26961

What are you trying to do is essentially a parameterized test. This feature isn't included in standard django or python unittest modules, but a number of libs provide it: nose-parameterized, py.test, ddt

My favorite so far is ddt: it resembles NUnit-JUnit style parameterized tests most, pretty lightweight, don't get in your way and does not require dedicated test runner (like nose-parameterized do). The way it can help you is that it modifies test name to include all parameters, so you would clearly see which test case failed by looking at a test name.

With ddt your example would look like this:

import ddt

@ddt.ddt
class TestProcessCreateAgencyOfferAndDispatch(TestCase):

    @ddt.data(TYPE_A, TYPE_B, TYPE_C)
    def test_foo_bar_type(self, type):
        self.assertTrue(foo(bar=type))

In such case names will look like test_foo_bar_type__TYPE_A (technically, it constructs it something like [test_name]__[repr(parameter_1)]__[repr(parameter_2)]).

As a bonus, it is much cleaner (no helper method), and you get three methods instead of one. The advantage here is that you can test various code paths in a method and get one test case per each path (but a certain amount of thinking is needed, sometimes it's better to have a dedicated test for some of code paths)

Upvotes: 3

Alasdair
Alasdair

Reputation: 309109

Most TestCase assertion methods, including assertTrue, take an optional msg argument.

If you change your BAR_TYPES tuple to include the variable names, then you can include this in the message that is shown when the assertion fails.

class TestProcessCreateAgencyOfferAndDispatch(TestCase):
    BAR_TYPES = (
        ('TYPE_A', TYPE_A),
        ('TYPE_B', TYPE_B),
        ('TYPE_C', TYPE_C),
        ...
    )

    def _foo_test(self, var_name, bar_type):
        self.assertTrue(foo(bar=bar_type), var_name)

    def test_foo_bar_type(self):
        for (var_name, bar_type) in BAR_TYPES:
            _foo_test(bar=bar_type), var_name=var_name)

Upvotes: 0

Related Questions