Instead of intro
Unittest is probably the most famous framework for writing tests 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 opportunity that I personally (I think, not just one) lack in unittest.
A bit about Unit tests
Before discussing (and condemning) the test framework, I consider it necessary to talk a bit about testing in general. When I first heard the phrase “unit testing,” I thought that these were the duties of some quality control service that checks software modules for compliance. What was my surprise when I learned that programmers should write these same tests. The first time I did not write tests ... in general. Nothing will replace those feelings when you wake up in the morning and read from the user “The program does not work. With this urgent need to do something. " At first it seemed to me that this is a completely normal process, until I wrote the first unit test, which found an error. Unit tests generally seem useless exactly until they detect a problem. Now I just can not commit a new feature without writing a test for it.
But tests serve not only to verify the correctness of writing code. Here is a list of functions that, in my opinion, perform tests:
- Error detection in the program code
- Making programmers confident that their code works
- Corollary from the previous paragraph: the ability to safely make changes to the program
- Tests - a kind of documentation that most accurately describes the behavior of the system.
In a sense, tests repeat the structure of the program. Are program design principles applicable to tests? I believe that yes - this is the same program, even if it checks another.
Description of the problem
At some point, I got the idea to write an abstract test. What do I mean by this? This is such a test, which itself is not executed, but declares methods that depend on the 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 property dictionary for an object, obj is an object for which we are testing a serializer. check_dict checks recursively for matching dictionaries. I think many who are familiar with c unittest will immediately say that this test does not meet my definition of the abstract. Why? Because the test_fields_creation method will run 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 SerializerChecker from TestCase, but to implement the inheritors something like this:
class VehicleSerializerTest(SerializerChecker, RecursiveTestCase): model = Vehicle serializer = VehicleSerialize
RecursiveTestCase is a descendant of TestCase that implements the check_dict method.
This decision is ugly at once from several positions:
- In the SerializerChecker class, we need to know that the descendant must inherit from TestCase. This dependency can create 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
The error that gives the development environmentIt may seem that you can simply add a stub for check_dict and all problems are 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 legitimate questions about the architecture.
- There are many assert * methods in TestCase. Do we need to write a stub for each used? Still seems like a good solution?
Summarizing
Unittest does not provide the implied opportunity to "disable" the class inherited from TestCase. I would be very happy if such a function were added to the framework. And how do you solve this problem?