我们究竟在测试什么?

文摘   2024-10-23 07:03   四川  

单元测试简单的示例都以一个计算器为例开始,那么今天我们就来演示一下。

public int add(int x, int y) { return x + y;}

你认为我们需要多少个测试来覆盖这段代码?

“覆盖率”这个概念通常比较模糊。人们常说的“足够多的测试”到底是指多少个测试呢?

我在工作中经常问这个问题(当然不仅仅是单元测试)。我得到各种各样的答案,比如1和1,2和2,大数字和负数,这些都是很好的用例,通常会找出5至15个合适的用例。


现在想想看:这些真的是好的测试吗?我们真正在测试什么,这些用例都是有用的吗?

这并不让人感到意外。比如:

return 1+1; 

真正执行计算的代码深藏在“+”运算符内部。实际上,它位于隐式赋值运算符“=”和一些内存操作中。

当我们将两个非常大的数字相加时会发生什么?会引发一个异常。该异常由……抛出。

我们也可以为这些情况编写测试,检查是否在较大的值时抛出异常。

简单的东西反而测试起来复杂

回到最初的问题:我们需要多少测试覆盖这个方法才感到放心呢?

可以肯定的是,测试肯定是有用的。它们会告诉我们预期的行为是否发生了变化。这些测试何时会失败?有几个可能。

在第一个选项中,如果我们不小心替换了操作符为“/”,测试用例会告诉我们会执行错误。例如,4+2,预案来预期6,但是实际2,就会得到不同的结果。

public int add(int x, int y) { return x / y;}

还有哪些无法通过测试?

也许这是操作系统的一个漏洞?但这些操作系统每天都被全球数百万人使用。如果存在漏洞,我们很可能会听说的。

实际上,这些测试将永远通过。但这并不是好的测试。它们会让我们产生虚假的自信,认为代码运行正常。我们没有见过失败的测试。那么,这对我们其他的测试又会产生怎样的影响呢?对某些测试的信任度较低意味着整体信任度较低。

让我们回到最初的问题。我们需要多少测试来覆盖这段代码?

也许答案是“没有”,因为如果代码很简单,我们可能就不需要围绕它编写测试。当有人进入代码并对其进行更改,使其变得更加复杂时,他们就会添加测试来防止他们引入的复杂性。

如果我是在使用TDD(Test-Driven Development)进行开发,情况会怎样呢?我会先编写测试代码,然后再编写代码本身。这意味着我最终可能只会有一个测试用例。

相同的代码,不同的Package

现在,相同的代码不再以函数运行,而是在API内部运行。

这是基于Spring的实现,用于通过POST请求添加两个数字。

@PostMapping("/add") public CalculationResult add(@RequestBody CalcRequest request) { int sum = request.getNumber1() + request.getNumber2();return new CalculationResult(sum);}

API测试不仅会运行我们的代码,还会运行大量Spring代码。包括接收请求、返回响应、将JSON转换为对象等。更不用提那些未处理的异常了。

这一次,可能会有比原来代码多得多的其他代码导致测试失败,也可能是因为配置问题导致失败。比如,如果服务器没有运行,那么测试就会失败,但这与代码无关。

所以问题不在于“覆盖率”。而是我们希望从测试中学到什么。

如果我们只关心自己的代码,就会回到之前的答案。但这次是有代价的。我们假设API测试的任何失败都与代码变更无关,因此不需要调试或重试。可以忽略失败。只有当测试通过时,它们才具有价值。

另一个选择是进行测试以验证所有内容,包括我们的代码。如果测试失败,就意味着某个地方出了问题,我们需要花时间进行调试、查看日志并修复配置,但这些工作可能毫无价值。

在进行测试时,我们需要了解我们正在测试的内容、什么会使测试通过以及什么会使测试失败。

我们的测试旨在为我们提供信息。如果我们在编写测试之前没有考虑这一点,我们就会编写一些没用的测试,并在它们上面浪费大量宝贵的时间。

软件质量保障
所寫即所思|一个阿里质量人对测试技术的思考。
 最新文章