📜 ⬆️ ⬇️

Reflections on TDD. Why this methodology is not widely accepted

Hi, Habr!

We have long and almost unsuccessfully looking for a bright head, wishing to press Mr. Kent Beck on the market - that is, we are looking for someone who is ready to write a book for us on TDD. With real examples, a story about their own cones and achievements. There are very few books on this topic, and you will not dispute the classics ... maybe, therefore, we have not yet met with this head.

Therefore, we decided not only to remind again that we are looking for such a person, but also to offer a translation of a sufficiently debatable article, the author of which, Doug Arcuri, shares his own thoughts about why TDD never became mainstream. Let's discuss whether he is right, and if not, why.


')
This is not an introductory test development course. Here I will present my own ideas about rebooting this discipline and talk about the practical difficulties of unit testing.

The legendary programmer Kent Beck is the author of the TDD methodology (development through testing) in its modern sense. Kent also, together with Erich Gammoy, participated in the creation of JUnit, a widely used testing framework.

In his book XP Explained (Second Edition), Kent describes how principles form at the intersection of values and practices . If you build a list of concepts and substitute them in a kind of formula, you get a transformation.

[KISS, Quality, YAGNI, ...] + [Testing, Specs, ...] == [TDD, ...] 

I deeply respect this work, which is a matter of life for Kent - not only for the masterpieces he created in the field of programming, but also for the fact that he tirelessly explores the essence of trust , courage , efficiency , simplicity and vulnerability . All these attributes turned out to be indispensable for the invention of Extreme Programming (XP).

TDD is a principle and discipline that is adhered to in the XP community. This discipline is already 19 years old.

In this article, I will share my opinion on how much TDD has learned. Then I will share interesting personal observations that came to me during the occupation of TDD. Finally, I’ll try to explain why TDD didn’t fire as much as it seemed it should. Go.

TDD, research and professionalism

For the past 19 years, the TDD discipline has been a subject of controversy in the programming community.
The first question that a professional analyst would ask you is “what percentage of developers who use TDD today”? If you asked any friend Robert Martin (Uncle Bob) and friend Kent Beck about this, the answer would be “100%”.

Just Uncle Bob is sure that it is impossible to consider yourself a professional if you do not practice development through testing .

Uncle Bob has been closely involved in this discipline for several years, so it is natural to give him attention in this review. Uncle Bob defended TDD and significantly expanded the boundaries of this discipline. You can be sure that I have the utmost respect for both Uncle Bob and his pragmatic dogmatism.

However, no one asks the following question: “after all, to practice means“ consciously use ”- but it does not allow judging the percentage ratio, right?” In my subjective opinion, most programmers did not practice TDD even for a symbolic period.

The reality is that we really do not know these numbers, since no one actively investigated this percentage. All specific data are limited to a small selection of companies collected on the WeDoTDD website. Here you will find statistics on such companies, interviews with those who practice TDD all the time, but this list is small. In addition, it is incomplete, since even a simple search shows other large organizations involved in TDD - but, perhaps, not at full capacity.

If we do not know how many companies practice TDD, then the following question arises: "How effective is TDD, judging by its measurable merits?"
Probably you will be pleased that over the years a number of studies have been conducted, confirming the effectiveness of TDD. Among them - certainly authoritative reports from Microsoft , IBM , the University of North Carolina and the University of Helsinki .



Expressive diagram taken from the report of the University of Helsinki.

To a certain extent, these reports prove that the density of errors can be reduced by 40-60%, which requires more intensive work; while the execution time increases by 15-35%. These numbers are already beginning to be seen in books and new industrial methodologies, in particular, in the DevOps community.

Partly answering these questions, go to the last: "What can I expect, starting to practice TDD"? It was for the answer to him that I formulated my personal observations of TDD. Let's get to them.

1. TDD requires a verbalization approach.

While practicing TDD, we begin to face the “target designation” phenomenon. Simply put, such short projects as preparing failed and successful tests are a serious intellectual challenge for a developer. The developer has to articulate clearly: “I think that this test will be successful” and “I think that this test will fail” or “I’m not sure, let me think about it after I try this approach.”

IDE has become for the developer of the rubber duck who pleads actively to talk with her. At a minimum, at TDD enterprises, conversations of such a plan must merge into a continuous hum.

Think first - and then take your next step (or steps).

Such reinforcement plays a key role in communication: it allows you not only to predict your next step, but also to stimulate you to write the simplest code that ensures the passage of a unit test. Of course, if the developer keeps silent, he will almost certainly be off course, after which he will have to return to the rut.

2. TDD pumps motor memory

The developer, making his way through his first TDD cycles, quickly gets tired - because this process is inconvenient and constantly stalled. This is a common situation with any activity that a person only starts, but has not yet mastered. The developer will resort to shortcuts, trying to optimize this cycle to get a hand and improve the motor memory.
Motor memory is indispensable for the work was to bring pleasure and went like clockwork. In TDD, this is necessary because of the repetition of actions.

Bring out the cheat sheet with such shortcuts. Learn the maximum hotkeys in your IDE to make your loops effective. Then keep looking.

The developer in just a few sessions perfectly masters the selection of shortcuts, in particular, several sessions are enough to build and run a test stand. When you practice creating new artifacts, highlighting text and navigating through the IDE, it will all seem natural to you. Finally, you will become a true professional and learn all the techniques of refactoring: in particular, extraction, renaming, generation, lifting, reformatting and descending.

3. TDD requires at least a little thought through their actions ahead

Whenever a developer thinks of embarking on TDD, he needs to keep in mind a brief mental map of the tasks that need to be solved. In the traditional approach to programming, such a map is not always the case, but the task itself can be presented “at the macro level” or have a research nature. Perhaps the developer does not know how to solve the problem, but only approximately imagines the goal. On the way to this goal, unit tests are neglected.

Sitting down to work and ending with the next "sitting" - also try to make a ritual out of it. Think and list first. Play with it. Another list. Then proceed, do, think. Celebrate. Repeat several times. Then think again and stop.

Be adamant in your work. Keep track of what has been done - put a tick. Never fold until there is at least one. Think!

Perhaps the wording of the list will take some time that does not fit into the working cycle. However, before starting work you should have a list. Without it, you do not know where you are going. Without a card - nowhere.

 //   // "" ->   // "a" ->   // "aa" ->  // "racecar" ->  // "Racecar" ->  //   //    

The developer should make a list of tests , as described by Kent Beck. The test list allows you to solve the problem in the form of cycles, seamlessly converging into each other. Above the list of tests you need to constantly process and update, even for a few seconds before the start of the tests. If the test list is passed almost completely minus the last stage, then the result is “red”, and the entire test fails.

4. TDD depends on communication with colleagues.

After the above list is complete, some steps may be blocked, because they are not quite clearly described what to do. The developer did not understand the list of tests. The opposite also happens - the list is too crude, in which there are a lot of assumptions about the not yet formulated requirements. If you get something like that, stop right away.

If we act without TDD, we can get redundantly complex implementations. Work in the style of TDD, but mindlessly, without a list, no less dangerous.

If you see that there are spaces in the test list, stand up and say it loudly.

In TDD, a developer must understand which product to do, being guided by the notions of the necessary requirements in the interpretation of the owner - and nothing more. If the requirement in this context is unclear, then the list of tests begins to fall apart. This failure needs to be discussed. A calm discussion quickly helps build trust and respect. In addition, fast feedback loops are formed this way.

5. TDD requires an iterative architecture.

In the first edition of his book on XP, Kent suggested that tests should be the driving force behind the architecture. However, over the years, stories have emerged of how sprint teams stumble upon a wall in just a few sprints.

Of course, building an architecture based on tests is irrational. Uncle Bob himself agreed with other experts that this is no good. A more extensive map is required, but not too far from the test lists that you developed “in the field”.

Kent many years later also voiced this thesis in his book TDD By Example . Competitiveness and security are two main areas where TDD cannot be the driving force, and the developer must deal with them separately. It can be said that competitiveness is a different level of system design, competitiveness needs to be developed by iterations, coordinating this process with TDD. This is especially true today, as some architectures are developing in the direction of the reactive paradigm and reactive expansions ( reactivity is competition at the zenith).

Build a larger map of the entire organization. Helping to see things a bit in perspective. Make sure both you and the team are on the same course.

However, the idea of ​​the organization of the entire system is the most important, and one TDD organization is not provided. The point is that unit tests are a low-level thing. Iterative architecture and TDD orchestration are complex in practice and require trust between all members of the team, pair programming, and solid code reviews. It is not quite clear how to achieve this, but soon you can be sure that short design sessions should be conducted in unison with the implementation of test lists in the subject area.

6. TDD reveals the fragility of unit tests and the degenerate implementation.

Unit tests have one fun property, and TDD provides it to the full. They do not allow to prove correctness. E. V. Dijkstra worked on this problem and discussed how mathematical proofs are possible in our case that would allow filling this gap.

For example, the following example solves all tests related to a hypothetical imperfect palindrome dictated by business logic. The sample was developed using the TDD methodology.

 //    @Test fun `Given "", then it does not validate`() { "".validate().shouldBeFalse() } @Test fun `Given "a", then it does not validate`() { "a".validate().shouldBeFalse() } @Test fun `Given "aa", then it validates`() { "aa".validate().shouldBeTrue() } @Test fun `Given "abba", then it validates`() { "abba".validate().shouldBeTrue() } @Test fun `Given "racecar", then it validates`() { "racecar".validate().shouldBeTrue() } @Test fun `Given "Racecar", then it validates`() { "Racecar".validate().shouldBeTrue() } 

Indeed, there are flaws in these tests. Unit tests are fragile even in the most trivial cases. It is never possible to prove their correctness, because if we tried, it would have required incredible mental work, and the input required for this would have been impossible to imagine.

 //   ,      fun String.validate() = if (isEmpty() || length == 1) false else toLowerCase() == toLowerCase().reversed() //   ,    fun String.validate() = length > 1 length > 1 

length > 1 can be called a degenerate implementation . It is quite sufficient to solve the problem, but as such it does not report anything about the problem that we are trying to solve.

The question is - when should a developer stop writing tests? The answer seems simple: when it is enough from the point of view of business logic , and not in the opinion of the author of the code. This may hurt our design passion , and simplicity can beat someone . These feelings are compensated by the satisfaction of seeing your own pure code and understanding that later the code can be refactored with confidence. All code will be very neat.

Keep in mind that, for all the unreliability, unit tests are necessary. Understand their strengths and weaknesses. If the full picture does not add up - perhaps this gap will help fill in mutational testing .

TDD has its benefits, but this methodology can divert us to building unnecessary sand castles. Yes, this is a limitation , but thanks to it, it is possible to move faster, further and more reliably. Perhaps this is what Uncle Bob had in mind when describing what, from his point of view, it means “to be a professional” .

But! No matter how fragile the unit tests may seem, they are an absolute necessity. They turn fear into courage . Tests provide gentle code refactoring; moreover, they can serve as guidance and documentation for any new developer who can immediately get up to speed and work to the benefit of the project - if this project is well covered with unit tests.

7. TDD Demonstrates Reverse Test Assertion Cycle.

Let's take another step forward. To understand the following two phenomena, investigate strange recurring events. First, let's take a quick look at FizzBuzz. Here is our list of tests.

 //    9  15. [OK] //  ,  3,  Fizz  . // ... 

Walked a few steps forward. Now our test fails.

 @Test fun `Given numbers, replace those divisible by 3 with "Fizz"`() { val machine = FizzBuzz() assertEquals(machine.print(), "?") } class FizzBuzz { fun print(): String { var output = "" for (i in 9..15) { output += if (i % 3 == 0) { "Fizz " } else "${i} " } return output.trim() } } Expected <Fizz 10 11 Fizz 13 14 Fizz>, actual <?>. 

Naturally, if you duplicate the expected data statements in assertEquals , then the desired result is achieved, and the test is performed.

Sometimes failing tests give the correct result needed to pass the test. I don’t know what to call such events ... maybe voodoo testing . How many times you happen to see this is partly dependent on your laziness and etiquette when testing, but I have noticed such things many times when a person tries to get an implementation that normally works with ready-made and predictable data sets.

8. TDD Demonstrates Transition Priority Condition

TDD can trap you. It happens that the developer gets confused in the hand-made transformations that he uses to achieve the desired implementation. At some point, the test code turns into a bottleneck in which we are skidding.

An impasse is formed. The developer has to retreat and disarm, removing some of the tests to get out of this trap. Developer remains unprotected.

Probably, Uncle Bob himself got into such dead ends over the years of his career, after which, apparently, he realized that while ensuring the passing of the test, it was necessary to set the correct sequence of actions in order to minimize the chance of falling into a dead end. In addition, he had to realize another condition. The more specific the tests become, the more generalized the code is .



The order of conversions. You should always strive for the simplest option (at the top of the list).

This is the precedence condition of the transformations . Apparently, there is a certain order of risk of refactoring, which we are ready to enter after passing the test. It is usually best to choose the transformation option shown at the very top of the list (the simplest one) - in this case the probability of getting into a dead end remains minimal.

TPP or, so to speak, Uncle Bob’s Test Analysis is one of the most intriguing, technological, and exciting phenomena currently observed.

Be guided by it so that your code is as simple as possible.
Print the TPP list and place it on your desk. Check with him not to fall into a dead end. Take as a rule: the order should be simple.

This concludes the story of my primary observations. However, in the final part of the article I would like to return to the question that we forgot to answer at the beginning: “What is the percentage of professional programmers using TDD today?”. I would answer: "I think there are few of them." I would like to explore this question below and try to explain why.

Is TDD fixed in practice?

Unfortunately not. Subjectively, it seems that the percentage of its supporters is low, and I continue to search for data. My experience in recruiting, team management and self-development (which fascinates me) allows me to make the following observations.

Reason 1: Insufficient contact with the real testing culture.

I can reasonably assume that most developers did not have the opportunity to learn and work in a true testing culture .

A testing culture is an environment in which developers consciously practice and improve the art of testing. Constantly teach colleagues who still lack experience in this area. In each pair and with each pull request, feedback has been established to help all participants develop testing skills. In addition, there is strong support and a sense of elbow throughout the hierarchy of engineers. All managers understand the essence of testing and believe in it. When deadlines begin to tighten, testing discipline is not discarded, but continue to follow.

Those who were lucky to test oneself in such a testing culture, as I, for example, had a chance to make such observations. This experience will be useful to us in new projects.

2:

TDD, , xUnit Patterns Effective Unit Testing . , -, , , . .

. , . . , , , … .

3:

: , , . , , ; - , – .

4:

, , TDD . , .

, , : « , ». : , « » — .

– .

Conclusion

XP – , . – , . TDD.

, , . «» , , – , .



XP Explained. , , .

, - .

, – , . , , , .

, , , .

TDD « » , . TDD . TDD .

. TDD , . TDD — , , . , TDD , . , .

 @Test fun `Given software, when we build, then we expect tests`() { build(software) shoudHave tests } 

, TDD – , , . . , , , .

Source: https://habr.com/ru/post/427853/


All Articles