如何聪明地编写测试

作者|Maxim Schepelin

Booking公司软件开发工程师

编译整理|TesterHome

以下为作者观点:

在我(作者)的职业生涯中,我多次看到团队如何开始自动化测试,当然并非所有尝试都成功。在这篇文章中,我将分享一些关于在团队中创建自动化测试文化以及塑造从零测试到不同级别的可靠测试集过程中一些技巧。

一些团队进行自动化测试的常见方式是设定一个目标,例如:“在本季度,我们将把测试覆盖率提高到X%”。

我认为这是提高质量的次优方法。因为,我们最终的目标并不是测试覆盖的行数百分比。我们的目标是,在代码库的整个生命周期内,对代码所做的新修改形成快速反馈回路

打个比方,在身材管理上,我们的目标不应该只是在这个季度锻炼身体,以迎接下一个海滩季节,我们的目标应该是在余生都保持良好的体型。

同样,我认为自动测试也是团队遵循的一套习惯,而保持新习惯的一个重要方面就是尽早发现新行为的好处,下面就是我认为应该做的事。

了解代码库的预期寿命

你的团队当前正在开发的代码库的预期寿命是多少?你期望代码在生产环境中保留多久?这些问题可能看起来与测试的讨论无关,但它们很重要。

这是因为代码库的生命周期将定义你期望发生更改的位置。根据我的经验,假设业务应用程序的寿命为5到7年是相当合理的。有时,甚至长达10年以上。现在想想10年后可能会发生什么变化:

  • 硬件代码运行于...

  • 操作系统

  • 软件所依赖的所有技术都将获得重大更新

  • 所有库和框架都将更新

  • 语言版本

  • 作业工具

  • 构建过程

  • 部署流程

上面这个列表并不完整,甚至没有涵盖由于新要求而对代码本身所做的任何更改。

你可能会看到,目前很多流行的开源项目已经将这些变化考虑在内的,因此它们正在不同的操作系统版本和不同的硬件上进行测试,作为其CI管道的一部分。这对于它们的寿命来说是完全合理的。另一个例子可能是为代码严重依赖的技术编写测试。不是因为您想测试其他人的代码,而是因为您希望在将 Kafka/Postgres/其他版本更新到下一个主要版本时拥有一个安全网。

这里的目的是让团队思考他们实际上应该测试什么。现在,让我们更接近实际测试。想象一下,你手上有了一个代码库,并且它还没测试过。你会从哪里开始编写测试呢?

识别经常变化的“热点”

从零测试过渡到测试良好的代码库需要团队付出大量努力。因此,尽快获得自动化测试的好处非常重要。

我曾见过一种反复出现的模式:一个团队设定了一个目标,要达到70%的代码行覆盖率。过了一段时间,当他们达到这个目标后,团队仍然感觉不到他们编写的测试有任何价值:上线后的缺陷率仍然很高,上线成功率仍然不变。最终,团队会对自动化测试产生怨恨……"我们已经试过了,它对我们团队不起作用"。

我理解一些人放弃的原因。他们不得不挤出时间编写测试,与产品负责人协商技术债务问题,并投入了大量精力。然而,他们并没有从这些投入中得到什么。

另一种方法是找出变更发生频率较高的地方,并开始编写涵盖这些热点的测试。请记住,自动测试的最终目标是为新变化提供快速反馈回路。因此,值得考虑变化发生的位置。幸运的是,有这样一个工具--"git-extras "软件包中的 "git effort"。下面是它的工作原理:


it effort **/*.go --above 10 path commits active days assert/assertions.go........................ 223 163 assert/assertions_test.go................... 145 108 mock/mock.go................................ 106 81 mock/mock_test.go........................... 62 54 suite/suite.go.............................. 46 37 require/require.go.......................... 45 39 assert/assertion_forward.go................. 44 38 require/require_forward.go.................. 43 37 assert/assertion_format.go.................. 36 31 suite/suite_test.go......................... 34 26 assert/doc.go............................... 19 17 assert/http_assertions.go................... 18 15 assert/forward_assertions_test.go........... 17 17 require/requirements.go..................... 16 15 assert/forward_assertions.go................ 16 16 assert/http_assertions_test.go.............. 15 9 assert/assertion_order.go................... 11 10 _codegen/main.go............................ 11 10

这是一个开源项目的例子。它显示了每个文件收到的提交次数,以及每个文件的活跃开发天数。从上面的例子中,你可能会发现更改频率并不是平均分布的。有些文件几乎每天都会更改,而有些文件每年才更改几次。

这就是为什么代码覆盖率是一个误导性指标的原因之一:它忽略了更改频率。你可能会获得很高的测试覆盖率,但如果热点仍未被发现,你将一无所获。

因此,请确保首先覆盖经常更改的测试代码。这样,你的团队就会更快地注意到自动化测试的好处。

这种策略可以帮助你选择从哪里开始——但要维持新习惯还需要做更多的工作。那么,我们来谈谈如何确保你的测试确实可以帮助你验证传入的更改。

在与几个团队合作,从没有测试到实现良好的测试自动化的过程中,我注意到,在一开始,人们认为测试是二元对立的:"我们有 X 的测试 "或 "我们没有 X 的测试"。

后来,人们开始问自己的测试有多好。同样,我认为这是对测试覆盖率指标的不良影响,因为测试覆盖率指标只能反映测试用例是否覆盖了某一行代码。但我们不要把这一点与代码执行中可能出现的逻辑分支或输入参数的变化混为一谈。

我将使用 "测试完整性 "来描述测试覆盖所有可能执行路径的程度。为了说明代码覆盖率和测试完备性之间的区别,我们来看下一个例子:

我有一个简单的函数,可以将两个数字相加:

func Sum(a, b int) int {    return a + b}

要实现该函数100%的代码覆盖率,只需编写一个调用该函数的测试即可,测试中不需要任何参数。assert.Equal(Sum(1, 2), 3)`。100% 的代码覆盖率!但这能覆盖所有可能的情况吗?

显然没有,根据输入参数的不同,可能会有更多的变化:

# these are just basic school mathassert.Equal(Sum(1, 2), 3)assert.Equal(Sum(1, 0), 1)assert.Equal(Sum(3, -1), 2)assert.Equal(Sum(-1, -1), -2)assert.Equal(Sum(-1, 0), -1)
# there are more language specific test-casesassert.Equal(Sum(math.MaxInt, 1), math.MinInt)assert.Equal(Sum(math.MinInt, -1), math.MaxInt)

因此,与其询问某件事情是否有测试,不如询问这些测试场景有多完整。


将两个数字相加的例子可能看起来有些矫揉造作,但根据研究 "简单测试可防止大多数关键故障",研究表明:

  • 几乎所有灾难性故障(92%)都是由于软件中明确提示的非致命错误处理不当造成的。

  • 大多数生产故障(77%)可以通过单元测试重现

作为工程师,我们在考虑可能出错的情况并设计测试用例以应对这些故障时,应保持适当的偏执。

结论

设定一个可衡量的目标,以了解采用自动化测试的进展情况,这可能很有诱惑力。然而,这可能会导致你达到了目标,却没有实现目标。

另一种方法是不追求行覆盖率指标,而是确保经常更改的代码具有良好的测试完整性。

这样,当团队对经常变化的代码有了一个安全网后,他们就可以专注于从自动化测试中获得真正的价值:

  • 是否可以通过增加更多测试来提高部署成功率?

  • 能否通过自动化测试而不是手动步骤重现错误?

  • 下一次对依赖项进行重大版本更新时,我们如何才能更早地发现问题?


对这些问题的回答可能不会产生容易衡量的目标,但它会为您制定更有意义的测试策略。(原文链接:https://medium.com/booking-com-development/write-tests-smarter-not-harder-fb49c7ab89fd)

你可能感兴趣的:(数据库,功能测试,自动化测试)