This is the first post in a series of posts where I will implement a chess game in the TDD workflow. Though others have already done something similar (e.g. TDD and modeling a chess game by Erik Dietrich), I'm doing this to craft my TDD workflow and share its values and result with you. Some explanations will be skipped here as they are already covered in that series TTD-ing Avatar Health in C# and C++.
I will use Unity and its packages Test Framework, a Unity integration of the NUnit framework, and Code Coverage analysis tool. Other C# .NET framework could be used instead, but Unity was the handiest for me to use. The whole project is on GitHub.
I create a new Unity project (3D). Both packages Test Framework and Code Coverage are already installed as they are part of the already installed Engineering feature set. The package doc detail how to setup tests for a project. My project structure starts simple:
As the folder structure implies I'll do EditMode tests for the Runtime production code. My main reasons to prefer EditMode (over PlayMode) tests are:
The doc EditMode vs. PlayMode tests explains the difference further.
Chess has been played for over a thousand years and it evolved roughly into its current form by about 1500. The chess rules are known to many and the basic rules are:
A Position data structure seems to be at the base of the domain model. The board squares are positions, and the pieces have a position and move into a new position. To streamline calculations in the domain model the Position will be defined as 0-based Column and Row, although the presentation / view will be 'a' to 'h' Rank and 1-based File. I deem it well fitting to define Position as a Struct.
Some upfront design like this fits well with the TDD workflow, as long as it does go towards wholistic water flow design phase, as the TDD cycles should drive most of the design by taking on the next rule / requirement, one at a time.
I create the test file and write the first test and see it fail, as the code does not compile.
// PositionTest.cs
using NUnit.Framework;
namespace PositionTest
{
public class Constructor
{
[Test]
public void ColumnAndRow_AreInitialized()
{
var position = new Position(0, 1);
Assert.That(position.Column, Is.EqualTo(0));
Assert.That(position.Row, Is.EqualTo(1));
}
}
}
I write the minimal production code to compile successfully and intentionally make the test fail to verify that the test is not passing when it should not.
// Position.cs
public struct Position
{
public int Column;
public int Row;
public Position(int column, int row)
{
Column = -1;
Row = -1;
}
}
It make the test pass by providing the obvious solution that holds for all cases.
// Position.cs
public struct Position
{
public int Row;
public int Column;
public Position(int column, int row)
{
Column = column;
Row = row;
}
}
I encapsulate the properties with getters and declare the structure as immutable with the readonly keyword.
// Position.cs
public readonly struct Position
{
public int Column { get; }
public int Row { get; }
public Position(int column, int row)
{
Column = column;
Row = row;
}
}
I'm certain that the domain logic will be comparing positions thus I write the tests for it.
// PositionTest.cs
public class Equals
{
[Test]
public void AreEqual_WhenWithSameRowAndSameColumn()
{
var pos1 = new Position(0, 0);
var pos2 = new Position(0, 0);
Assert.That(pos1.Equals(pos2), Is.True);
}
[Test]
public void AreNotEqual_WhenNotWithSameRowAndSameColumn()
{
var pos1 = new Position(1, 0);
var pos2 = new Position(0, 0);
Assert.That(pos1.Equals(pos2), Is.False);
}
}
I ran this test and expected it to fail as I had not implemented anything, but it passes. Then I remember that struct has a default implementation ofEquals that makes these tests pass, paraphrasing from the Microsoft doc: "Any struct has a default implementation of value equality that it inherits from the System.ValueType. This implementation uses reflection which is relatively slow compared to a custom implementation that you write specifically for the type."
Before I go any further I will add tests for the equals operator.
// PositionTest.cs
[Test]
public void AreEqual_WhenWithSameRowAndSameColumn_UsingOperator()
{
var pos1 = new Position(0, 0);
var pos2 = new Position(0, 0);
Assert.That(pos1 == pos2, Is.True);
}
[Test]
public void AreNotEqual_WhenNotWithSameRowAndSameColumn_UsingOperator()
{
var pos1 = new Position(1, 0);
var pos2 = new Position(0, 0);
Assert.That(pos1 != pos2, Is.True);
}
These tests produce the compile errors: Cannot apply operator '==' / '!=' to operands of type 'Position' and 'Position'
Although my default approach is to use the profiler to identify performance bottlenecks and verify improvements, I will this time take the Microsoft doc words as gospel truth and thus do a custom equality implementation. I use the IDEA (Rider) to do it in an automatic manner and thus also deem it safe to jump over Red (tests fail) step
// Position.cs
public readonly struct Position : IEquatable<Position>
{
// ...
public bool Equals(Position other)
{
return Column == other.Column && Row == other.Row;
}
public override bool Equals(object obj)
{
return obj is Position other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(Column, Row);
}
public static bool operator ==(Position left, Position right)
{
return left.Equals(right);
}
public static bool operator !=(Position left, Position right)
{
return !left.Equals(right);
}
}
I make Position inherit IEquatable and execute the action generate equality members which makes the tests pass and provides the clear refactored solution.