Beginning Test-Driven Development in .NET Core

My previous two posts focused on making better software in asp.net core by applying dependency injection and unit testing to our software design. Today, we'll extend that theme by looking at test-driven development in .NET Core. My goal is to show you how it can be used to create better applications and improve your productivity as a developer - sound good?

What is test-driven development?

TDD is not a specific framework or technology - it is an approach. It's a thought process where we think of testing our code from a different perspective by writing the test code first before a single line of related functional code exists!

In a typical scenario, you first write some code, then write some tests - rinse and repeat. In TDD, you write the test case before you write a single line of code. Naturally, the test fails. Next, you write just enough code to satisfy the test so it passes. After that, we refactor as necessary to improve the design while ensuring that all tests still pass. This cycle is commonly known as Red, Green, Refactor.

You repeat this cycle many times. Typically, the initial cycles are very quick but gradually slow down as more time is spent refactoring.

Get notified on new posts Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.

Project setup

I'm using Visual Studio 2017 but will begin by setting up my project using and the new dotnet core tooling.

From the command line in my project folder, I first create a new solution file:

dotnet new sln -n dotnetCoreTDD

Next, I made a new console app project.

dotnet new console -n TDDDemoApp

Then, a new test project based on xUnit unit testing tool.

dotnet new xunit -n TDDDemoAppTests

Finally, I added both projects to my new solution.

dotnet sln dotnetCoreTDD.sln add tdddemoapp/tdddemoapp.csproj dotnet sln dotnetCoreTDD.sln add tdddemoapptests/tdddemoapptests.csproj

TDD process in action

Opening the solution, I'm starting with a completely clean slate - an empty console and test project. Let's change that.

I'm thinking about a component for my program that can analyze a piece of text and return some useful information about it like the total word count, repeated words, maybe even more advanced things like grammar and spelling.

With a few requirements in mind for this new component, I jump straight to my test project and make a new class for its tests: TextAnalyzerUnitTests.cs

To begin, I'd like a function to simply return the total word count of a given piece of text. My first step is to write a test for it. Here's what I came up with:

The test case simply news up TextAnalyzer, calls the method and checks for the expected result. As I have not written a single line of code for TextAnalyzer yet, I can't run this test or even compile the project.

We are definitely in the red.

The next step is to create the TextAnalyzer component. To start, I created a new TextAnalyzer.cs class in the console project and added the minimal amount of code to get everything compiling successfully.

At this point, we don't care about the purity of our design or elegant code. We'll take care of that when we refactor. For now, GetTotalWordCount() only throws an exception but with this stubbed out everything compiles and I can at least run the test.

Our test fails as expected because we're just throwing an exception but we're ready to fix that now by refactoring GetTotalWordCount() with the necessary code to count the words in a given string.

public int GetTotalWordCount(string text) { int wordCount = 0, index = 0; while (index < text.Length) { // check if current char is part of a word while (index < text.Length && !char.IsWhiteSpace(text[index])) index++; wordCount++; // skip whitespace until next word while (index < text.Length && char.IsWhiteSpace(text[index])) index++; } return wordCount; }

With the method properly implemented, I rerun the test and success - a green check! The method is working correctly and returns the expected word count in the test case.

We've just created a test-driven class and method - sweet! This process is very iterative. So at this point, we might spend more time refactoring GetTotalWordCount() to improve its design by adding things like validation, error handling etc. or we might move on to our next requirement. Either way, we follow the same process of writing the failing test case first, adding a minimal amount of code to pass it, and finally an exercise of refactoring and testing to ensure everything still passes.

Benefits of TDD

Intentions are clear - We didn't write a line of code until we knew exactly what we wanted to do because we let our tests drive our code design. This results in no unnecessary code and a laser focused design.

You have to eat your own dog food - TDD forces you to write your tests as a consumer of your own code. This makes you continuously think about its API.

Iterative cycles - First we started with no code, then we added the bare minimum to get things compiling, finally we implemented the real functionality. We quickly did 3 iterations in this guide but in a real-world project, we'll perform many more iterative rounds of development and testing as our project evolves over time. The iterative process of TDD means we can react to new or changing requirements with confidence.

Defects are identified early - Testing early and often means many bugs will surface as soon as they're introduced. The earlier a bug is caught, the cheaper (in time and money) it is to fix.

Testing guaranteed - Despite our best intentions, we all know adequate unit testing just doesn't always happen. TDD forces us into the habit of writing unit tests for production code - no excuses.

Wrapping up

Applying TDD requires a fundamental change in how we think about writing code. So if you're planning to try it out on your next project (or within an existing one) be patient and approach it in baby steps. To get a feel for it, try and isolate a single class or component and apply the process of thinking through the design from a testing perspective. If you find yourself simultaneously changing the code and tests it's usually a sign of a misstep in your design - no big deal. Keep iterating and you'll get it.

Thanks for reading!

Source code