13. First Write Test, Is It Test Driven Development

13 Starting with tests, is this Test-Driven Development? #

Hello, I am Zheng Ye.

In the previous lecture, I explained to you why programmers should write tests. Today, I am going to discuss with you at which stage programmers should write tests.

You may say, isn’t writing tests just writing code first and then writing tests? Yes, this is an intuitive answer. However, there are indeed people in this industry who have explored some different practices. Next, we will enter the less intuitive part.

Since automated testing is something that programmers should do, can we take it to the extreme and write the tests before writing the code?

Some people have indeed done this, and thus, a practice emerged of first writing tests and then writing code. What is the name of this practice? It is called Test First Development.

I know that when I ask this question, a name has already come to mind for many people, and that is Test-Driven Development (TDD), which is the well-known TDD, and TDD is the focus of our content today.

To many people, TDD means writing tests before writing code. I must clarify that this understanding is incorrect. Writing tests first and then writing code refers to Test First Development, not Test-Driven Development.

The next question that follows is, what exactly is Test-Driven Development? Test-Driven Development and Test First Development differ by only one word: “driven”. Only by understanding what “driven” means can we understand Test-Driven Development. To understand “driven”, let’s first look at the difference between these two practices.

Test-Driven Development #

The first step in learning TDD is to remember the rhythm of TDD: “Red-Green-Refactor”.

“Red” means writing a new test that has not yet passed. “Green” means writing functional code that passes the test. And “Refactor” is the process of adjusting the code after completing the basic functionality.

The “Red and Green” mentioned here originated from unit testing frameworks. When a test fails, it is displayed in red, and when it passes, it is displayed in green. This convention was established at the beginning of unit testing frameworks and has been inherited by the descendants of different languages.

As mentioned earlier, JUnit, one of the contributors to the popularity of unit testing frameworks, was co-authored by Kent Beck. Similarly, Kent Beck brought TDD from a niche circle to the public.

Considering that Kent Beck is a contributor to both unit testing frameworks and TDD, it is not difficult to understand why the rhythm of TDD is called “Red-Green-Refactor”.

Test-driven development and test-first development are the same in the first two steps: first write tests, then write code to fulfill functionality. The difference between the two lies in the fact that test-driven development doesn’t stop there; it has another important step: refactoring.

In other words, after completing the functionality and ensuring the tests pass, we will return to the code again to address any code smells or duplications that may have occurred. Because our focus in the second step, the “Green” step, is only on passing the tests.

The difference between test-first development and test-driven development lies in refactoring.

Many people believe that passing the tests means they are done, but this overlooks the potential “code smells” that new code might introduce.

If you truly understand refactoring, you will know that it is the process of eliminating code smells. Once you have tests, you can confidently refactor because any errors you introduce will be caught by the tests.

In test-driven development, refactoring and testing go hand in hand: without tests, you can only refactor with anxiety; without refactoring, the level of code chaos will gradually increase, and writing tests will become increasingly difficult.

Because of the collaboration between refactoring and testing, it drives you to write better code. This is the simplest understanding of the word “driven”.

Test-Driven Design #

Next, let’s further understand the concept of “driving” – writing code driven by tests.

Many people resist testing for two main reasons. First, they believe that testing requires additional effort. I put the word “additional” in quotation marks here because you may instinctively think that testing is extra work. However, in reality, testing should be part of a developer’s job, as I mentioned in the previous article.

Second, many people think that code is difficult to test because it contains too much code. The underlying assumption here is that the code has already been written, and then tests are written to test it.

If we reverse our thinking and start with a test, we can ask ourselves how to write code that passes the test. Once we consider the test first, our design approach completely changes: we need to write code that can be tested, i.e., code with testability. From this perspective, does testing become simpler?

This may still sound a bit abstract, so let’s take a common coding issue as an example: static methods.

Many people like to use static methods when writing code because they are convenient to use and can be called directly in any part of the code. However, when you start writing tests, you will encounter a problem. If your code directly calls a static method, it is nearly impossible to test. This is especially true if the static method contains some business logic that returns different values based on different business scenarios. Why is this the case?

Let’s consider what common testing techniques look like. If we are doing unit testing, the target of the test should be a unit. In the era where object-oriented programming is popular, this unit is typically a class. When testing a class, especially a business class, it usually involves interacting with other classes.

For example, in a common three-tier architecture for REST services, the resource layer needs to access the service layer, and the service layer needs to access the data layer. When writing the service layer code, because it depends on the data layer, a common practice is to create a fake data layer object. This allows you to complete the service layer code and perform thorough tests, even if the data layer code has not been written yet.

In the “primitive age” of coding, we would usually create a mock class to simulate the class being depended upon. Since it is a fake class, we would make it return fixed values. Objects created using this approach are generally referred to as stub objects.

The reason why this “faking” solution works is that the fake object should have the same interface as the original object and follow the same contract. From a design perspective, this is known as adhering to the Liskov Substitution Principle. However, this is not the main focus of our discussion today, so we won’t delve into it further.

Because this “faking” solution is so common, some frameworks have been developed to support it. One of the well-known frameworks is the Mock framework. With Mock objects, we can simulate various behaviors of the object being depended upon, such as returning different values or throwing exceptions.

The reason why it is not called a Stub object is that Mock objects often have an additional powerful capability: verifying the usage of the Mock object during method calls, such as how many times a method was called.

Now, let’s go back to the discussion about static methods. You will find that the approach using Mock objects does not work for static methods. This is because static methods escape the object system – they cannot be inherited, which means they cannot be handled using object-oriented techniques. You cannot use Mock objects, and therefore, you cannot easily set the corresponding method return values.

To make a static method return the desired value, you must examine the implementation details of the static method, carefully set the corresponding parameters, and follow the specific paths inside the method. Only then can you achieve the expected result.

What’s worse is that if the method is maintained by someone else and they decide to change its implementation, the parameters you carefully set will no longer work. To reset the parameters, you will need to read through the code again.

As a result, you are back to where you started. More importantly, this is not the focus of your work, and it won’t improve your key performance indicators (KPIs). Clearly, you have gone off track.

By now, you should realize that static methods are not friendly to testing. Therefore, if you want your code to be more testable, a good solution is to avoid writing static methods as much as possible.

This is a typical example of how “viewing code from a testing perspective leads to a shift in code design.”

To further supplement the discussion on static methods, let me add a few more points. From a fundamental perspective, static methods are global methods, and static variables are global variables. As we all know, global methods and global variables are things we should strive to eliminate in our programs. If we allow the use of static methods unchecked, we will end up with a similar effect as with global variables – our programs will become fragile and crash if someone modifies the code elsewhere.

Static is convenient but evil. Therefore, we should limit its use. Unless your static method does not involve any state and has a simple behavior, such as checking if a string is empty, you should not write static methods. As you may have noticed, such static methods are better suited as library functions. Therefore, when writing applications, try to avoid using them as much as possible.

The previous discussion on whether static methods can be mocked was a bit absolute. There are indeed some frameworks available in the market that can mock static methods, but I do not recommend using this feature because it is not a universally applicable solution. It only exists in certain specific languages and frameworks.

Moreover, as mentioned earlier, it will lead you down a non-reversible path in terms of design.

What should you do if you encounter third-party code with static methods in your own code? It’s simple. Wrap the third-party code so that your business code only interacts with your own encapsulation.

Based on my understanding of the programming habits of most people, the statement above contradicts the instincts of many programmers. However, if you analyze it from the perspective of code testability, you will reach this conclusion.

The approach of testing first and then writing code completely changes the way you look at code and may even require you to adjust your design in order to test more effectively. Therefore, many TDD practitioners interpret TDD as Test Driven Design.

Another typical scenario where design changes due to considering testing is Dependency Injection (DI).

However, because of the popularity of DI containers like Spring, most code nowadays is written in a style that conforms to Dependency Injection. The original approach was to directly instantiate an object, and this is an intuitive approach. However, you can deduce, based on the earlier discussion, the transition from newing an object to dependency injection.

Understanding how to write testable code is crucial for improving software design, even if you do not do TDD. So, before writing code, think about how to test it.

Even if I have made adjustments, does it mean that all the code can now be tested? Not necessarily. From my personal experience, the code that cannot be tested is often related to third-party code, such as code that accesses a database or interacts with third-party services. However, the amount of untestable code is already limited. We can isolate them in a small corner.

So far, we have discussed the philosophy of TDD. Some people may be eager to try it, but more people will use their so-called “experience” to tell you that TDD is not that easy to do.

How can we do TDD well? In the next article, I will continue to explain and the “task decomposition drama” will finally begin!

Summary #

Some excellent programmers are not only writing tests, but also exploring the practice of writing tests. Some people try to write tests first, which leads to a practice called Test-First Development. Some people go further by writing tests while adjusting the code, which is called Test-Driven Development (TDD).

In terms of steps, the key difference is that after passing the test, TDD requires going back to the code to eliminate code smells.

Test-Driven Development has become an excellent practice in the industry. The first step in learning Test-Driven Development is to remember its rhythm: Red - Green - Refactor. Putting tests first brings a change in perspective as we need to write testable code. To do this, we may even need to adjust the design, which is why some people also refer to TDD as Test-Driven Design.

If you can only remember one thing from today’s content, please remember: We should write testable code.

Finally, I would like you to share your understanding of Test-Driven Development. After studying this content, what differences have you discovered compared to your previous understanding? Feel free to share your thoughts in the comments section.

Thank you for reading. If you find this article helpful, please consider sharing it with your friends.