TDD is one of the most well-known practices in agile development. Sadly, it's also one of the most misunderstood. This post will debunk some of these myths, besides offering you a practical guide into TDD in C#.
As such, this post assumes at least basic knowledge of C# and a familiarity with the concept of unit tests. Before getting our hands dirty, though, we'll start with some fundamentals. What exactly is TDD and why would you want to use it? How do people actually do TDD?
After the step-by-step guide is over, we'll share some final thoughts, including how TDD relates to some other high-profile techniques in agile development.
Let's get started.
TDD In C#? Let's Start With TDD In General
Before we get to the tutorial part of the post, let's make sure we're on the same page when it comes to TDD as a concept.
What Is TDD? Why Should You Care?
TDD stands for test-driven development. It's a common misconception to think TDD is a testing technique, and the name certainly plays a part in that. Actually, TDD is a software development methodology in which you use unit tests to drive the development of the code. Yep, that's right: you write your tests before writing the code that will be tested.
If you find the premise of TDD weird, don't worry: that's a common reaction. There's a method to TDD's madness, though: the goal is to produce simple, high-quality code that's easy to understand and maintain.
How Do People Do TDD?
Development in TDD happens in a short cycle composed of short phases. People call this cycle "red-green-refactor," named after the colors unit test frameworks traditionally use to express failure or success when running unit tests.
The first phase starts when you write a unit test for a scenario that doesn't exist yet. The test will obviously fail since you're attempting to test code that you haven't written yet. Actually, in compiled static-typed languages such as C# or Java, you wouldn't even be able to run the test, since the code itself won't compile.
In the next stage, you write as little code as you can to make the test pass. It's ok to "cheat" in this phase. In other words, writing code that makes the test pass but doesn't get you any closer to solving the problem isn't only allowed, but encouraged, even if it doesn't appear to make a lot of sense.
The third phase is the refactor one. This phase is the only one in which you're allowed to write production code that's not in response to a failing test. Here, your goal should be to make the code better, removing duplications, splitting larger functions into smaller ones, anything you need to make the code better. The catch is that the functionality of the code can't change. And, of course, the test must continue to pass.
TDD In C#: Let's Get Ready
You've just seen, in a nutshell, how the TDD cycle works. Now let's learn how to do it in C#, in practice.
Obtaining the Requirements
Until not that long ago, if I were to write any tutorial involving C#, I would assume a mostly Windows-based audience. I was wrong in doing so, since .NET's been cross-platform for a while. So, for this tutorial, I won't make any assumptions about platforms. You should be able to follow along whether you're working on Linux, Windows, or macOS.
So, here's what you'll need:
- Visual Studio Code or any text editor of your preference.
- The .NET SDK. At the time of this writing, the most recent, non-LTS version is .NET 5.0.
- Familiarity with the String Calculator kata by Roy Osherov. Read through his tutorial so you can familiarize yourself with the exercise.
Obtaining The Project
The post would get too long if I walked you through getting the project ready for you to perform TDD. This post isn't a C# or .NET tutorial, after all. So, I've already created the project for you. All you have to do is go to the repository on GitHub and clone or download the repository.
After you've done that, go to your terminal. Access the folder you've just cloned or downloaded, and then open it on Visual Studio Code or your favorite text editor.
The TDD Cycle
We'll now start the TDD cycle. Let's begin by writing a failing unit test.
Red: Start By Writing a Failing Test
On your text editor, open the class StringCalculatorTest. Create a new method called Add_EmptyStringAsParam_ReturnsZero(). It will verify whether passing an empty string to the Add method will result in zero. Don't forget to add the [Test] attribute to the method. Its content should be:
Assert.AreEqual(0, StringCalculator.Add(""));
By now, the complete test class should look like this:
using NUnit.Framework; namespace StringCalculatorKata.Test { public class StringCalculatorTest { [Test] public void Add_EmptyStringAsParam_ReturnsZero() { Assert.AreEqual(0, StringCalculator.Add("")); } } }
After that, the code won't even compile, since the Add method doesn't exist. Let's fix that. Paste the following code to the StringCalculator class:
public static int Add(string numbers) { throw new NotImplementedException(); }
The code now compiles and you can run the tests for the first time. Go back to your terminal and from the root of the folder, run:
dotnet test src/StringCalculatorKata.sln
The test should fail with a message like the following:
Failed Add_EmptyStringAsParam_ReturnsZero [16 ms] Error Message: System.NotImplementedException : The method or operation is not implemented.
Success! We have a failing test. However, the test is still not failing the way we'd like it to. You see, for you to have trust in your tests, it's essential not only to see them fail but to see them failing the right way. Since our test has an assertion, it should fail by failing the assertion. As it is right now, it's failing due to an exception being thrown. So, let's change that. In the Add method, replace the line throwing the exception with the following one:
return int.MinValue;
If you now run the test again, the failure message should look like this:
Failed Add_EmptyStringAsParam_ReturnsZero [39 ms] Error Message: Expected: 0 But was: -2147483648
Now we're talking! We now have a test that fails, and fails for the right reason. We're ready to continue.
Green: Let's Make the Test Pass
Our goal here isn't to solve the problem in a general way. Rather, we just have to make this one single test pass. It doesn't matter if we have to cheat to get there.
To cause the test to pass, we just have to make a single change to the code: make the Add method return 0.
public static int Add(string numbers) { return 0; }
Now the test passes:
A total of 1 test files matched the specified pattern. Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1, Duration: 40 ms
Refactor: Take a Sad Song, and Make It Better
The next phase in the TDD cycle is the refactor one. Refactoring means changing a piece of code without changing its functionality. The objective of refactoring is to make the code better, simpler, and easier to understand and change.
This is the only stage in the TDD process where you're allowed to change the production code without writing a test first. In this phase, you'd typically remove duplication, tie up some functions, and split a huge class into smaller classes. We don't have anything to refactor in our example, so we'll skip this step.
Repeat: It's the Circle of Life
TDD continues by repeating this three-phase cycle until the development concludes. We don't have time to do the complete solution of the StringCalculator kata in this post. However, I've done that in the repository I've linked to earlier. If you go to the branch called finished-kata, you'll see the complete solution. What's more: by using the git log command, you'll be able to see every step in the process.
TDD In C# (And Other Languages) Is a Proven Technique. Does It Have a Place in Distributed Teams?
In this post, we offered you a guide on how to get started with TDD in C#. You learned some fundamentals about the approach, why people use it, and how it works. Then, you learned how to get started with TDD in practice, using C# and .NET.
TDD is a tried-and-true approach, often used along with other agile techniques. For instance, it's very common to do TDD while pair programming: one team member writes the failing test, and their partner writes the production code. Then, both contribute to refactoring, and afterward, they change sides. This style of programming is sometimes called ping-pong programming.
Some people question the usefulness of those techniques, especially now, when the software development world seems to have taken an irreversible turn toward remote work. Sure, there are challenges involved, but it is possible for agile teams to go remote. If you want to learn more, take a look at our guide on distributed agile teams.
Thanks for reading, and until next time!
This post was written by Carlos Schults. Carlos is a consultant and software engineer with experience in desktop, web, and mobile development. Though his primary language is C#, he has experience with a number of languages and platforms. His main interests include automated testing, version control, and code quality.