Hi, Habr! I present to your attention the translation of the article Test Contra-variance
From the translator: frankly, the choice of words is co-contravariance, with respect to the design of tests, is a bit strange. The semantics of course can be traced, but very metaphorical. Most likely, just for the red-word and the headline that attracts attention, so do not much carp. The rest - a great note on the topic of TDD in the format of a dialogue. It is told why TDD is so painful as to make a pleasant tool out of unit tests and does not treat them as necessarily breaking violence against freedom of expression.
Do you write unit tests?
Of course!
First tests, and then the code?
Yes, I follow the three TDD rules .
Is there a difference in the structure of test modules and code?
I make one test class for each class in the code.
That is, if the class in the main code is called User, then you will have a test
class named UserTest?
Yes, almost always.
It turns out that the test structure is covariant to the code structure?
Well, I suppose so.
So you tie the test structure to the code?
I never thought it was connectedness, but apparently yes.
And when you refactor the structure of the code classes without affecting the behavior,
tests break down?
Yes it's true.
Therefore, you can not run tests during refactoring?
Why?
Because refactoring is a sequence of minor edits that do not break
tests.
Well, based on the definition, then it really is not a refactoring.
Instead of minor edits, you have to make one big change and hope
that then you collect everything back, including tests.
Yes, and so what?
This is an example of the Fragile Test Problem.
The Problem of Fragile Tests?
Yes, a common complaint among developers who have tried TDD for the first time.
They notice that minor changes in the code lead to significant
edits in tests.
Exactly, very annoying. Almost threw TDD when I first encountered
a problem.
Unfortunately, this is a common reaction.
And what to do?
The structure of the tests should be contravariant to the code.
Contravariant?
Yes, the test structure should not reflect the structure of the code. From the fact that
some class is called X, should not follow the appearance of the test with
name XTest.
But wait, this is not by the rules!
What are the rules?
For each class there must be an appropriate test.
There is no such rule.
How not? I just read about it.
Not everything you read is the rule.
Well, if the structure of the tests should be contravariant, then how to do it like this?
Let's first define the simple fact. If a small change in one module
system leads to significant changes in other components, then the system has
bad design.
Obviously, software design 101 talks about the same thing.
Therefore, if a small change in the code leads to a big change
in tests, this is also a design problem.
The idea is clear, I agree.
Therefore, tests should have their own design. He can't just repeat
structure of the main code.
Hm That is, if the designs are the same, then they are connected, and connectedness leads
to fragility.
Exactly. The connectivity of tests and code should be minimal.
Stop! But tests and code must be related, as they describe the same thing.
behavior.
True, their behavior is connected, but this does not mean the coherence of the code structure
and tests. And even connectedness of behavior should not be as strong as you
you think.
Is there an example?
Suppose I start writing a new class. Let's call it X. And I'm doing a new test.
named XTest.
But you just said, "don't do that."
Do not get ahead of events, we just started. As you add new tests to XTest,
I am adding new code in X.
And refactor the code!
Naturally. By separating private methods from original functions that
are called in XTest.
And you refactor tests, right?
For sure! I look at the connection between XTest and X and work on minimizing it.
This can be done by adding parameters to the constructor X or increasing
level of abstraction of arguments. Or even the introduction of a polymorphic interface
between XTest and X. (1)
And all this just to write a test?
Look at it from the other side. XTest is X's first client. I always strive
reduce connectivity between client and server. Therefore, I use the same
techniques applicable to reduce connectivity in normal code.
Well, but the structure of the tests still follows the structure of the code. X and XTest
don't go anywhere.
Yes, at the class level, they are the same, but this will change. But mind you that
we already have significant differences at the level of methods.
Indeed, XTest simply uses public methods X, and the main part
code in private methods that you have highlighted.
Right! Structural symmetry is broken, but I'm going to break even more.
What is it like?
As I peer at private methods, I inevitably begin to see
how can they be grouped into classes. If the group of methods uses
a subset of the fields X, it can be separated into a separate class. (2)
But you do not write a new test?
Yes! More and more functions will be allocated, more and more classes will be
detected. And after a while we will have a whole family of classes,
sitting behind a simple API X.
And they will all be covered with XTest.
Right! The structure will be almost completely independent. And also API X gradually
will become so clean and abstract that it will be minimally connected
with customers, including XTest.
Clear. It seems that the structure of the tests can develop independently of
code and I agree that this is good. But what about the behavior, they still
pores are strongly related by behavior.
Think about what happens during the development of X. How does this affect XTest?
Well, there will be more and more tests, and the interface with X will be cleaner and
more abstract.
Right, now repeat the first part again.
There will be more and more tests?
Yes, and each of the tests is very specific, which is a small specification.
certain behavior. And in the sum they will give ...
Full requirements for the behavior of the X API.
Exactly! As development progresses, the test suite becomes
specification - tests begin to be more specific, specific.
Of course I understand.
But what happens to the classes hidden behind the X API? What makes every good
designer to cope with the growing list of requirements?
To cope with the abundance of requirements, it is naturally necessary to generalize.
Right! Instead of writing code for every single case, we
generalize it.
And how does this affect related behavior?
During the development process, test behavior becomes more and more defined,
while the code becomes more common, they move in opposite
directions along the axis of generality.
And it reduces connectivity?
Yes, because if the code meets the requirements described in the tests,
it also has the ability to cover undescribed requirements. (3)
And this is a very important point. Because no test suite can take into account
all behaviors. The code must be so general as to cover
sets of tests begin to satisfy all system requirements.
Are you saying that tests are incomplete?
Of course! It is simply impractical to describe absolutely everything. So what happens
if we gradually increase the code commonality, until any possible
tests will not pass?
Wow! We continue to write failing tests that increase
code commonality, until we can write a failing test.
Wow!
So much for wow. Backfilling - the process of generalization is the process of unleashing,
we unleash a generalize!
Incredible! That is, we unleash both structure and behavior.
Right, can you retell the point?
Okay, so the test structure should not reflect the structure of the code, because
such coherence makes the system fragile and makes refactoring difficult.
In other words, test design should not be code dependent to avoid coherence.
with him.
Well, what about behavior?
While the tests are becoming more specific, the code will be increasingly
general The behavior of the tests and the code moves in opposite directions along
community axes until we can no longer write a new failing test.
Great, I think you understand everything!
Forward to victory with contravariant tests!
(1) Here Uncle Bob skidded a little. Most likely he wanted to tell about one of the stages of writing tests / code - the formation of a public interface. Depending on the experience, the developer can either immediately know (design in the head) what the interface should be, or it will be a gradual morphing of tests and code in the right direction. At this stage, edits will be significant on both sides. Newbies throw TDD also because they do not know how to make good interfaces yet and they have to rewrite twice as much code. But this is a great practice! Moves to the side to sit with a notebook, draw and think.
(2) By no means a guide to action! This is just an example. Not all classes can be divided by this feature.
(3) It may seem quite controversial, but Uncle Bob suggests that we can no longer come up with a test or requirements that would break the code. The code is already so general that it takes into account many undescribed details.
Source: https://habr.com/ru/post/339790/