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:
- Write a failing test
- Write just enough code to make the test pass
- 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:
publicclassWarriorTests{[Fact]publicvoidEquipWeapon_WithNewWeapon_IncreasesAttackByWeaponBunus(){// Arrangevarsut=newWarrior();varweapon=newWeapon(attackBonus:10);// Actsut.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.
dotnetbuild...BuildFAILED.[D:\projects\TDD\GameLibrary\GameLibrary.UnitTests\GameLibrary.UnitTests.csproj]0Warning(s)5Error(s)TimeElapsed00: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]publicvoidEquipWeapon_WithExistingWeapon_ReplacesOldWeapon(){// Arrangevarsut=newWarrior();varoldWeapon=newWeapon(attackBonus:10);varnewWeapon=newWeapon(attackBonus:20);sut.EquipWeapon(oldWeapon);// Actsut.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:
publicclassWarrior{publicintAttack{get;set;}publicvoidEquipWeapon(Weaponweapon){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:
publicclassWeapon{publicWeapon(intattackBonus){this.AttackBonus=attackBonus;}publicintAttackBonus{get;set;}}
And, with that, the tests should not just build but they should both pass:
dotnettest...Passed!-Failed:0,Passed:2,Skipped:0,Total:2,Duration:4ms
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:
- Attack and AttackBonus should be read-only properties since callers should not be able to modify them directly.
- Perhaps we can improve naming a bit by using HP (hit points) as opposed to Attack in both classes.
So let’s do that:
publicclassWarrior{publicintHP{get;privateset;}publicvoidEquipWeapon(Weaponweapon){HP=weapon.HP;}}publicclassWeapon{publicWeapon(inthp){HP=hp;}publicintHP{get;}}
And a quick update to the tests:
publicclassWarriorTests{[Fact]publicvoidEquipWeapon_WithNewWeapon_IncreasesAttackByWeaponBunus(){// Arrangevarsut=newWarrior();varweapon=newWeapon(hp:10);// Actsut.EquipWeapon(weapon);// Assert sut.HP.Should().Be(10);}[Fact]publicvoidEquipWeapon_WithExistingWeapon_ReplacesOldWeapon(){// Arrangevarsut=newWarrior();varoldWeapon=newWeapon(hp:10);varnewWeapon=newWeapon(hp:20);sut.EquipWeapon(oldWeapon);// Actsut.EquipWeapon(newWeapon);// Assert sut.HP.Should().Be(20);}}
And, re-running the tests should result in an all-pass again:
dotnettest...Passed!-Failed:0,Passed:2,Skipped:0,Total:2,Duration:4ms
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 2 ways I can help you:
In-depth Courses For .NET Developers: Whether you want to upgrade your software development skills to find a better job, you need best practices for your next project, or you just want to keep up with the latest tech, my in-depth courses will help you get there, step by step. Join 700+ students here.
Patreon Community. Get access to the source code I use in all my YouTube videos, plus get exclusive discounts for my courses. Join 25+ .NET developers here.