Tautological tests, good and bad

Tautological tests are sometimes called tests that are too closely related to the underlying implementation, literally repeating its step-by-step. They are regularly scolded, and in general, it seems, developers are able to avoid and recognize them. Under pressure from management, sometimes such tests occur retroactively in an attempt to catch up with coverage.



However, there are complex cases.



  1. The first case is tests that reproduce the behavior of libraries. A typical case is http-client from some library where you need to create a client, stuff cookies and headers into it and send a request. There is no point in testing every step, static methods, new and other test-killer killers can still get caught there, so the easiest way is to wrap it in a thin wrapper class. Unit tests are not needed for such a class. You can test this class in the context of dependency integration tests, that is, we call it against a real service and make sure that the library works as expected. It is important that the wrapper class does not contain additional logic that can be tested as part of the unit test.
  2. The second case is tests that check something that does not return a result (or returns slurred), respectively, the only way to check that something was called is Spy with a counter. This is a valid case, albeit limited. This may include invoked stored procedures, sending emails.
  3. The third case is tests of strategies, policies, and other behavioral patterns. For example, when we create an order, we want to get an entry in the database, then he gets in the order queue, and then something is written to the logs and emails.


It is important to understand that our test in this case is a specification of a rule, a behavior. That is, he prescribes in which order the components are pulled, not because he follows them, but because he follows them. In this sense, it is logical to write it more abstractly: for example, if you need to call several services - anyway, in parallel or sequentially, then the test should describe just that. If tests are thrown by objects, then he probably should not poke his nose into the fields of this object. Therefore, our test may look almost like an implementation, or as the simplest and most naive implementation of some behavior, but at the same time know the measure in detail and not be a tautology.



Sometimes these cases, however, flow into each other. Those. we may have several calls to legacy libraries that do not really return anything, or exchange incomprehensibly with what, in order, it is not known by whom, and it’s not very clear who “prescribes” the behavior to whom. Testing it is like explaining a route to a bus driver, so it’s best to follow the first scenario.



All Articles