Read time: 4 minutes

Today I’ll show you how Test Driven Development (TDD) can help you speed up your development process.

TDD is a very different approach to software development, and it can be a bit confusing at first.

Not many devs know about it and, even when they do, they are not sure how to apply it in their day-to-day work given how counterintuitive it is.

Yet, once you get the hang of it, it can be a very powerful tool to get things done faster and with better quality.

So let’s go through a practical example to see how it works.

Let’s start.


What Is TDD?

In simple terms, Test Driven Development (TDD) is a software development approach where you write a test before you write just enough production code to make the failing test pass.

The main idea is that by starting with the tests first, you can focus on the requirements and the design of your code before you start writing the code itself.

To implement a feature using TDD you usually follow these 3 phases:

  1. Write a failing test
  2. Write just enough code to make the test pass
  3. Refactor the code

Let’s go over each of these phases with a practical example.


The requirement

For this example, let’s say we have been asked to implement a basic Warrior character in our video game application.

Regarding this Warrior character:

  • A warrior can equip a weapon.
  • Each weapon has an attack bonus, and equipping it will increase the warrior’s overall attack.
  • A warrior can only equip one weapon at a time.
  • If the warrior tries to equip a new weapon while already having one equipped, the old weapon will be replaced by the new one.

Instead of jumping right into implementing classes and methods, let’s start by writing the unit tests for this new feature.


1. Write Failing Tests

OK, so a warrior can equip a weapon and, when he does, his attack increases.

Let’s write a test for that:

public class WarriorTests
{
    [Fact]
    public void EquipWeapon_WithNewWeapon_IncreasesAttackByWeaponBunus()
    {
        // Arrange
        var sut = new Warrior();
        var weapon = new Weapon(attackBonus: 10);

        // Act
        sut.EquipWeapon(weapon);

        // Assert        
        sut.Attack.Should().Be(10);
    }
}

Notice that neither the Warrior class nor the Weapon class exist yet.

So, if we try to build this, it won’t even compile.


dotnet build

...

Build FAILED.

 [D:\projects\TDD\ 
GameLibrary\GameLibrary.UnitTests\GameLibrary.UnitTests.csproj]
    0 Warning(s)
    5 Error(s)

Time Elapsed 00:00:01.53

Yet, the test will verify that a weapon can be equipped on a warrior and that the warrior’s attack is increased by the weapon’s attack bonus.

We also know that if the warrior tries to equip a new weapon while already having one equipped, the old weapon will be replaced by the new one.

Let’s write a test for that too:

[Fact]
public void EquipWeapon_WithExistingWeapon_ReplacesOldWeapon()
{
    // Arrange
    var sut = new Warrior();
    var oldWeapon = new Weapon(attackBonus: 10);
    var newWeapon = new Weapon(attackBonus: 20);

    sut.EquipWeapon(oldWeapon);

    // Act
    sut.EquipWeapon(newWeapon);

    // Assert        
    sut.Attack.Should().Be(20);
}

We could add more test cases, but that should be good to start.

Now, on to the next phase.


2. Make the tests pass

Let’s start by creating the Warrior class:

public class Warrior
{
    public int Attack { get; set; }

    public void EquipWeapon(Weapon weapon)
    {
        Attack = weapon.AttackBonus;
    }
}

That should be good enough to satisfy our Warrior requirements, and potentially make our test cases pass.

However, we are still missing that Weapon class.

So let’s add it:

public class Weapon
{
    public Weapon(int attackBonus)
    {
        this.AttackBonus = attackBonus;
    }

    public int AttackBonus { get; set; }
}

And, with that, the tests should not just build but they should both pass:

dotnet test 

...

Passed!  - Failed:     0, Passed:     2, Skipped:     0, Total:     2, Duration: 4 ms

We are pretty much done. We have enough code to make our tests pass, and therefore satisfy our requirements.

Yet, I think we can add a couple of improvements.

So let’s move to the next phase.


3. Refactor

Here are two possible improvements:

  1. Attack and AttackBonus should be read-only properties since callers should not be able to modify them directly.
  2. Perhaps we can improve naming a bit by using HP (hit points) as opposed to Attack in both classes.

So let’s do that:

public class Warrior
{
    public int HP { get; private set; }

    public void EquipWeapon(Weapon weapon)
    {
        HP = weapon.HP;
    }
}

public class Weapon
{
    public Weapon(int hp)
    {
        HP = hp;
    }

    public int HP { get; }
}

And a quick update to the tests:

public class WarriorTests
{
    [Fact]
    public void EquipWeapon_WithNewWeapon_IncreasesAttackByWeaponBunus()
    {
        // Arrange
        var sut = new Warrior();
        var weapon = new Weapon(hp: 10);

        // Act
        sut.EquipWeapon(weapon);

        // Assert        
        sut.HP.Should().Be(10);
    }

    [Fact]
    public void EquipWeapon_WithExistingWeapon_ReplacesOldWeapon()
    {
        // Arrange
        var sut = new Warrior();
        var oldWeapon = new Weapon(hp: 10);
        var newWeapon = new Weapon(hp: 20);

        sut.EquipWeapon(oldWeapon);

        // Act
        sut.EquipWeapon(newWeapon);

        // Assert        
        sut.HP.Should().Be(20);
    }
}

And, re-running the tests should result in an all-pass again:

dotnet test

...

Passed!  - Failed:     0, Passed:     2, Skipped:     0, Total:     2, Duration: 4 ms

And, from here, you could continue adding more test cases and more code to satisfy any new requirements.


Did this speed up your development process?

Yes! By starting with the tests first, we were able to focus on the requirements and the design of our code before we started writing the code itself.

Because of that, we were able to write just enough code to satisfy the requirements, as opposed to writing a bunch of code and then trying to figure out how to test it.

So even when we did not start with the Warrior code immediately, we ended up with:

  • Less overall code to be written
  • Enough tests to verify the requirements
  • A better design


I’d like to unit test my existing code too, but I don’t have time

TDD works best when you start a new feature from scratch.

But if you have an existing code base that’s missing unit tests and you don’t have much time available, there are multiple techniques you can also use to speed things up, like:

  • Using AutoFixture
  • Running tests in parallel
  • Running tests live
  • Unit test with ChatGPT

I go over all of those in my Mastering C# Unit Testing course, where I also cover a few other techniques to master the art of unit testing real-world applications.

And that’s it for today.

I hope it was useful.



Whenever you’re ready, there are 3 ways I can help you:

  1. ​Building Microservices With .NET:​ The only .NET backend development training program that you need to become a Senior C# Backend Developer.

  2. ASP.NET Core Full Stack Bundle: A carefully crafted package to kickstart your career as an ASP.NET Core Full Stack Developer, step by step.

  3. Promote yourself to 15,000+ subscribers by sponsoring this newsletter.