|
|
@@ -0,0 +1,184 @@
|
|
|
+# 第 14 课:单元测试专题
|
|
|
+
|
|
|
+## 学习顺序
|
|
|
+
|
|
|
+1. 阅读 [main.go](./main.go),先理解被测的 add 和 parseScore。
|
|
|
+2. 在 [main_test.go](./main_test.go) 中只敲 TestAdd,执行一次测试。
|
|
|
+3. 再敲 TestAddTable,把结构体切片、循环、匿名函数串起来。
|
|
|
+4. 最后敲 TestParseScore,学习验证错误和边界情况。
|
|
|
+
|
|
|
+共享根目录的 `go.mod`,仅使用标准库,无需安装测试框架。本课暂不使用并行测试、mock、基准测试或模糊测试。
|
|
|
+
|
|
|
+## 1. 测试如何启动
|
|
|
+
|
|
|
+```go
|
|
|
+package main
|
|
|
+
|
|
|
+import "testing"
|
|
|
+
|
|
|
+func TestAdd(t *testing.T) {
|
|
|
+ got := add(2, 3)
|
|
|
+ want := 5
|
|
|
+ if got != want {
|
|
|
+ t.Fatalf("实际=%d,期望=%d", got, want)
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- 测试文件以 `_test.go` 结尾。
|
|
|
+- 普通测试函数形如 `func TestXxx(t *testing.T)`,Test 后的名字不能以小写字母开头。本课统一使用 TestAdd 这样的名称。
|
|
|
+- `testing` 是标准库包,T 是它提供的类型,`*testing.T` 是指针类型,t 是参数名。
|
|
|
+- go test 负责生成测试入口、调用这些函数、传入 t;不需要在 main 中调用测试。
|
|
|
+- 同包测试使用 `package main`,可以直接访问本课未导出的 add 和 parseScore。
|
|
|
+- 测试文件不会被普通 go build 编译进应用程序。
|
|
|
+
|
|
|
+一次测试的核心是:准备输入 → 调用函数 → 比较实际与期望 → 不一致就报告失败。got、want、tt 都只是惯用变量名,不是关键字。
|
|
|
+
|
|
|
+## 2. Logf、Errorf 与 Fatalf
|
|
|
+
|
|
|
+| 方法 | 标记失败 | 是否继续当前测试 |
|
|
|
+| --- | --- | --- |
|
|
|
+| `t.Logf(...)` | 否,只记录信息 | 继续 |
|
|
|
+| `t.Errorf(...)` | 是 | 继续 |
|
|
|
+| `t.Fatalf(...)` | 是 | 停止当前测试 |
|
|
|
+
|
|
|
+它们支持格式字符串。测试正常执行完且未标记失败,就是通过,不需要额外调用成功方法。
|
|
|
+
|
|
|
+Fatalf 在子测试中只停止当前子测试的执行,其他用例通常仍会运行;不要把它理解为立即结束整个测试进程。普通 `return` 也能结束当前测试函数,但不会自动把它标记为失败。
|
|
|
+
|
|
|
+不要在测试中仅用 fmt.Println 输出“失败”来代替 t.Errorf / t.Fatalf,否则 go test 可能仍报告通过。
|
|
|
+
|
|
|
+## 3. 表驱动测试
|
|
|
+
|
|
|
+```go
|
|
|
+tests := []struct {
|
|
|
+ name string
|
|
|
+ a int
|
|
|
+ b int
|
|
|
+ want int
|
|
|
+}{
|
|
|
+ {name: "positive", a: 2, b: 3, want: 5},
|
|
|
+ {name: "zero", a: 0, b: 0, want: 0},
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+这是第八课学过的“结构体切片”,每条记录表示一组输入与期望结果,不是特殊测试语法。
|
|
|
+
|
|
|
+```go
|
|
|
+for _, tt := range tests {
|
|
|
+ t.Run(tt.name, func(t *testing.T) {
|
|
|
+ got := add(tt.a, tt.b)
|
|
|
+ if got != tt.want {
|
|
|
+ t.Errorf("实际=%d,期望=%d", got, tt.want)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+按层理解:
|
|
|
+
|
|
|
+1. range 每次取出一条测试记录 tt。
|
|
|
+2. 外层 t.Run 创建有名字的子测试。
|
|
|
+3. 第二个参数是匿名函数,由框架调用;不在末尾加自行调用的 `()`。
|
|
|
+4. 内层 t 是子测试自己的管理器,遮蔽外层同名参数。
|
|
|
+5. 匿名函数使用外层 tt,本质上是上节课学习的闭包。
|
|
|
+6. 最后的 `})` 分别关闭匿名函数体和 Run 的调用参数列表。
|
|
|
+
|
|
|
+本课没有调用 t.Parallel,子测试回调结束后才继续下一次循环,先按顺序理解。并发执行以及旧版本 Go 循环变量捕获的差异留到后续,不要随意添加并行语句。
|
|
|
+
|
|
|
+## 4. 验证错误与边界
|
|
|
+
|
|
|
+TestParseScore 的 wantErr 表示“期望本次调用发生错误”,不是错误文本本身。
|
|
|
+
|
|
|
+```go
|
|
|
+got, err := parseScore(tt.input)
|
|
|
+hasErr := err != nil
|
|
|
+if hasErr != tt.wantErr {
|
|
|
+ t.Fatalf("错误=%v,期望发生错误=%t", err, tt.wantErr)
|
|
|
+}
|
|
|
+if tt.wantErr {
|
|
|
+ return
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+先比较是否发生错误与预期是否一致:
|
|
|
+
|
|
|
+- 期望成功却失败:测试失败。
|
|
|
+- 期望失败却成功:测试失败。
|
|
|
+- 确实发生预期错误:本用例通过错误检查,结束,不再使用无效结果。
|
|
|
+- 确实成功:继续核对结果数值。
|
|
|
+
|
|
|
+本例覆盖 0、100 两个合法边界,以及 -1、101、非整数文本和空文本。错误只检查是否存在,不逐字匹配标准库错误信息。它不验证错误的具体分类;更精细的错误断言留到错误进阶。
|
|
|
+
|
|
|
+## 5. 测试命令
|
|
|
+
|
|
|
+以下命令在**仓库根目录**执行:
|
|
|
+
|
|
|
+```powershell
|
|
|
+# 运行本课所有测试,显示每个用例和日志
|
|
|
+go test -v ./lessons/14_testing
|
|
|
+
|
|
|
+# 强制重新执行,不复用成功的测试缓存
|
|
|
+go test -count=1 -v ./lessons/14_testing
|
|
|
+
|
|
|
+# 只运行最简单的 TestAdd,不匹配 TestAddTable
|
|
|
+go test -v ./lessons/14_testing -run '^TestAdd$'
|
|
|
+
|
|
|
+# 只运行一个指定子测试
|
|
|
+go test -v ./lessons/14_testing -run '^TestParseScore$/^upper_bound$'
|
|
|
+
|
|
|
+# 查看本课语句覆盖率
|
|
|
+go test -cover ./lessons/14_testing
|
|
|
+
|
|
|
+# 运行全仓库测试
|
|
|
+go test ./...
|
|
|
+```
|
|
|
+
|
|
|
+-run 接收正则表达式,不是普通字符串包含查找;`^`、`$` 分别约束开头与结尾。子测试用 `/` 分层筛选。
|
|
|
+
|
|
|
+显示 `(cached)` 表示复用了之前的成功测试结果,不是本次重新执行。覆盖率高不代表逻辑一定正确;本课只测试业务函数,没有覆盖 main 的终端展示,因此总覆盖率不会是 100%。
|
|
|
+
|
|
|
+## 6. 编译普通程序与编译测试程序
|
|
|
+
|
|
|
+普通程序仍可以编译运行,在仓库根目录执行:
|
|
|
+
|
|
|
+```powershell
|
|
|
+New-Item -ItemType Directory -Force bin/14_testing | Out-Null
|
|
|
+go build -o ./bin/14_testing/testing_demo.exe ./lessons/14_testing
|
|
|
+.\bin\14_testing\testing_demo.exe
|
|
|
+```
|
|
|
+
|
|
|
+普通程序预期输出:
|
|
|
+
|
|
|
+```text
|
|
|
+2 + 3 = 5
|
|
|
+转换后的成绩:85
|
|
|
+运行自动测试:go test -v ./lessons/14_testing(在仓库根目录执行)
|
|
|
+```
|
|
|
+
|
|
|
+也可以用 `go run ./lessons/14_testing`,但它只运行 main,不运行 `_test.go` 中的测试。
|
|
|
+
|
|
|
+go test 本身也需要先编译。若想单独保留测试可执行文件:
|
|
|
+
|
|
|
+```powershell
|
|
|
+go test -c -o ./bin/14_testing/lesson14.test.exe ./lessons/14_testing
|
|
|
+.\bin\14_testing\lesson14.test.exe '-test.v'
|
|
|
+```
|
|
|
+
|
|
|
+直接运行测试产物时使用 `-test.v` 等测试二进制参数,而不是 go test 的 `-v`。在 PowerShell 中给带点的参数加引号,确保原样传入。修改源码或测试后需要重新编译。当前目录的 bin 已在前面的命令中创建。
|
|
|
+
|
|
|
+## 动手练习
|
|
|
+
|
|
|
+- [ ] 只运行 TestAdd,找到 Logf 输出。
|
|
|
+- [ ] 把 want 从 5 改成 6,观察 FAIL 和报错文件行号,再恢复。
|
|
|
+- [ ] 将 Fatalf 临时换成 Errorf,并在其后加 Logf,比较是否继续执行;不要在 Fatalf 后写不可达的代码。
|
|
|
+- [ ] 在加法测试表里增加一条自己的输入。
|
|
|
+- [ ] 给成绩表添加 `" 85 "`,期望错误,因为 Atoi 不会自动去除空白。
|
|
|
+- [ ] 只运行 upper_bound 子测试。
|
|
|
+- [ ] 全部恢复为正确测试,执行 `go test -count=1 ./...`,确认通过。
|
|
|
+
|
|
|
+本课测试代码应直接放在本课目录。若另建 practice 子目录,需要把被测函数和对应测试一起放进去,再执行 `go test -v ./lessons/14_testing/practice`,不能只复制测试却指望自动访问父目录函数。
|
|
|
+
|
|
|
+下一课学习包与模块,把可复用函数拆到自定义包中并正确导入。
|
|
|
+
|
|
|
+[上一课:函数进阶](../13_function_values/README.md) · [学习总览](../../README.md)
|