65.9K
CodeProject 正在变化。 阅读更多。
Home

井字游戏示例的单元测试

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2013年7月3日

CPOL

3分钟阅读

viewsIcon

47075

downloadIcon

597

具有一些单元测试覆盖的极简井字棋游戏实现。

引言

我非常喜欢单元测试。虽然有很多关于单元测试的书籍和在线资源,但我总是很难找到一个简单而完整的、带有单元测试的解决方案示例。这个小项目是我为使此类项目更多地出现在网络上所做的贡献。

背景  

井字棋是一个简单的游戏。对于学生游戏开发者来说,这是一个常见的第一个项目。如果您不知道规则,这是官方的维基百科页面:http://en.wikipedia.org/wiki/Tic-tac-toe

使用代码

该解决方案有三个项目 - TicTacToeLib是主项目,它包含所有游戏逻辑,TicTacToeLibTests 是测试项目,TicTacToe 是一个 Windows Forms 项目,用于显示游戏,但它可以很容易地被任何其他 GUI 项目类型(WPF、Web 等)取代。

两个类处理游戏逻辑 - BoardFieldBoardField 的容器。

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;
         }
    }
} 

从游戏规则中,我们可以定义五个游戏结束条件

  • 获胜条件:同一行中的所有字段都属于同一玩家。 在代码术语中,PLAYER1PLAYER2 中的所有字段状态都在同一行中。
  •    

  • 获胜条件:同一列中的所有字段都属于同一玩家
  •       

  • 获胜条件:主对角线中的所有字段都属于同一玩家
  • 获胜条件:所有字段都属于同一玩家的反向对角线
  • 平局条件:所有字段的值都不是 EMPTY,但没有获胜条件适用。

每当任何字段中的状态发生变化时,都会在 Board 类中调用 CheckWinCondition 方法。 如果任何获胜条件或平局适用,则板会触发 GameEnd 事件。 作为参数,事件发送 GameStatus 类,这是一个简单的两个枚举集合,包含有关获胜玩家和获胜条件的信息。 调用者应适当地处理它 - 在此示例中,Windows Form 禁用用于显示字段的所有控件,显示游戏结果,并突出显示获胜的行、列或对角线。

最后是测试。由于类 FieldGameStatus 很简单,因此只有 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 项目。

历史  

  • 初始版本。
© . All rights reserved.