Maybe you have heard about the wonders of practicing TDD to develop your software. Or maybe you have read one of those rants about TDD being death. Never mind, if you want to learn how to inaugurate your TDD journey, you are at the right place. Let’s start from the very beginning.
What is TDD
TDD is a development technique based on the idea of writing tests before the production code. It was invented by KentBeck in the nineties as part of Extreme Programming. It basically works this way:
You decide the new piece of functionality you want to add and write a test that describes it
You write production code until the test pass, indicating that the functionality is working
You refactor the code for a better design
Repeat the steps until you have developed the full functionality
Key points to take into account are:
You only write one test on each iteration. That is: you don’t define the complete functionality up-front.
Each test asks you to write a part of the functionality that the production code currently lacks.
Every test should be as small as possible, and the changes in production code should be also as small as possible. This is what we call “baby steps”.
Other authors contributed to refining the technique. Namely, Robert C. Martin developed the Three Laws of TDD. They help us to decide how much test and code we should write. Here they are:
You must write a failing test before you write any production code.
You must not write more of a test than is sufficient to fail or fail to compile.
You must not write more production code than is sufficient to make the currently failing test pass.
We will return to these laws in the examples of this post.
The TDD cycle
One of the characteristics of TDD is the red-green-refactor cycle.
The cycle starts with a test that fails because no code makes it pass yet. Usually, testing frameworks represent failing tests in red color, so we say that we are in the red phase of the cycle.
Our goal is to make the test pass, something that is represented with the green color. So, we can say that our goal is to put the tests in green adding enough production code, but no more.
At this point, we should start with the most simple or obvious implementation that can work. Even if it seems too obvious or rough, we only want to make the test pass as soon as possible, establishing the desired behavior.
When we have all the tests passing in green, it is time to refactor the current implementation to a better design. This includes, among other actions:
Remove code duplication if possible
Improve naming of variables, functions, classes, constants
Improve code organization by extracting parts of it to private methods or new classes as responsibilities emerge
Improve code design as applicable conditions appear
What TDD is not
TDD uses tests as a development tool, but tests in TDD are not the same as Quality Assurance tests, although they overlap a lot.
The main differences between the TDD style of testing and QA are:
Some TDD tests are redundant for a QA suite. Usually, you start writing a lot of tests that are not needed for QA and should be removed after they fulfilled their purpose of driving development.
In general, the goal of TDD tests is to challenge the current production code to add new behavior. QA tests are meant to verify that behavior is the expected one.
Nevertheless, one of the outcomes of working using TDD is that we get a nice regression suite of unitary tests after clean it a bit.
Code quality and TDD
TDD is not a guarantee of code quality or good code design. It is a tool that helps to build software with better quality and design, preventing lots of defects and providing us with a methodology to add new functionality or improve design, without breaking anything.
This is possible because once we make a test pass, these very same tests become a regression test that ensures that the developed behavior stays untouched.
The refactor phase in the TDD cycle is the moment that we use to increase code quality, migrating from simple, naive, implementation to better-structured ones.
Benefits of TDD
TDD helps us to develop a discipline when writing software. Every test defines a short-test goal and helps us to keep focused on a single task. Using TDD you work relaxed, one step at a time. In fact, some studies show evidence that teams doing TDD spent less time debugging, have fewer bugs after deployment, and write a lot more tests.
Hands-on
Let’s start learning how to develop using Test Driven Development with a pretty simple exercise known as The Leap Year Kata. A Kata is a coding exercise that we should repeat frequently to gain automation of certain thought processes and steps. Kata is a term borrowed from a kind of exercise in martial arts.
The Leap Year Kata consists of developing a simple class or function to calculate if a given year is a leap year or not. The point of the exercise is to learn how the TDD cycle works, thus the simplicity of the problem.
For this post, I will develop a Year class, with a method isLeap that returns a bool value indicating if the year is a leap year or not. Let’s suppose a pairing session between an expert TDD practitioner and an entry-level developer.
– Ok, let’s do this. — said the expert. We are going to write this Year class guided by tests. First of all, we should take a look at the rules for calculating if a year is a leap year.
– Yes. All years that can be divided by 4, but not by 100, are Leap years.
– Good. So, years that can be divided by 100 are not leap ones. Except for years divisible by 400 that are leap years.
– Then, maybe we can start by writing a test to verify that a year is not a leap year. – Said the newbie developer.
– No, my friend. Those are a lot of steps at once. – Replied the expert.
– A lot? But, it’s only a single thing.
– Let’s see. What do you need to check that a year is or not a leap year?
– Well, I only need a class called Year, with a method isLeap. I can instantiate it with an example of a known common year and verify that the isLeap method returns false. Something like this:
– I can see that we need no less than three things: a class named year, a method in thas class named isLeap, and that the method can return false if the year is not a leap year.
– And are you suggesting writing a test for every single one of them?
– Exactly, my little padawan.
– How is that possible?
– By applying the TDD rules. What is the first one of them?
– Hum… We cannot write production code unless we have a failing test.
– So…
– So, we need to write a test.
– And… what does the second law say?
– It says that you cannot write more than one test enough to fail or not compile.
– That’s correct. And if we write that test you show me before, we will find that it can fail for a lot of reasons. The first thing we need is to be able to instantiate the class, so our first test should force us to define the class to the point that we can instantiate one object.
– So, that’s a baby step!
– Sure! Like this:
– But, but, but… you are not even passing an argument to instantiate the class.
– I know. But this test is enough to fail and to tell us exactly what we need to do next. See what happens if we execute it:
Error : Class ‘Year’ not found
– We should write the class, I guess.
– Yes, but how much code should we write.
– Well, we can create a new file named year.php, and define a class named Year, containing a meth…
– Hold! We only need to write enough code to make this test pass. We don’t even need to create a new file for that.
– But, that’s not a correct practice. You should separate classes in files.
– I know that, but we will have the refactoring phase for that. In the meantime, we need to make the test pass, and we can achieve that by doing the following:
– See? The test passes.
– All right, all right, teacher. But now, we should move the class to its own file.
– Of course, but now we are protected by a test. If we move the class to the wrong file, the test will not pass, indicating our mistake.
– Really?
– For sure! Let's move it and finish our first iteration.
– So, we have moved the class and the test is still passing. That’s great. We have completed a cycle. What should we do the next, little padawan developer?
– We could write a test that invokes the “isLeap” method, but we should instantiate the class before.
– You’re right. However, we can refactor our current test to have an instance of the class before writing the test. What do you think?
– I can’t see how useful that would be, but I trust you.
We prepare ourselves for the next iteration by doing this, leaving the code easy to change as needed, while the test is green. See the code:
– That’s not a huge change.
– No, it isn’t. But it is an enabler for our next step.
– Now, if we run the test, it fails because of a new reason:
Error : Call to undefined method App\Katas\LeapYear\Year::isLeap()
– Yes, little padawan. And the reason is that we don’t have the required method.
– I see. But… you are writing all in the same test. Is that right?
– At this point, it is fine. We could write each iteration in a different test, but we will remove these simple tests at the end of the process, so…
– Wait! Are we going to remove this test at the end? Are you kidding me?
– No, I’m not kidding. The point is that some of the tests that we write in TDD are useless or redundant out of this context, so we’ll remove them, leaving those that can act as regression tests.
– Oh!
– Let’s continue. We need to write our “isLeap” method.
– Yes, we can then pass the year as a parameter and check if we can divide it by four or not.
– My dear padawan: what is the minimum piece of code that will make the test pass?
– Hum… Let me think… Oh! I know: it will be enough with the method definition.
– That’s correct.
– So, we have the test passing again. Could we improve the code in some way?
– Is there code to improve?
– Maybe.
– Ufff. Let me think. Possibly we can declare the return type but it will force us to return a value, and we don’t know what to return at this moment.
– Any other improvement that can help us with the next steps?
– Errr… We could introduce the parameter needed in the constructor even if we don’t use it. This will break the test but… we can prevent that by changing the test first, and then modifying the production code to use it.
– Fantastic, my padawan! You learn fast. Now the production code, please.
– Yes. Here it is:
– Great! Now we are ready to start testing behavior. Maybe your first test makes sense now.
– Do you mean that we have been doing this workaround only to return to my first test? Seriously?
– Don’t get angry. Anger leads to the dark side. You should practice those baby steps until you automate and perform them in a few seconds. That will help you avoid many silly errors, like typos, putting the wrong file in the wrong folder, and using wrong names... Let’s see your test now and run it. What should happen?
– Easy. It will fail.
– Because…
– Because… we don’t have any code that checks that the value of the year can be divided by four, so we will need to add code that can do exactly that.
– Can you see my point? If we had executed your first test, we would have found that it failed because we have neither the class nor the method. Now, we are sure that the unique valid reason for the test to fail is not having code that performs the behavior that we want.
– So, let’s consider the test again:
– Fine. If we execute it, it fails this way:
Failed asserting that null is false.
– Yes. And it fails because of a good reason: the behavior is not implemented.
– Then we now can check if the year is divisible by…
– I have a better idea, for now, we can simply do the following:
– Now, I know you’re fooling me.
– Not at all. Run the test, please.
– It passes. But, this will not detect leap years.
– Yes. It’s pretty obvious. But that’s exactly the behavior that we want at this moment as defined by our tests. Now, our goal is to challenge the current implementation with a new test.
– Let me guess. We should write a test that verifies that if we instantiate the class with a genuine leap year it will be detected.
– You are right. And here is such a test:
– And it fails because of the right reason:
Failed asserting that false is true.
– True. We need to implement something that makes it pass.
– Yesssss. Finally!
– Great job, young padawan. Now the test passes and the code identifies most of the leap years. But maybe we can refactor it a bit, don’t you think?
– Yes, let’s do it. I see that we can eliminate the if and return the result of the boolean expression. Also, we can declare the return type as boolean.
– Nice. But we need to detect special no leap years, like 1900 or 1800.
– We need a test for that, teacher.
– And here it is:
– If we run it, it fails.
Failed asserting that true is false.
– I‘ve noted that you are running all of the tests. Why?
– Because I want to be sure that we don’t break a test that was passing before. This would mean that we have altered the behavior in some way. If this happens, we should stop and fix the code to make that test pass again.
– Ok, so every test that we make pass becomes a regression test.
– Exactly. That’s mandatory to be able to refactor. While refactoring, tests must be passing. All of them. That guarantees that we preserve the behavior.
– But right now, we have a test in red, so it’s time to implement something new, isn’t it?
– Yes. We should manage the situation in which a year is divisible by 100.
– That was pretty simple, but it does the job.
– It is fine enough. Maybe we can refactor something.
– Let me see... We have this idea of divisible by in the code, perhaps we can make it explicit in code extracting the calculation to a private method.
– Interesting. I like it. Let’s do it. But don’t forget to check that tests keep passing.
– Wow! We are near to complete the development. We need a new test so we can implement the management of the special leap years every 400 years.
– For example, the year 2000.
– That’s right. Let’s write the test:
– As expected, this test doesn’t pass.
– We are on the right track to implement production code that makes it green:
– So, my dear padawan, we are mostly done. Do you think we could refactor something here?
– I can’t see a good opportunity for that. The code is pretty simple and clean. Perhaps we could combine the two last conditions, because if a year number is divisible by four, but not by 100, then the year is a leap year and common if not.
– Yes. I agree. So, we can refactor the code to reflect that and finish here our first lesson.
– It’s nice, but I’m not sure if it is better. Anyway, having tests has allowed us to try this new approach being sure that the behavior is not broken. TDD allows for that and to decide if you can refactor to a better design, or experiment with different options.
– This approach has surprised me. It seems slow, but I liked the process a lot.
– You should practice TDD exercises, every day if possible. By doing so, you will gain experience, speed, and trust. Now, you should practice this kata several times, until you perform it fluidly and fast.
– I will do.
– Next time, I will propose you a slightly more complex exercise, and you’ll see that the process is exactly the same.
– Thank you. I’m looking forward to a new session.
Top comments (0)