井字游戏示例的单元测试





5.00/5 (2投票s)
具有一些单元测试覆盖的极简井字棋游戏实现。
引言
我非常喜欢单元测试。虽然有很多关于单元测试的书籍和在线资源,但我总是很难找到一个简单而完整的、带有单元测试的解决方案示例。这个小项目是我为使此类项目更多地出现在网络上所做的贡献。
背景
井字棋是一个简单的游戏。对于学生游戏开发者来说,这是一个常见的第一个项目。如果您不知道规则,这是官方的维基百科页面:http://en.wikipedia.org/wiki/Tic-tac-toe。
使用代码
该解决方案有三个项目 - TicTacToeLib是主项目,它包含所有游戏逻辑,TicTacToeLibTests 是测试项目,TicTacToe 是一个 Windows Forms 项目,用于显示游戏,但它可以很容易地被任何其他 GUI 项目类型(WPF、Web 等)取代。
两个类处理游戏逻辑 - Board
和 Field
。Board
是 Field
的容器。
Field
类的唯一目的是保存有关其状态的信息。它可以是 EMPTY
,这是默认值,也可以是 PLAYER1
/PLAYER2
。每当状态改变时,会触发 FieldStatusChanged
事件。
public class Field
{
private FIELD_STATUS _fieldStatus;
public event EventHandler FieldStatusChanged;
// some code omitted
public FIELD_STATUS FieldStatus
{
get
{
return _fieldStatus;
}
set
{
if (value != _fieldStatus)
{
_fieldStatus = value;
OnFieldStatusChanged();
}
}
Board
类监听来自其 Fields
集合的 FieldStatusChanged
事件,并检查游戏结束条件。在 Board
类中为每个字段创建字段的事件处理程序。
private void AddFieldEventListeners()
{
for (int i = 0; i < _fields.GetLength(0); i++)
{
for (int j = 0; j < _fields.GetLength(1); j++)
{
_fields[i, j].FieldStatusChanged += Board_FieldStatusChanged;
}
}
}
从游戏规则中,我们可以定义五个游戏结束条件
- 获胜条件:同一行中的所有字段都属于同一玩家。 在代码术语中,
PLAYER1
或PLAYER2
中的所有字段状态都在同一行中。 - 获胜条件:同一列中的所有字段都属于同一玩家
- 获胜条件:主对角线中的所有字段都属于同一玩家
- 获胜条件:所有字段都属于同一玩家的反向对角线
- 平局条件:所有字段的值都不是
EMPTY
,但没有获胜条件适用。
每当任何字段中的状态发生变化时,都会在 Board
类中调用 CheckWinCondition
方法。 如果任何获胜条件或平局适用,则板会触发 GameEnd
事件。 作为参数,事件发送 GameStatus
类,这是一个简单的两个枚举集合,包含有关获胜玩家和获胜条件的信息。 调用者应适当地处理它 - 在此示例中,Windows Form 禁用用于显示字段的所有控件,显示游戏结果,并突出显示获胜的行、列或对角线。
最后是测试。由于类 Field
和 GameStatus
很简单,因此只有 Board
类进行了测试。BoardTests
类中有九个测试。前三个检查 Board 类是否抛出带有错误构造函数参数的异常。
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void TestConstructorFieldNull()
{
Board board = new Board(null);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void TestConstructorFieldNotSquareMatrix()
{
Field[,] fields = new Field[2, 3];
fields[0, 0] = new Field();
fields[0, 1] = new Field();
fields[0, 2] = new Field();
fields[1, 0] = new Field();
fields[1, 1] = new Field();
fields[1, 2] = new Field();
Board board = new Board(fields);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void TestConstructorNullFieldsInMatrix()
{
Field[,] fields = new Field[3, 3];
fields[0, 0] = new Field();
fields[0, 1] = new Field();
fields[0, 2] = new Field();
fields[1, 0] = new Field();
fields[1, 1] = null;
fields[1, 2] = new Field();
fields[2, 0] = new Field();
fields[2, 1] = new Field();
fields[2, 2] = new Field();
Board board = new Board(fields);
}
第四个测试测试 Fields
集合是否已成功设置为类 Fields
属性。
[TestMethod]
public void TestConstructorRegularCase()
{
Field[,] fields = new Field[3, 3];
fields[0, 0] = new Field();
fields[0, 1] = new Field();
fields[0, 2] = new Field();
fields[1, 0] = new Field();
fields[1, 1] = new Field();
fields[1, 2] = new Field();
fields[2, 0] = new Field();
fields[2, 1] = new Field();
fields[2, 2] = new Field();
Board board = new Board(fields);
Assert.AreEqual(fields.GetLength(0), board.Fields.GetLength(0));
Assert.AreEqual(fields.GetLength(1), board.Fields.GetLength(1));
}
最后五个测试模拟和测试获胜条件。所有测试都有三个部分,首先是 Board
对象构造
[TestMethod]
public void TestAllFieldsInRowWinCondition()
{
Field[,] fields = new Field[3, 3];
fields[0, 0] = new Field() { FieldStatus = FIELD_STATUS.EMPTY };
fields[0, 1] = new Field() { FieldStatus = FIELD_STATUS.EMPTY };
fields[0, 2] = new Field() { FieldStatus = FIELD_STATUS.EMPTY };
fields[1, 0] = new Field() { FieldStatus = FIELD_STATUS.EMPTY };
fields[1, 1] = new Field() { FieldStatus = FIELD_STATUS.EMPTY };
fields[1, 2] = new Field() { FieldStatus = FIELD_STATUS.EMPTY };
fields[2, 0] = new Field() { FieldStatus = FIELD_STATUS.EMPTY };
fields[2, 1] = new Field() { FieldStatus = FIELD_STATUS.EMPTY };
fields[2, 2] = new Field() { FieldStatus = FIELD_STATUS.EMPTY };
Board board = new Board(fields);
其次,为 Board GameEnd
事件创建事件处理程序。 触发事件后,它将断言板是否返回了正确的获胜参数
board.GameEnd += (sender, e) =>
{
Assert.AreEqual(GAME_STATUS.PLAYER_ONE_WON, e.GameProgress);
Assert.AreEqual(WIN_CONDITION.ROW, e.WinCondition);
Assert.AreEqual(0, e.WinRowOrColumn);
};
第三,通过更改棋盘字段状态来模拟游戏
fields[0, 0].FieldStatus = FIELD_STATUS.PLAYER1;
// [X] [ ] [ ]
// [ ] [ ] [ ]
// [ ] [ ] [ ]
fields[1, 1].FieldStatus = FIELD_STATUS.PLAYER2;
// [X] [ ] [ ]
// [ ] [0] [ ]
// [ ] [ ] [ ]
fields[0, 1].FieldStatus = FIELD_STATUS.PLAYER1;
// [X] [X] [ ]
// [ ] [0] [ ]
// [ ] [ ] [ ]
fields[2, 2].FieldStatus = FIELD_STATUS.PLAYER2;
// [X] [X] [ ]
// [ ] [0] [ ]
// [ ] [ ] [0]
fields[0, 2].FieldStatus = FIELD_STATUS.PLAYER1;
// [X] [X] [X]
// [ ] [0] [ ]
// [ ] [ ] [0]
在此测试中,第 0 行中的所有字段都属于玩家 1 (X),并且会触发 GameEnd
事件。 断言验证玩家 1 为获胜者,行作为获胜条件,行 0 作为获胜行。 如果在文章解决方案中模拟此游戏,则 Windows 窗体将以下列方式处理它
由于其余测试具有相同的结构,因此此处不再描述。
关注点
经过测试的库使维护更容易。 它们让人们对代码和可能的代码更改充满信心。
为了进一步学习,我鼓励读者进行更多测试用例和另一个将使用 TicTacToeLib 的 GUI 项目。
历史
- 初始版本。