Fat Setups, Skinny Tests

| Comments

Dave Stanek recently turned me on to a different style of unit testing:

  • write a test class for each scenario
  • put all the execution in the setup method
  • verify outcomes in tiny test methods

I’m sure someone has a better name for this, but I think of it as fat-setup/skinny-test.

A Simple Example

Here’s a fictional class that is good at finding Jedi of both good and evil inclination. Its main job is to return a tuple containing a list of light jedi, and another list of dark. Empty lists mean no matches.

Unfortunately, the adapter JediFinder uses returns None when there are no matches, so our class need to do a little logic to detect that and return empty lists instead.

JediFinder

class JediFinder(object):
    """A class good at finding different kinds of Jedi in the Universe"""
    def __init__(self, search_adapter):
        self.search_adapter = search_adapter

    def find_jedi(self):
        light_jedi, dark_jedi = self.search_adapter.find()
        if light_jedi is None:
            light_jedi = []
        if dark_jedi is None:
            dark_jedi = []
        return light_jedi, dark_jedi

The Tests

So now we want to test our finder. Here’s one way I might do fat setups. First I have a base test class that sets up things used in many/all of the test cases:

    from mock import Mock
    class JediFinderTest(object):
        def setup(self):
            self.search_adapter = Mock()
            self.finder = JediFinder(self.search_adapter)

Now I can define other test cases, each based on a different scenario. In this case, making sure the code does the right then when no jedi are found, when only dark side jedi are found, only light side, and when both are found.

    class TestWithNoResults(JediFinderTest):
        def setup(self):
            JediFinderTest.setup(self)
            self.search_adapter.find.return_value = (None, None)
            self.results = self.finder.find_jedi()

        def test_returns_empty_lists(self):
            assert self.results == ([], [])

    class TestOnlyLightJediFound(JediFinderTest):
        def setup(self):
            JediFinderTest.setup(self)
            self.search_adapter.find.return_value = (["Obi-Wan"], None)
            self.results = self.finder.find_jedi()

        def test_light_is_obiwan(self):
            assert self.results[0] == ["Obi-Wan"]

        def test_dark_is_empty(self):
            assert self.results[1] == []

    class TestOnlyDarkFound(JediFinderTest):
        def setup(self):
            JediFinderTest.setup(self)
            self.search_adapter.find.return_value = (None, ["Vader"])
            self.results = self.finder.find_jedi()

        def test_dark_is_vader(self):
            assert self.results[1] == ["Vader"]

        def test_light_is_empty(self):
            assert self.results[0] == []

    class TestLighAndDarkFound(JediFinderTest):
        def setup(self):
            JediFinderTest.setup(self)
            self.search_adapter.find.return_value = (["Obi-Wan"], ["Vader"])
            self.results = self.finder.find_jedi()

        def test_dark_is_vader(self):
            assert self.results[1] == ["Vader"]

        def test_light_is_obiwan(self):
            assert self.results[0] == ["Obi-Wan"]

Better?

I like this style because the smaller test methods get right to the point. They express what the test is verifying, not filling lines with setup and configuration.

Another developer can come along, pretty much ignore the setups, and just look at test methods to learn what’s going on.

I think it also helps with maintenance. You can add new test methods to each case when new features are added without refactoring or duplicating test setups.

blog comments powered by Disqus