Go中的软件架构:可测试性

89 阅读4分钟

Go中的软件架构:可测试性

什么是可测试性?

可测试性是指通过(通常是基于执行的)测试使软件证明其故障的难易程度

还有一个叫做测试自动化金字塔的概念,在Succeeding with Agile中介绍过,用来定义我们应该做多少工作,应该实现多少测试,取决于我们要测试的那一层。

这个分层与我之前谈到领域驱动设计时讨论的分层相似。

测试自动化金字塔定义了三个层次,从上到下。UI服务单元

Testability - Test Automation Pyramid

  • UI:代表用户界面,它是最上面的一层,是测试最少的一层。
  • 服务:代表服务,如用例,在这里我们应该根据需要写尽可能多的测试,它是中等规模。
  • 单元:表示单元测试,是最大的一个,这是我们应该最关注写测试的地方。

因为在这篇文章的上下文中,我们正在构建后端API,所以我喜欢在原来的金字塔上添加一些东西。

Testability - Test Automation Pyramid

  • 手动,这就是字面上的意思,当有一些人工测试需要做的时候。
  • UI重命名为API:因为对于后端来说,公共API是我们应用程序的入口,比如HTTP端点;以及
  • 服务:这将是集成层,我们在这里与数据存储互动。

根据与我们正在建设的项目相关的一些特性,如最后期限或可用资源,通常需要做出妥协以按时交付项目,有时不实施测试就是这种妥协。

Testability - Test Automation Pyramid

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

Testability - Test Automation Pyramid

这意味着在实践中,项目进行得越久,如果一开始就不进行测试,就越难修复缺陷。

单元测试

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:定义用于表示测试的nameinputwithErr ,表示输出
    • L8-33:定义了实际的测试案例。
  • L36-52: 使用子测试来运行测试
    • L37-40:创建一个tt 的本地副本,以便在L39中的子测试中传入数值。
    • L42-45:检验测试用例输入的Validate调用是否与预期的测试用例输出相匹配。
    • L47-50:同样地,它确认返回的输出与预期的类型相匹配。

集成测试

在代码需要数据存储的情况下,有一种方法可以使用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-vcrh2non/gock.