While refactoring my code using Test Driven Development (TDD), should I keep making new test cases for the new refactored code I am writing?
This question is bases on the following TDD steps:
- Write just enough of a test for code to fail
- Write just enough code for the test to pass
- Refactor
My doubt is in the refactor step. Should new unit test cases be written for refactored code?
To illustrate that, I will give a simplified example:
Suppose I am making a RPG and I am making a HPContainer system that should do the following:
- Allow the player to lose HP.
- HP should not go below zero.
To answer that, I write the following tests:
[Test]
public void LoseHP_LosesHP_DecreasesCurrentHPByThatAmount()
{
int initialHP = 100;
HPContainer hpContainer= new HPContainer(initialHP);
hpContainer.Lose(5)
int currentHP = hpContainer.Current();
Assert.AreEqual(95, currentHP);
}
[Test]
public void LoseHP_LosesMoreThanCurrentHP_CurrentHPIsZero()
{
int initialHP = 100;
HPContainer hpContainer= new HPContainer(initialHP);
hpContainer.Lose(200)
int currentHP = hpContainer.Current();
Assert.AreEqual(0, currentHP);
}
To satisfy the requirements, I implement the following code:
public class HPContainer
{
private int currentHP = 0;
public void HPContainer(int initialHP)
{
this.currentHP = initialHP;
}
public int Current()
{
return this.currentHP;
}
public void Lose(int value)
{
this.currentHP -= value;
if (this.currentHP < 0)
this.currentHP = 0;
}
}
Good!
The tests are passing.
We did our job!
Now let's say the code grows and I want to refactor that code, and I decide that adding a Clamper
class as following is a good solution.
public static class Clamper
{
public static int ClampToNonNegative(int value)
{
if(value < 0)
return 0;
return value;
}
}
And as a result, changing the HPContainer class:
public class HPContainer
{
private int currentHP = 0;
public void HPContainer(int initialHP)
{
this.currentHP = initialHP;
}
public int Current()
{
return this.currentHP;
}
public void Lose(int value)
{
this.currentHP = Clamper.ClampToNonNegative(this.currentHP - value);
}
}
The tests still pass, so we are sure we did not introduce a regression in our code.
But my question is:
Should unit tests be added to the class Clamper
?
I see two opposing arguments:
-
Yes, tests should be added because we need to cover
Clamper
from regression. It will ensure that ifClamper
ever needs to be changed that we can do that safely with test coverage. -
No,
Clamper
is not part of the business logic, and is already covered by the test cases of HPContainer. Adding tests to it will only make unnecessary clutter and slow future refactoring.
What is the correct reasoning, following the TDD principles and good practices?
Best Answer
Testing before and after
"refactored code" implies you are adding the tests after you've refactored. This is missing the point of testing your changes. TDD very much relies on testing before and after implementing/refactoring/fixing code.
You shouldn't be adding your unit tests after you refactor, but rather before (assuming these tests are warranted of course).
Refactoring means unchanged behavior
The very definition of refactoring is to change the code without changing its behavior.
As unit tests are written specifically to test the behavior, it doesn't make sense for you to require additional unit tests after refactoring.
Refactoring can inherently never lead to needing additional unit tests that were not needed before.
Adding tests needs to happen sometimes
That being said, if there were tests that you should have had from the beginning but you had forgotten it until now, of course you can add them. Don't take my answer to mean that you can't add tests just because you had forgotten to write them before.
Similarly, sometimes you forget to cover a case and it only becomes apparent after you've encountered a bug. It's good practice to then write a new test that now checks for this problem case.
Unit testing other things
It seems to me that
Clamper
should be aninternal
class, as it is a hidden dependency of yourHPContainer
. The consumer of yourHPContainer
class doesn't know thatClamper
exists, and doesn't need to know that.Unit tests only focus on external (public) behavior to consumers. As
Clamper
should beinternal
, it requires no unit tests.If
Clamper
is in another assembly altogether, then it does need unit testing as it is public. But your question makes it unclear if this is relevant.Clamping?
I don't think the clamping method is all that necessary to begin with.
What you've written here is a more limited version of the existing
Math.Max()
method. Every usage:can be replaced by
Math.Max
:If your method is nothing but a wrapper around a single existing method, it becomes pointless to have it.