[Translation] TDD: a development methodology that changed my life

[Translation] TDD: a development methodology that changed my life


At 7:15 in the morning. Our tech support is inundated with work. We were just told about us in the program “Good Morning America” and many of those who visit our site for the first time have encountered errors.

We have a real breakthrough. We, right now, before we lose the opportunity to turn visitors to a resource into new users, we are going to roll out a patch pack. One of the developers has prepared something. He thinks it will help deal with the problem. We place a link to the updated version of the program, which has not yet gone into production, in the chat company, and we ask everyone to test it. Works!

Our heroic engineers run scripts to deploy systems and in a few minutes the update goes into battle. Suddenly, the number of calls to tech support doubles. Our urgent fix something broke, the developers grab git blame, and the engineers at this time roll back the system to the previous state.

image

The author of the material, the translation of which we are publishing today, believes that all this could have been avoided thanks to TDD.

Why am I using TDD?


I have not been in such situations for a long time. And it's not that the developers have stopped making mistakes. The fact is that for many years in each team, which I led and on which I influenced, the TDD methodology has been applied. Errors, of course, still happen, but the penetration into the production of problems that could bring the project down has dropped to almost zero, even though the software update frequency and the number of tasks that need to be solved during the update process have increased exponentially since when that happened, what I said at the beginning.

When someone asks me why he should contact TDD, I tell him this story, and I can remember about a dozen other similar cases. One of the most important reasons why I switched to TDD is that this methodology allows to improve code coverage by tests. , which leads to production getting 40-80% less errors . This is what I like most about TDD. This removes a whole mountain of problems from the developers.

In addition, it is worth noting that TDD saves developers from fear of making changes to the code.

In projects in which I participate, sets of automatic modular and functional tests almost daily prevent production of a code that could seriously disrupt these projects. For example, now I’m looking at 10 library automatic updates made last week, such that, before being released without using TDD, I would be afraid that they might spoil something.

All of these updates have been automatically integrated into the code, and they are already used in production. I didn’t check any of them manually, and didn’t worry at all that they might have a bad effect on the project. At the same time, I did not have to think long to give this example. I just opened GitHub, looked at recent mergers, and saw what I was talking about. The task that was previously solved manually (or, even worse, the task that was ignored) is now an automated background process. You can try to do something similar without good code coverage with tests, but I would not recommend doing so.

What is TDD?


TDD stands for Test Driven Development.The process implemented in the application of this methodology is very simple:


Tests detect errors, tests complete, refactoring

Here are the basic principles for using TDD:

  1. Before writing the implementation code of some possibility, write a test that allows you to check whether this future implementation code is working or not. Before proceeding to the next step, the test is run and made sure that it gives an error. Thanks to this, you can be sure that the test does not produce false-positive results, this is a kind of testing of the tests themselves.
  2. Create an implementation of the opportunity and ensure that it passes the test successfully.
  3. Perform, if necessary, refactoring of the code. Refactoring, if there is a test that is able to indicate to the developer whether the system works correctly or incorrectly, gives the developer confidence in his actions.

How can TDD help save the time it takes to develop programs?


At first glance it may seem that writing tests means a significant increase in the amount of project code, and the fact that all this takes developers a lot of extra time. In my case, at first, everything was like that, and I tried to understand how, in principle, you could write test code, and how to add tests to code that was already written.

TDD is characterized by a specific learning curve, and as long as a novice climbs this curve, the time required for development can increase by 15-35% . Often this is exactly what happens. But about 2 years after using TDD, something incredible starts to happen. Namely, I, for example, began, with the preliminary writing of unit tests, to program faster than before, when TDD did not use.

A few years ago I was implementing, in the client system, the ability to work with fragments of a video clip. Namely, it was about allowing the user to specify the beginning and end of a fragment of a recording, and receive a link to it, which would make it possible to refer to a specific place in the clip, and not to the entire clip.

I did not have a job. The player reached the end of the fragment and continued to reproduce it, but I had no idea why this was so.

I figured the problem was incorrectly connecting event listeners. My code looked like this:

  video.addEventListener ('timeupdate', () = & gt; {
  if (video.currentTime & gt; = clip.stopTime) {
  video.pause ();
  }
 });  

The process of finding a problem looked like this: making changes, compiling, reloading, clicking, waiting ... This sequence of actions was repeated again and again.

In order to check each of the changes made to the project, it was necessary to spend almost a minute, and I experienced an incredibly many solutions to the problem (most of them 2-3 times).

Maybe I made a mistake in the timeupdate keyword? Did I understand the specifics of working with the API? Does the video.pause () call work? I made changes to the code, added console.log () , went back to the browser, clicked on the Update button, clicked on the position at the end of the selected fragment, and then patiently waited until the clip is completely played. Logging inside the if construct didn’t lead to anything. It looked like a hint about a possible problem. I copied the word timeupdate from the API documentation to be absolutely sure that I didn’t make a mistake when entering it. I refresh the page again, click again, wait again.And again the program refuses to work correctly.

I finally put console.log () outside the if block. "It will not help," I thought. In the end, the if expression was so simple that I just didn’t imagine how to write it wrong. But logging in this case worked. I choked on coffee. “What is this !?” - I thought.
Murphy's debugging law. The place of the program that you have never tested, since you firmly believed that it could not contain errors, will be exactly the place where you will find an error after, completely exhausted, you make changes to this place only because of having already tried everything that they could think of.

I set a breakpoint in the program to figure out what was going on. I researched the value of clip.stopTime . To my surprise, it was undefined . Why? I looked at the code again. When the user selects the end time of the fragment, the program places the fragment end marker in the right place, but does not set the value of clip.stopTime . “I am an incredible idiot,” I thought, “I cannot be allowed on computers until the end of my life.”

I did not forget about it even years later. And all - thanks to the feeling that experienced, yet finding a mistake. You probably know what I'm talking about. With all this happened. And, perhaps, everyone can recognize themselves in this meme.


This is how I look when I program

If I wrote that program today, I would start working on it like this:

  describe ('clipReducer/setClipStopTime', async assert = & gt; {
  const stopTime = 5;
  const clipState = {
  startTime: 2,
  stopTime: Infinity
  };
  assert ({
  given: 'clip stop time',
  should: 'set clip stop time in state',
  actual: clipReducer (clipState, setClipStopTime (stopTime)),
  expected: {... clipState, stopTime}
  });
 });  

There is a feeling that there is much more code here than in this line:

  clip.stopTime = video.currentTime  

But that's the point. This code acts as a specification. This is both documentation and evidence that the code works as required by this documentation. And, since this documentation exists, if I change the order of working with the fragment end time marker, I don’t have to worry about whether I violated the correctness of the end time of the clip as I made these changes.

Here , by the way, useful material on writing unit tests, such as the one that we just looked at.

The meaning is not how long it takes to enter this code. The point is how much time it takes to debug if something goes wrong. If the code is incorrect, the test will give an excellent error report. I will immediately know that the problem is not the event handler. I will know that it is either in setClipStopTime () , or in clipReducer () , where the state change is implemented. Thanks to the test, I would know what functions the code performs, what it actually outputs, and what is expected of it. And, more importantly, my colleague will have the same knowledge, who, six months after I wrote the code, will introduce new features into it.

Starting a new project, I, as one of the first things, perform the setting watcher script , which automatically runs unit tests every time you change a certain file. I often program using two monitors. On one the developer console is opened from them, in which the results of executing such a script are displayed, the interface of the environment in which I write the code is displayed on another.When I make a change to the code, I usually, within 3 seconds, find out if the change turned out to be working or no.

For me, TDD is much more than just insurance. This is the ability to constantly and quickly, in real time, obtain information about the status of my code. Instant reward in the form of passed tests, or an instant error report in the event that I did something wrong.

How did the TDD methodology teach me to write better code?


I would like to make one confession, although it is embarrassing to admit it: I didn’t imagine how to build applications before I learned TDD and unit testing. I have no idea how I was taken to work at all, but after I interviewed many hundreds of developers, I can say with certainty that there are many programmers in a similar situation. TDD has taught me almost everything I know about effective decomposition and composition of software components (I mean modules, functions, objects, user interface components, and the like).

The reason for this is that unit tests force the programmer to test components in isolation from each other and from I/O subsystems. If the module is provided with some input data - it must provide some, previously known, output data. If he does not do this, the test fails. If it does, the test completes successfully. The point here is that the module should work independently of the rest of the application. If you are testing the logic of a state, you should be able to do this without displaying anything on the screen or saving something to the database. If you are testing the formation of a user interface, then you should be able to test it without having to load the page into a browser or access network resources.

Among other things, the TDD methodology has taught me that life becomes much simpler if, when developing user interface components, strive towards minimalism. In addition, business logic and side effects should be isolated from the user interface. From a practical point of view, this means that if you are using a UI framework based on components like React or Angular, it may be advisable to create presentation components that are responsible for displaying something on the screen, and container components that do not mix with each other.

A presentation component that receives certain properties always produces the same result. Similar components can be easily tested using unit tests. This allows you to find out whether the component is working correctly with properties, and whether the conditional logic used to form the interface is correct. For example, it is possible that the component forming the list should not display anything other than an invitation to add a new item to the list if the list is empty.

I knew about the principle of sharing responsibility long before I mastered TDD, but I didn’t know how to divide responsibility between different entities.

Unit testing allowed me to learn how to use mocks to test something, and then I learned that mocking is a sign that with code something is wrong .This stunned me and completely changed my approach to software composition.

All software development is composition: the process of breaking big problems into many small, easily solvable problems, and then creating solutions for these problems, which form the application. The locking performed for the sake of unit tests indicates that the atomic units of the composition are not atomic, in fact. Learning how to get rid of mocks without degrading code coverage with tests allowed me to learn how to identify countless hidden causes of strong connectedness of entities.

This allowed me, as a developer, to grow professionally. It taught me how to write much simpler code that is easier to expand, maintain, scale. This applies to the complexity of the code itself, and to the organization of its work in large distributed systems like cloud infrastructures.

How does TDD help save teams time?


I have already said that TDD, in the first place, leads to improving code coverage. The reason for this is that we do not begin to write the code to implement some possibility until we write a test that checks the correctness of the work of this future code. First we write a test. Then we allow it to complete with an error. Then we write an implementation feature code. We test the code, we get an error message, we achieve the correct passing of tests, we perform refactoring and repeat this process.

This process allows you to create a “barrier” through which only very few errors are able to “skip”. This error protection has a surprising effect on the entire development team. It eliminates the fear of the merge command.

The high level of code coverage by tests allows the team to get rid of the desire to manually control any, even a small change in the code base. Code changes become a natural part of the workflow.

Freedom from the fear of making changes to the code resembles the blurring of a certain machine. If this is not done, the machine will eventually stop - until it is lubricated and restarted again.

Without this fear, the process of working on programs turns out to be much calmer than before. Pull requests do not delay to the last. The CI/CD system will run the tests, and, if the tests fail, will stop the process of making changes to the project code. In this case, error messages and information about exactly where they occurred, it will be very difficult not to notice.

That's the point.

Dear readers! Do you use TDD techniques when working on your projects?

<< a>

Source text: [Translation] TDD: a development methodology that changed my life