Le Cube ◇ Mass Psychosis

The Blog

FMOD
ReaScripts for Cockos REAPER
Test-Focused Development
TDD-ing Avatar Health in C# and C++
Part 1&2 - The Avatar Health assignment
Part 3 - Implementation begins - C#
Part 3 - Implementation begins - C++
Part 4 - Taking Damage
Part 5 - The Dying part
Part 6 - The Replenishing part
Part 7 - The Increasing and Max part
Part 8 - Adding the Config
TDD-ing Chess in C#
Lifting

TDD-ing Avatar Health in C# and C++

Part 6 - The Replenishing part

by Egill Antonsson
published on 2022 May 09
updated on 2022 May 14

The requirement

Let's recap it from the user perspective:

  • Link's Life Gauge can be replenished by consuming certain food
The complete requirement list from the user perspective is in Part 2

Let's reword this focusing on the domain model and using its terminology:

  • Avatar Health / CurrentPoints can be replenished by consuming food

The code

The code shown below is focused on the current cycle step, thus only showing the relevant lines .
The complete code and the Unity project is on GitHub.

Replenish cycling

The domain model focus will be a new method named Replenish.

RED

One case is if the player decides to make the Avatar
use an item that replenishes when Health is full,
a complete waste of an item as CurrentPoints don't increase,
but a valid case regardless.

// HealthTest.cs
// inside nested class Replenish
[Test]
public void CurrentPoints_WhenFullHealth()
{
	var health = new Health(12);
	health.Replenish(1);
	Assert.That(health.CurrentPoints, Is.EqualTo(12));
}

GREEN

I effortlessly pass the test by adding an empty Replenish method.

// Health.cs
public void Replenish(int replenishPoints) { }

Of course this will not hold for other cases,
so I'll cycle in more test cases to drive the implementation.

RED

Let's do a test for a more sensible valid case,
when replenish will actually increase the CurrentPoints.

// HealthTest.cs
// inside nested class Replenish
[Test]
public void CurrentPoints_WhenNotFullHealth()
{
	var health = new Health(12);
	health.TakeDamage(2);
	health.Replenish(1);
	Assert.That(health.CurrentPoints, Is.EqualTo(11));
}

I use the existing method TakeDamage to decrease the CurrentPoints
before the method Replenish is invoked (the entry point of this test).
(TakeDamage has already been tested and thus will work as expected).

GREEN

// Health.cs
public void Replenish(int replenishPoints)
{
	CurrentPoints = Math.Min(replenishPoints + CurrentPoints, FullPoints);
}

I'm confident that this is the generic solution for any valid input
so don't add more tests cases.

But I have to handle invalid input, so let's do a cycle for those.

Handle invalid input values

RED

As this will be similar to the 'take damage' handling,
I copy the existing 'take damage throws error' test
and paste it under the nested Replenish class
and change to 'replenish' appropriately.

// HealthTest.cs
// inside nested class Replenish
[TestCase(0)]
[TestCase(-1)]
public void ThrowsError_WhenReplenishPointsIsInvalid(int replenishPoints)
{
	var health = new Health(12);
	var exception = Assert.Throws(Is.TypeOf<ArgumentOutOfRangeException>(),
		delegate
		{
			health.Replenish(replenishPoints);
		});
	Assert.That(exception.Message, Does.Match("invalid").IgnoreCase);
}

This test case 'copy / paste' is valid
as it's tests the same handling for a different entry point and input.

GREEN

I invoke the existing ValidatePoints method
passing in replenishPoints and 1 for lowestValidValue

// Health.cs
public void Replenish(int replenishPoints)
{
	ValidatePoints(replenishPoints, 1); // method not shown
	CurrentPoints = Math.Min(replenishPoints + CurrentPoints, FullPoints);
}

Now the Unity Test Runner looks like this:

Unity Test Runner: after adding Replenish tests
Unity Test Runner: after adding Replenish tests