背景
前两天已经完成了两个阶段:
- 第一天:用
struct、slice、map、error串起基础数据操作。 - 第二天:把散函数封装成
TaskStore,形成一个小模块。
第三天的目标是继续往真实项目靠近:把代码放进独立 package,并用 go test 验证它。
这一步的意义是:代码不只是“能跑”,还要“能被验证”。
1. 从 main 程序拆成 package
这次创建了一个独立目录:
demo/taskstore/
并初始化模块:
go mod init taskstore
一开始遇到了一个问题:
go: error reading go.mod: missing module declaration
原因是 go.mod 文件存在,但内容是空的,没有模块声明。
修复方式:
go mod edit -module=taskstore
修复后 go.mod 至少应该包含:
module taskstore
然后运行:
go test
输出:
? taskstore [no test files]
这个输出说明:Go 已经识别当前目录是一个 package,只是还没有测试文件。
2. 第一个测试:Create 成功
测试文件命名为:
store_test.go
测试函数必须符合:
func TestXxx(t *testing.T)
例如:
func TestStoreCreate(t *testing.T) {
store := NewStore()
task, err := store.Create("学习测试", 3)
if err != nil {
t.Fatalf("Create returned error: %v", err)
}
if task.ID != 1 {
t.Fatalf("expected ID 1, got %d", task.ID)
}
if task.Title != "学习测试" {
t.Fatalf("expected title 学习测试, got %s", task.Title)
}
if task.Done {
t.Fatalf("expected Done false, got true")
}
if task.Priority != 3 {
t.Fatalf("expected priority 3, got %d", task.Priority)
}
}
这里第一次理解了测试的基本写法:
准备数据
执行操作
检查结果
也就是常说的 Arrange / Act / Assert。
3. 不只测成功,也要测失败
Create 不应该只测正常标题,还要测空标题。
func TestStoreCreateEmptyTitle(t *testing.T) {
store := NewStore()
_, err := store.Create(" ", 3)
if err == nil {
t.Fatalf("expected error, got nil")
}
if err != ErrEmptyTitle {
t.Fatalf("expected ErrEmptyTitle, got %v", err)
}
}
这一步的重点是:
单元测试要覆盖成功路径,也要覆盖错误路径。
否则代码看起来能用,但边界条件可能没有被验证。
4. 测试 Get
Get 的成功路径需要先准备数据:
func TestStoreGet(t *testing.T) {
store := NewStore()
created, err := store.Create("学习 Get", 2)
if err != nil {
t.Fatalf("Create returned error: %v", err)
}
got, err := store.Get(created.ID)
if err != nil {
t.Fatalf("Get returned error: %v", err)
}
if got.ID != created.ID {
t.Fatalf("expected ID %d, got %d", created.ID, got.ID)
}
if got.Title != created.Title {
t.Fatalf("expected title %s, got %s", created.Title, got.Title)
}
if got.Priority != created.Priority {
t.Fatalf("expected priority %d, got %d", created.Priority, got.Priority)
}
}
不存在的情况也要测:
func TestStoreGetNotFound(t *testing.T) {
store := NewStore()
_, err := store.Get(99)
if err == nil {
t.Fatalf("expected error, got nil")
}
if err != ErrTaskNotFound {
t.Fatalf("expected ErrTaskNotFound, got %v", err)
}
}
5. 测试 MarkDone
修改类方法不能只看返回值,还要确认状态真的变了。
func TestStoreMarkDone(t *testing.T) {
store := NewStore()
created, err := store.Create("学习 MarkDone", 1)
if err != nil {
t.Fatalf("Create returned error: %v", err)
}
err = store.MarkDone(created.ID)
if err != nil {
t.Fatalf("MarkDone returned error: %v", err)
}
got, err := store.Get(created.ID)
if err != nil {
t.Fatalf("Get returned error: %v", err)
}
if !got.Done {
t.Fatalf("expected Done true, got false")
}
}
不存在的任务也要测:
func TestStoreMarkDoneNotFound(t *testing.T) {
store := NewStore()
err := store.MarkDone(99)
if err == nil {
t.Fatalf("expected error, got nil")
}
if err != ErrTaskNotFound {
t.Fatalf("expected ErrTaskNotFound, got %v", err)
}
}
6. 测试 Delete
删除操作的测试重点是:删除后再查一次。
func TestStoreDelete(t *testing.T) {
store := NewStore()
created, err := store.Create("学习 Delete", 1)
if err != nil {
t.Fatalf("Create returned error: %v", err)
}
err = store.Delete(created.ID)
if err != nil {
t.Fatalf("Delete returned error: %v", err)
}
_, err = store.Get(created.ID)
if err == nil {
t.Fatalf("expected error after delete, got nil")
}
if err != ErrTaskNotFound {
t.Fatalf("expected ErrTaskNotFound, got %v", err)
}
}
删除不存在任务:
func TestStoreDeleteNotFound(t *testing.T) {
store := NewStore()
err := store.Delete(99)
if err == nil {
t.Fatalf("expected error, got nil")
}
if err != ErrTaskNotFound {
t.Fatalf("expected ErrTaskNotFound, got %v", err)
}
}
这里的理解是:
Delete 返回 nil 只能说明方法没有报错。
再 Get 一次,才能确认数据真的被删除了。
7. 测试 List
List 测试先只验证数量:
func TestStoreList(t *testing.T) {
store := NewStore()
_, err := store.Create("任务 1", 1)
if err != nil {
t.Fatalf("Create returned error: %v", err)
}
_, err = store.Create("任务 2", 2)
if err != nil {
t.Fatalf("Create returned error: %v", err)
}
tasks := store.List()
if len(tasks) != 2 {
t.Fatalf("expected 2 tasks, got %d", len(tasks))
}
}
这里刻意没有测试:
tasks[0].Title == "任务 1"
因为 Store 内部使用 map 保存任务,而 Go 不保证 map 的遍历顺序。
所以 List() 返回的顺序不能依赖。如果以后业务需要稳定顺序,就应该在 List 里显式排序。
8. 今天的测试覆盖
今天最终覆盖了这些场景:
Create 成功
Create 空标题失败
Get 成功
Get 不存在失败
MarkDone 成功
MarkDone 不存在失败
Delete 成功
Delete 不存在失败
List 返回数量
运行:
go test
输出:
PASS
ok taskstore
9. 今天的几个关键理解
TestXxx 是 Go 的测试识别规则
Go 会自动识别形如下面的函数:
func TestStoreCreate(t *testing.T)
函数名必须以 Test 开头,参数必须是 *testing.T。
t.Fatalf 会让测试失败并停止当前测试
fmt.Println 只是打印内容,不会告诉测试框架失败。
而:
t.Fatalf("expected ID 1, got %d", task.ID)
会标记测试失败,并停止当前测试函数。
测试要验证行为结果
比如 Delete,不能只看 Delete 有没有返回错误,还要再 Get 一次确认数据确实不存在。
不要依赖 map 遍历顺序
因为 Go 的 map 遍历顺序不固定,所以 List 测试只验证数量,不验证顺序。
今天的收获
第三天最大的收获是:开始从“写代码能跑”转向“写代码能被验证”。
现在这个小模块已经不只是有功能,还能通过测试保护基本行为。
下一步
下一篇可以继续做:
把测试改成表驱动测试,并学习 errors.Is 的错误判断方式。
这样可以减少重复测试代码,也更接近真实 Go 项目的测试写法。