Unit Testing

TestCase follows the Arrange, Act, Assert pattern espoused by the TDD community; however, the implementation may seem confounding at first. The arrange and action steps are implemented as class methods and the assertions are implemented as instance methods. This is a side-effect of using existing test runners and little more. The goal is to assure that the arrangement and action methods are run precisely once before the assertions are checked. The common unit test running tools follow the same approach as their xUnit brethren.

  1. Run class-level setup method(s)

  2. For each test case defined in the class, do the following:
    1. Run the instance-level setup methods(s)
    2. Run the test function
    3. Run the instance-level tear down method(s)
  3. Run class-level tear down methods

The AAA approach to unit testing encourages a single action under test along with many atomic assertions. In an xUnit framework, it is natural to model assertions as specific tests. Each test should be a single assertion to ensure that the root of a failure is as succinct as possible. Since we want many small assertions for a single arrangement or environment, we use the class-level setup and tear down methods.

TestCase implements a class-level setup method that delegates the arrange and action steps to sub-class defined methods named arrange and act. Test case implementations should implement class methods named arrange and act then implement test cases for each assertion:

class TheFrobinator(TestCase):
    @classmethod
    def arrange(cls):
        super(TheFrobinator, cls).arrange()
        cls.swizzle = cls.patch('frobination.internal.swizzle')
        cls.argument = 'One'
        cls.frobinator = Frobinator()

    @classmethod
    def act(cls):
        cls.return_value = cls.frobinator.frobinate(cls.argument)

    def test_should_return_True(self):
        assert self.return_value == True

    def test_should_swizzle_the_argument(self):
        self.swizzle.assert_called_once_with(self.argument)

Patching

The example included an instance of creating a patch as well. Fluent Test incorporates Michael Foord’s excellent mock library and exposes patching as the TestCase.patch() and TestCase.patch_instance() methods. Both methods patch out a specific target from the time that the patch method is called until the class-level tear down method is invoked. Patching is a great method for isolating the class that is under test since you can replace the collaborating classes, control their behavior, and place assertions over each of the interactions.

There are two primary use cases that TestCase exposes. The most common one is exposed by TestCase.patch(). It patches the target by calling mock.patch(), starts the patch, and returns the patched object. TestCase.patch_instance() is similar except that it is really meant for patching types. It returns a tuple of the patcher and patcher.return_value. This simplifies the common case of patching a class to control/inspect the instance of the class created in the unit under test. To continue our previous example, if the Frobinator creates an instance of the Swizzler, then we can use the following to test it:

class TheFrobinator(TestCase):
    @classmethod
    def arrange(cls);
        super(TheFrobinator, cls).arrange()
        cls.swizzler_cls, cls.swizzler_inst = cls.patch_instance(
            'frobination.Swizzler')
        cls.argument = 'One'
        cls.frobinator = Frobinator()

    @classmethod
    def act(cls):
        cls.return_value = cls.frobinator.frobinate(cls.argument)

    def test_should_create_a_Swizzler(self):
        self.swizzler_cls.assert_called_once_with()

    def test_should_swizzle_the_arguments(self):
        self.swizzler_inst.swizzle.assert_called_once_with(self.argument)

Exception Handling

Another useful extension that TestCase provides is to wrap the action in a try-except block. The test case can list exceptions that it is interested in receiving by adding the class attribute allowed_exceptions containing a tuple of exception classes. When an exception is raised from act() and it is listed in allowed_exceptions, then it is saved in the exception for later inspection. Otherwise, it is raised and propagates outward.