Unittest and abstract tests

Instead of joining


Unittest is probably the most famous test writing framework in Python. It is very easy to learn and easy to start using in your project. But nothing is perfect. In this post I want to talk about one feature that I personally (I think, not one) lack in unittest.

A little bit about Unit tests


Before discussing (and condemning) the test framework, I consider it necessary to talk a little about testing in general. When I first heard the phrase “unit testing”, I thought that it was the responsibility of some quality control service that checks the software modules for compliance with the requirements. Imagine my surprise when I found out that these same tests should be written by programmers. At first I did not write tests ... at all. Nothing can replace those sensations when you wake up in the morning and read from the user “The program does not work. Something urgently needs to be done. ” At first it seemed to me that this was a completely normal process, until I wrote the first unit test, which found an error. Unit tests generally seem to be useless exactly until they detect problems. Now I just can’t commit a new function,
But tests are not only used to verify that the code is written correctly. Here is a list of functions that, in my opinion, perform tests:

  • Detection of errors in the program code
  • Giving programmers confidence that their code works
  • Corollary from the previous paragraph: the ability to safely modify the program
  • Tests - a kind of documentation that most correctly describes the behavior of the system

In a sense, tests repeat the structure of the program. Are the principles of building programs applicable to tests? I believe that yes - this is the same program, albeit testing another.

Description of the problem


At some point, I got the idea to write an abstract test. What do I mean by that? This is a test that does not execute itself, but declares methods that depend on parameters defined in the heirs. And then I discovered that I can not do this humanly in unittest. Here is an example:

class SerializerChecker(TestCase):
    model = None
    serializer = None
    def test_fields_creation(self):
        props = TestObjectFactory.get_properties_for_model(self.model)
        obj = TestObjectFactory.create_test_object_for_model(self.model)
        serialized = self.serializer(obj)
        self.check_dict(serialized.data, props)

I think, even without knowing the implementation of TestObjectFactory and the check_dict method, it is clear that props is a dictionary of properties for an object, obj is an object for which we are checking the serializer. check_dict recursively checks dictionaries for a match. I think many people familiar with unittest will immediately say that this test does not meet my definition of abstract. Why? Because the test_fields_creation method will execute from this class, which we absolutely do not need. After some searching for information, I came to the conclusion that the most adequate option is not to inherit the SerializerChecker from TestCase, but to implement the heirs somehow:

class VehicleSerializerTest(SerializerChecker, RecursiveTestCase):
    model = Vehicle
    serializer = VehicleSerialize

RecursiveTestCase is a descendant of TestCase that implements the check_dict method.

This solution is ugly at once from several positions:

  • In the SerializerChecker class, we need to know that the child must inherit from TestCase. This dependency can cause problems for those unfamiliar with this code.
  • The development environment stubbornly believes that I am wrong, since SerializerChecker does not have a check_dict method

image

Error that the development environment produces

It might seem that you can simply add a stub for check_dict and all problems have been resolved:

class SerializerChecker:
    model = None
    serializer = None
    def check_dict(self, data, props):
        raise NotImplementedError
    def test_fields_creation(self):
        props = TestObjectFactory.get_properties_for_model(self.model)
        obj = TestObjectFactory.create_test_object_for_model(self.model)
        serialized = self.serializer(obj)
        self.check_dict(serialized.data, props)

But this is not a complete solution to the problem:

  • We, in fact, created an interface that implements not a descendant of this class, but RecursiveTestCase, which creates reasonable questions for the architecture.
  • TestCase has many assert * methods. Do we really need to write a stub for each used? Still seems like a good solution?

Summarizing


Unittest does not provide the sane ability to "disconnect" a class inherited from TestCase. I would be very happy if such a function were added to the framework. How do you solve this problem?

Also popular now: