20年后回头看,关于自动化测试心路历程的碎碎念!

2779 篇文章 4 订阅
2619 篇文章 14 订阅

以下仅代表作者观点:

本文涵盖了我(作者)从对自动化测试感觉不好,到感觉良好的旅程。

承认这一点并不容易,但过去20年来,我一直在做错误的测试。最近的一个启示,让我得以从争论中走出来,自信地认为,我的自动化测试新方法终于与我的目标一致了。

“你做错了”

关于自动化测试,或者至少是互联网上人们谈论自动化测试的方式,总让我觉得自己做错了什么。对话往往是这样的:

:我正在思索如何编写测试,我知道我想测试什么,但如何将其转化为自动化测试套件呢?

网上有人说:编写单元测试,尽可能测试最小的逻辑单元,最多一个函数。

:那有依赖关系的函数怎么办?

网上有人说:使用依赖注入编写函数,这样更容易测试,然后你就可以模拟依赖关系了。

:当函数需要读写数据库时怎么办?

网上有人说:你的单元测试应该能够在没有数据库的情况下隔离执行。

:但我要测试的大部分内容都要求我的应用程序在更广泛的系统环境中运行。

网上有人说:听起来你的应用程序有太多没用的东西了。

:是的,它有很多附加的东西,这正是它为用户提供价值的方式。

网上有人说:你做错了。

在这样的对话中,无论提问者是我还是像我一样的人,我都会感到自己的不足。

然而,我已经做了很多关于这个主题的研究。我发现几乎所有的文献(和教程)都侧重于测试的机制,而不是目标。有很多文献缺乏对测试方法的解释。

以下是一些常见的测试 "最佳实践":

  • 单元测试应测试逻辑的小单元,并应可单独执行。

  • 如果需要在测试中使用数据库或其他依赖项,则应使用模拟。

  • 测试运行时间应尽可能短。

  • 如果代码覆盖率达到100%,那就说明你做对了。

真正的问题

这些所谓的最佳实践的真正问题在于,它们的主要目标是速度和代码覆盖率。快速测试覆盖整个代码库是聪明之举,但不应该成为测试套件的首要目标。

测试软件的目的是确保其功能能够正常运行!

我逐渐意识到,编写低级单元测试通常只是测试功能实现的正确性,而不是测试功能本身,这是浪费时间。它们无法兑现自动化测试的承诺。相反,它们往往会带来虚假的安全感。

实现100%的代码覆盖率,并不能保证应用程序的功能按预期运行,也不能保证它们没有错误,更不能保证它们能够抵御边缘情况。它所能证明的,只是你编写的代码可以测试其他代码,而不一定是有效的测试。

为了说明这一点,这里有一个函数,如果提供的用户名可用且不是保留用户名,则返回 true。 

function isUsernameAvailable(string $username): bool{
    $user = getUserByUsername($username);
    $reservedUsernames = ['admin', 'test'];
    
    return $user === null && !in_array($username, $reservedUsernames);}

下面是一个测试,通过使用随机生成的用户名测试功能,代码覆盖率达到100%。

test('returns true if the username is available', function () {
    $username = (FakerFactory::create())->userName;
    expect(isUsernameAvailable($username))->toBeTrue();});

但请注意,这个测试是不够的。它测试的是功能,而不是特性!要使这项 "功能完善",我们还需要额外的测试,从而确保保留的用户名被视为不可用。

test('returns false if the username is reserved', function () {
    $username = 'admin';
    expect(isUsernameAvailable($username))->toBeFalse();});

当然,任何人都可能写出糟糕的测试(我就是活生生的证明!)。然而,问题的关键在于,代码覆盖率只是一个毫无意义的指标

下面是PHPUnit(最流行的PHP测试框架之一)建议的测试组织方式。(下载地址:https://docs.phpunit.de/en/10.3/organizing-tests.html)

图片

请注意,tests/unit 目录中的类与 src 目录中应用程序的类(和结构)如出一辙。这可以理解为,更容易测试代码的实现并确保代码覆盖率。

陷阱就在这里。

测试很难,所以人们倾向于采用更简单的方法。但这样做会导致目标不一致。

请记住,测试的目的是确保应用程序的功能按预期运行。上面的截图显示了一个(只有一个!)集成测试,恰如其分地命名为 PuttingItTogetherTest。好像应用程序类一起工作没什么意义似的。

但是,遗憾的是,在过去20年的软件开发过程中,我一直是这样组织测试套件的,我从未对我的测试套件是否与我的测试目标一致感到完全满意。

于是,我扔掉了一直使用的蓝图,从一张白纸开始。

为什么要测试和测试什么

进行测试的主要原因是建立发布信心(发布新功能的信心)和可靠性(确保功能按预期运行)。

测试应基于其目的,而不是其机制!

看到这一点后,我开始定义我的测试。

功能测试(Feature Tests)

由于大多数应用程序都包含多个功能,因此功能测试应该占自动化测试的大部分。

功能测试可以视作应用程序功能的内部文档。功能测试应具有战略性、自文档化((self-documenting))、全面性,并有助于避免错误。

在我看来,一个应用程序中,功能测试的一些例子如下:

  • 元素不变时不跟踪。

  • 元素状态改变时对其进行跟踪。

  • 元素过期时被跟踪。

  • 元素删除时被跟踪。

  • 元素的一个或多个属性发生变化时会被跟踪。

  • 元素的一个或多个字段发生变化时,跟踪该元素。

请注意,这些测试名称是如何简洁描述的。将它们放在一起阅读,你就会对应用程序的功能有所了解。你可能已经(正确地)得出结论:当元素被修改或以特定方式改变时,该应用程序会 "跟踪元素"。

集成测试(Integration Tests)

集成测试测试应用程序与第三方软件或服务之间的互操作性,考虑与其他应用程序(同一系统内)和外部应用程序接口的集成。

在集成测试中,模拟(mocking)依赖关系非常有用。例如,模拟API端点比实际向其发送请求更容易进行测试,尽管有时这样做也是可取的。

集成测试的示例包括:

  • 具有特定源的Webhook事件会触发刷新。

  • 具有特定元素的Webhook事件不会触发刷新。

集成测试阅读起来不太直观,因为它们往往是针对与之集成的软件的上下文而设计的。

接口测试(Interface Tests)

接口测试通过一个或多个界面来测试应用程序的用户体验(UX)。这可以包括图形用户界面(GUI)、命令行界面(CLI)和应用程序编程接口(API)。

接口测试是高级测试,可模拟浏览器、控制台和HTTP请求。

接口测试的示例包括:

  • 用户以未认证用户身份访问仪表板时,会被重定向到登录页面。

  • 用户以经过身份验证的用户身份访问仪表板时,会看到图表。

由于接口测试的高层次性质和直观的测试用例,它们往往易于阅读(和编写)。

端到端测试(End-to-End Tests)

端到端测试测试应用程序的目标(业务目标或其他目标)。包括检测系统更广泛部分的问题(可能是暂时的)(并可能提供后备行为)、为不同的人演示不同的行为、处理痛点并提供业务指标。

端到端测试很难实现自动化,除了人工输入和审核外,可能还需要外部测试系统。

端到端测试的例子包括:

  • 仪表板上的图表能为用户提供准确而有价值的见解。

  • 通过应用程序接口(API)提供的数据证明了我们所提供服务的价格等级是合理的。 

测试作为文档

当我们对测试进行上述分类,并给它们起一个描述性的名称时,我们最终会得到 "内部文档",它不仅描述而且还验证了应用程序的行为。这一点非常宝贵,尤其是在复杂的系统和开源软件中,可以立刻根据测试规范检查错误报告。

我已经开始为我的插件生成测试规范。(https://github.com/putyourlightson/craft-blitz)这个测试规范是一个动态文档,会随着时间的推移而增长,并且每次测试运行时都会自动重新生成。

下面是Blitz插件的测试规范,我建议你看一看。

本文档概述了Blitz插件的测试规范。

  • 功能测试

  • 缓存请求

测试请求是否可缓存以及在何种情况下可缓存。

  • 与包含URI模式相匹配的请求可缓存。

  • 带有生成标记的请求可缓存。

  • 带有无缓存参数的请求不可缓存。

  • 带标记的请求不可缓存。

  • 带有_includes路径的请求是缓存的include。

  • 包含 include 操作的请求是缓存的 include。

  • 请求的可缓存网站URI包含允许的查询字符串(当 urls 缓存为唯一页面时)。

  • 当urls缓存为同一页面时,请求的可缓存站点URI不包含查询字符串。

  • 请求的可缓存网站URI包含页面触发器。

  • 请求的可缓存网站URI可使用正则表达式。

可视化效果如下:

图片

测试实践

如何实施测试取决于您使用的代码测试框架及其提供的自动测试类型。

我的一部分“觉醒”发生在我从Codeception转向优秀的Pest PHP时。虽然这一转变极大地改善了编写测试的体验,但它并不是整个过程的关键。

以下是 Blitz 测试文件和结构在使用新测试方法之前(左侧)和之后(右侧)的对比。

图片

图注:Blitz 测试文件和结构之前(左侧)和之后(右侧)。

请注意,现在的测试不再照搬源代码结构,而是归入上述四个类别之一,并使用单个文件涵盖与每个功能/集成/接口相关的所有测试用例。

以前,集成和接口测试与其他测试放在一起,以反映源代码。现在,它们作为独立的测试文件存在,各有各的用途。

此外,每个缓存存储驱动程序都事先单独进行了测试,即使它们本质上都在测试相同的东西。取而代之的是一个 CacheStorageTest 测试文件和一个 CacheStorageDrivers 数据集文件,后者将每个驱动程序都分配给了测试。

即使采用新的测试方法,所有110个测试的执行时间也仅为4秒即可完成!

图片

关于我如何开始实施测试的细节,我将留待下次再谈,但我只想说,与你如何思考和实现测试目标相比,你所使用的代码测试框架并不那么重要。

启示

到目前为止,这听起来似乎只是观点的转变。但它的意义要深远得多。它彻底改变了测试的认识、结构、编写和评估方式。

在得到启示之前,我一直在编写单元测试--每个类一个测试文件,类中每个方法一个或多个测试--目的是覆盖尽可能多的方法。这样就形成了一个测试应用程序实现的测试套件,并努力实现代码覆盖率。

自从有了这个启示,我就开始编写上述定义的单个功能/集成/接口测试文件,每个文件都有尽可能多的测试。我不使用 "单元 "测试的概念,也不区分单元测试和功能测试(这一点我还不清楚)。相反,我的测试目标是覆盖尽可能多的功能。这导致测试套件既要测试应用程序的功能,又要努力实现功能覆盖。

这看起来似乎是一个微妙的区别,但编写鼓励功能质量保证的测试和编写增加代码覆盖率的测试,两者感觉上有天壤之别。然而,我花了20年的时间才区分开来。

我不知道我做得对不对,但我可以肯定地说,我做得没有那么错!

不管 "互联网上的某个人 "会怎么说,我觉得自己更接近于破解如何在软件应用程序中建立可靠发布的信心和可靠性。

更广泛的测试应用

这种测试方法的最大优点,也是我认为它行之有效的另一个原因,就是它可以应用于任何系统(不仅仅是软件)。让我们以一辆(非自动驾驶)汽车为例。

功能测试:

  • 踩下加速踏板时加速。

  • 踩下制动踏板时减速。

  • 当开关处于 "开 "的位置时,加热驾驶员座椅。

集成测试:

  • 连接音乐应用程序时,通过扬声器播放音频。

  • 导航至目的地时,播报逐向导航信息。

接口测试:

  • 在驾驶过程中可以轻松打开或关闭座椅加热开关。

  • 在驾驶过程中,导航应用程序始终可视可听。

端到端测试:

  • 驾驶员座椅加热至舒适温度。

  • 逐向导航播报时,音频/音乐音量可适当调低。

通过阅读这些测试,我们可以清楚地看到,这个装置的功能包括加速、减速和为驾驶员的臀部保暖。

它还可以播放音乐,并使用第三方应用程序提供转弯导航。驾驶过程中可以安全地使用座椅加热器和导航应用程序。最后,驾驶员的屁股永远不会太热,也不会因为音乐声太大而错过转弯。

我相信你可以把同样的测试方法应用到任何家用物品上:椅子、吸尘器或食品加工机。实际上,我发现这是一个有趣的小练习!

最后:下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保100%免费】

软件测试面试文档

我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值