Go 补强第三天:给 TaskStore 写单元测试

背景

前两天已经完成了两个阶段:

  • 第一天:用 structslicemaperror 串起基础数据操作。
  • 第二天:把散函数封装成 TaskStore,形成一个小模块。

第三天的目标是继续往真实项目靠近:把代码放进独立 package,并用 go test 验证它。

这一步的意义是:代码不只是"能跑",还要"能被验证"。

1. 从 main 程序拆成 package

这次创建了一个独立目录:

text 复制代码
demo/taskstore/

并初始化模块:

powershell 复制代码
go mod init taskstore

一开始遇到了一个问题:

text 复制代码
go: error reading go.mod: missing module declaration

原因是 go.mod 文件存在,但内容是空的,没有模块声明。

修复方式:

powershell 复制代码
go mod edit -module=taskstore

修复后 go.mod 至少应该包含:

go 复制代码
module taskstore

然后运行:

powershell 复制代码
go test

输出:

text 复制代码
?       taskstore       [no test files]

这个输出说明:Go 已经识别当前目录是一个 package,只是还没有测试文件。

2. 第一个测试:Create 成功

测试文件命名为:

text 复制代码
store_test.go

测试函数必须符合:

go 复制代码
func TestXxx(t *testing.T)

例如:

go 复制代码
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)
    }
}

这里第一次理解了测试的基本写法:

text 复制代码
准备数据
执行操作
检查结果

也就是常说的 Arrange / Act / Assert。

3. 不只测成功,也要测失败

Create 不应该只测正常标题,还要测空标题。

go 复制代码
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)
    }
}

这一步的重点是:

text 复制代码
单元测试要覆盖成功路径,也要覆盖错误路径。

否则代码看起来能用,但边界条件可能没有被验证。

4. 测试 Get

Get 的成功路径需要先准备数据:

go 复制代码
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)
    }
}

不存在的情况也要测:

go 复制代码
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

修改类方法不能只看返回值,还要确认状态真的变了。

go 复制代码
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")
    }
}

不存在的任务也要测:

go 复制代码
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

删除操作的测试重点是:删除后再查一次。

go 复制代码
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)
    }
}

删除不存在任务:

go 复制代码
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)
    }
}

这里的理解是:

text 复制代码
Delete 返回 nil 只能说明方法没有报错。
再 Get 一次,才能确认数据真的被删除了。

7. 测试 List

List 测试先只验证数量:

go 复制代码
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))
    }
}

这里刻意没有测试:

go 复制代码
tasks[0].Title == "任务 1"

因为 Store 内部使用 map 保存任务,而 Go 不保证 map 的遍历顺序。

所以 List() 返回的顺序不能依赖。如果以后业务需要稳定顺序,就应该在 List 里显式排序。

8. 今天的测试覆盖

今天最终覆盖了这些场景:

text 复制代码
Create 成功
Create 空标题失败
Get 成功
Get 不存在失败
MarkDone 成功
MarkDone 不存在失败
Delete 成功
Delete 不存在失败
List 返回数量

运行:

powershell 复制代码
go test

输出:

text 复制代码
PASS
ok      taskstore

9. 今天的几个关键理解

TestXxx 是 Go 的测试识别规则

Go 会自动识别形如下面的函数:

go 复制代码
func TestStoreCreate(t *testing.T)

函数名必须以 Test 开头,参数必须是 *testing.T

t.Fatalf 会让测试失败并停止当前测试

fmt.Println 只是打印内容,不会告诉测试框架失败。

而:

go 复制代码
t.Fatalf("expected ID 1, got %d", task.ID)

会标记测试失败,并停止当前测试函数。

测试要验证行为结果

比如 Delete,不能只看 Delete 有没有返回错误,还要再 Get 一次确认数据确实不存在。

不要依赖 map 遍历顺序

因为 Go 的 map 遍历顺序不固定,所以 List 测试只验证数量,不验证顺序。

今天的收获

第三天最大的收获是:开始从"写代码能跑"转向"写代码能被验证"。

现在这个小模块已经不只是有功能,还能通过测试保护基本行为。

下一步

下一篇可以继续做:

text 复制代码
把测试改成表驱动测试,并学习 errors.Is 的错误判断方式。

这样可以减少重复测试代码,也更接近真实 Go 项目的测试写法。

相关推荐
范闲1 小时前
第二章:Octo v0.1.1:从「能用」到「好用」——终端 AI 聊天体验的全面升级
人工智能·后端
贰先生1 小时前
Xiuno BBS 审计之问题03:附件上传高危风险
后端
Ai拆代码的曹操1 小时前
记一次 K8s Pod Pending 排查:两层坑叠加,从资源到存储的完整复盘
后端
贰先生1 小时前
Xiuno BBS 审计之问题04:Cookie 缺少 HttpOnly/Secure/SameSite 安全属性
后端
贰先生1 小时前
Xiuno BBS 审计之问题06:硬编码 auth_key 加密密钥
后端
贰先生1 小时前
Xiuno BBS 审计之问题02:后台用户搜索可能造成严重性能 DoS
后端
沈七1 小时前
Go 补强第四天:把重复单测改成表驱动测试
后端
范闲1 小时前
第一章:从零构建终端 AI 助手 —— Octo 项目实战
后端·llm
Gopher_HBo2 小时前
docker-run-调用链全图解
后端