测试
程序入口 之后,棋盘已经写出。测试代码在 tests 目录,用来核对 Board 的公开行为。CMake 用 FetchContent 取 GoogleTest,tictactoe_test 链接动态库 tictactoe;本章只讲棋盘测试。
测试用例结构
tests/ 只有一个测试文件 testing-board.cpp,共 15 条用例,覆盖初始状态、坐标范围、成功写入、拒绝、清空与终局判定。本章给出其中四条完整用例,其余只列用例名。
初始状态
testing-board.cpp 的第一个测试核对新建棋盘的初始状态。
testing-board.cpp
/// 新建棋盘的每个棋盘格子均为空。
TEST(BoardTest, InitialBoardIsEmpty) {
const Board board;
for (int row = 0; row < tictactoe::kBoardRows; ++row) {
for (int col = 0; col < tictactoe::kBoardCols; ++col) {
EXPECT_EQ(board.GetCellType({.row = row, .col = col}), CellType::kEmpty);
}
}
}TEST 宏把测试套件名和用例名组成一个测试。EXPECT_EQ 逐个棋盘格子比较实际取值与期望的 kEmpty。任何一个棋盘格子不等,这个测试就失败,失败信息会指出是哪一个棋盘格子。测试直接包含头文件调用真正的 Board,不经过终端程序。
测试函数上方那一行注释说明这个用例验证什么。一条测试只核对一件事时,一句话就够;读者扫过 TEST 行与注释,就知道这条测试检查的是哪条行为。
成功与拒绝
测试新建棋盘还不够。Board 的放置入口有成功和拒绝两条路径,两条都要有测试。占用与非法坐标各用一条完整用例:
testing-board.cpp
/// 已占用的棋盘格子不能被第二次写入。
TEST(BoardTest, PlaceCellRejectsOccupiedCell) {
Board board;
const CellPosition origin{.row = 0, .col = 0};
ASSERT_EQ(board.PlaceCell(origin, CellType::kX), PlaceCellResult::kSuccess);
EXPECT_EQ(board.PlaceCell(origin, CellType::kO), PlaceCellResult::kOccupied);
EXPECT_EQ(board.GetCellType(origin), CellType::kX);
}
/// 负坐标和上界坐标都不能写入。
TEST(BoardTest, PlaceCellRejectsInvalidPosition) {
Board board;
EXPECT_EQ(board.PlaceCell({.row = -1, .col = 0}, CellType::kX), PlaceCellResult::kOutOfRange);
EXPECT_EQ(board.PlaceCell({.row = 0, .col = -1}, CellType::kO), PlaceCellResult::kOutOfRange);
EXPECT_EQ(board.PlaceCell({.row = tictactoe::kBoardRows, .col = 0}, CellType::kX),
PlaceCellResult::kOutOfRange);
EXPECT_EQ(board.PlaceCell({.row = 0, .col = tictactoe::kBoardCols}, CellType::kO),
PlaceCellResult::kOutOfRange);
EXPECT_EQ(board.GetCellType({.row = 0, .col = 0}), CellType::kEmpty);
}第一条用例先用 ASSERT_EQ 铺好前提:写入 X 必须返回 kSuccess,否则后面的断言没有意义。ASSERT_* 失败会立即结束当前用例。随后两次 EXPECT_EQ 核对拒绝原因是 kOccupied、原值也没被改掉。
第二条用例覆盖四个方向:行坐标取 −1 与上界,列坐标取 −1 与上界。四条 EXPECT_EQ 都要求返回 kOutOfRange,末尾一条再确认棋盘没有被这些写入改动。
异常路径
PlaceCell 用返回值报告拒绝。查询没有可返回的“失败”取值,改用异常:
testing-board.cpp
/// 非法坐标不能用于读取棋盘格子类型。
TEST(BoardTest, GetCellTypeThrowsForInvalidPosition) {
const Board board;
EXPECT_THROW((void)board.GetCellType({.row = -1, .col = 0}), std::out_of_range);
EXPECT_THROW((void)board.GetCellType({.row = 0, .col = -1}), std::out_of_range);
EXPECT_THROW((void)board.GetCellType({.row = tictactoe::kBoardRows, .col = 0}),
std::out_of_range);
EXPECT_THROW((void)board.GetCellType({.row = 0, .col = tictactoe::kBoardCols}),
std::out_of_range);
}GetCellType 在 (3, 0) 这类坐标上没有对应的棋盘格子。它的边界判断与 std::array::at 相同,越界时抛标准库的 std::out_of_range。消息先写来源,再写失败条件与实际值。另一条用例 GetCellTypeErrorMessageIncludesPosition 核对 Board::GetCellType:、row=-1 与 col=0。测试文件直接包含 <stdexcept>,才能写出 std::out_of_range。
GetCellType 带 [[nodiscard]]。测试不关心返回值,只关心异常,所以显式写成 (void),让编译器知道这次丢弃是有意的。
EXPECT_THROW 核对异常类型,异常不抛或类型不同都算失败。EXPECT_* 失败后继续执行后面的断言,ASSERT_* 失败则立即返回当前用例。完整断言列表见 Primer。
其余用例
其余十一条用例各核对一条行为:
ValidPositionIncludesBothCorners核对左上角与右下角都在坐标范围内。PlaceCellWritesEmptyCell核对成功写入后能读回同一个棋子。PlaceCellRejectsEmptyType核对kEmpty不是一次有效写入。CheckPlaceableReportsReasonWithoutPlacing核对落子校验给出拒绝原因且不改棋盘。GetCellTypeErrorMessageIncludesPosition核对越界异常的消息带函数名与行列值。IsFullOnlyAfterEveryCellPlaced核对九格放满才返回true。GetWinnerHasNoValueOnNewBoard与GetWinnerReturnsXForTopRow核对空盘无赢家、顶行成三连判 X 胜。FullBoardWithoutWinnerHasNoWinner核对满盘且无三连时棋盘已满、没有赢家;平局由对局层判定。PreviewWinProbesWithoutPlacing核对只读推演,不改原棋盘。ClearRestoresEmptyBoard核对清空后九格皆空、赢家与满盘查询回到开局,同一格可以再次写入。
测试运行
构建脚本在编译之后运行 CTest:
终端
ctest --test-dir build --output-on-failureCMake 用 gtest_discover_tests 把 15 条用例注册给 CTest。每条用例独立运行;某一条失败不会挡住其余用例,CTest 跑完再汇总失败项。测试是启动程序前的最后一道检查。
CTest 是 CMake 自带的测试运行工具。它统一运行 add_test 注册的用例。
常见错误
顺利路径之外
测试只覆盖落子成功,没有覆盖越界坐标、已占格、满盘与三连。
- 出错的原因:只走顺利路径,拒绝分支的返回值与状态不变都没有被核对。
- 正确的做法:每个公开入口至少覆盖成功与拒绝各一条。
自测问题
InitialBoardIsEmpty在什么情况下会失败?查看答案
九个棋盘格子中任何一个的取值不是
kEmpty。EXPECT_EQ比较失败时会报告是哪个棋盘格子和两个值。测试为什么不通过终端程序核对棋盘?
查看答案
测试直接包含
board.h并调用Board,不依赖终端交互。这样失败能定位到具体函数,而不是一段画面。
本章回顾
你已经能读出一条测试的结构,并说出 15 条棋盘用例覆盖什么。下一章构建与运行,配置、编译、跑 CTest 并启动程序。