⬆️ ⬇️

Contravariant tests

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/



All Articles