Go中的软件架构:可测试性
什么是可测试性?
可测试性是指通过(通常是基于执行的)测试使软件证明其故障的难易程度。
还有一个叫做测试自动化金字塔的概念,在Succeeding with Agile中介绍过,用来定义我们应该做多少工作,应该实现多少测试,取决于我们要测试的那一层。
这个分层与我之前谈到领域驱动设计时讨论的分层相似。
测试自动化金字塔定义了三个层次,从上到下。UI、服务和单元。

- UI:代表用户界面,它是最上面的一层,是测试最少的一层。
- 服务:代表服务,如用例,在这里我们应该根据需要写尽可能多的测试,它是中等规模。
- 单元:表示单元测试,是最大的一个,这是我们应该最关注写测试的地方。
因为在这篇文章的上下文中,我们正在构建后端API,所以我喜欢在原来的金字塔上添加一些东西。

- 手动,这就是字面上的意思,当有一些人工测试需要做的时候。
- 将UI重命名为API:因为对于后端来说,公共API是我们应用程序的入口,比如HTTP端点;以及
- 服务:这将是集成层,我们在这里与数据存储互动。
根据与我们正在建设的项目相关的一些特性,如最后期限或可用资源,通常需要做出妥协以按时交付项目,有时不实施测试就是这种妥协。

如果我们参考美国国家标准和技术研究所发表的《软件测试基础设施不足的经济影响》一文,就会发现,在做出这样的决定时,我们应该非常小心,因为测试并不被认为是编写软件的基本部分,如果我们参考这篇文章,就会发现,修复缺陷的相对成本取决于需要修复的软件开发阶段。

这意味着在实践中,项目进行得越久,如果一开始就不进行测试,就越难修复缺陷。
单元测试
Go中的简易测试包括使用表驱动的测试,例如,使用优先级类型,我们有如下内容。
1func TestPriority_Validate(t *testing.T) {
2 t.Parallel()
3
4 tests := []struct {
5 name string
6 input internal.Priority
7 withErr bool
8 }{
9 {
10 "OK: PriorityNone",
11 internal.PriorityNone,
12 false,
13 },
14 {
15 "OK: PriorityLow",
16 internal.PriorityLow,
17 false,
18 },
19 {
20 "OK: PriorityMedium",
21 internal.PriorityMedium,
22 false,
23 },
24 {
25 "OK: PriorityHigh",
26 internal.PriorityHigh,
27 false,
28 },
29 {
30 "ERR: unknown value",
31 internal.Priority(-1),
32 true,
33 },
34 }
35
36 for _, tt := range tests {
37 tt := tt
38
39 t.Run(tt.name, func(t *testing.T) {
40 t.Parallel()
41
42 actualErr := tt.input.Validate()
43 if (actualErr != nil) != tt.withErr {
44 t.Fatalf("expected error %t, got %s", tt.withErr, actualErr)
45 }
46
47 var ierr *internal.Error
48 if tt.withErr && !errors.As(actualErr, &ierr) {
49 t.Fatalf("expected %T error, got %T", ierr, actualErr)
50 }
51 })
52 }
53}
- L4-34:定义一个测试用例的片断。
- L5-7:定义用于表示测试的
name,input,withErr,表示输出。 - L8-33:定义了实际的测试案例。
- L5-7:定义用于表示测试的
- L36-52: 使用子测试来运行测试
- L37-40:创建一个
tt的本地副本,以便在L39中的子测试中传入数值。 - L42-45:检验测试用例输入的Validate调用是否与预期的测试用例输出相匹配。
- L47-50:同样地,它确认返回的输出与预期的类型相匹配。
- L37-40:创建一个
集成测试
在代码需要数据存储的情况下,有一种方法可以使用docker来实现,我更喜欢使用ory/dockertest 包,例如对于postgresl.Task如果我们想测试Create方法,可以实现类似下面的方法。
30func TestTask_Create(t *testing.T) {
31 t.Parallel()
32
33 t.Run("Create: OK", func(t *testing.T) {
34 t.Parallel()
35
36 task, err := postgresql.NewTask(newDB(t)).Create(context.Background(),
37 internal.CreateParams{
38 Description: "test",
39 Priority: internal.PriorityNone,
40 Dates: internal.Dates{},
41 })
42 if err != nil {
43 t.Fatalf("expected no error, got %s", err)
44 }
45
46 if task.ID == "" {
47 t.Fatalf("expected valid record, got empty value")
48 }
49 })
50
51 //...
如果你注意到这不是遵循表驱动的方法,这是故意的,因为在这样的情况下,用于与数据存储交互的代码已经有了具体的目标(它使用存储库模式),所以除了实际执行与数据存储通信所需的相应调用外,没有其他逻辑,在这个例子中就是执行SQL INSERT命令。
总结
Testability 作为一个质量属性对于任何软件项目来说都是最基本的,特别是那些要持续较长时间的项目,感谢Go的测试支持已经包含在标准库中,只需要几个包就可以提高测试体验,比如 ory/dockertest, dnaeon/go-vcr和 h2non/gock.