我希望拥有的测试介绍
既然有很多话题我早就应该学,我竟然花了这么长时间才写出另一篇“我希望我写过的引言”,这着实让我吃惊。这次我要讲的是一个非常重要、却又常常被忽视的话题,而且说实话,除了那个纽约的漆黑夜晚,我没有带弹弓或皮凉鞋,而是走进了一条黑暗的小巷,这对我来说是最痛苦的。
当然,我指的是测试。
任何经验丰富的程序员或 Maury 节目的嘉宾都会告诉你,运行良好的测试从长远来看能帮你避免严重的问题。然而,它们很容易被忽视——在我编程的最初几年,我很容易(可以理解地)认为我的程序一旦完成就能一直运行良好。但后来我才明白,在专业的编程岗位上,这种情况很少发生。
这就是测试的作用所在。这篇文章是对测试要点的高级概述,内容包括:
- 不同类型的测试
- 为什么测试很重要
- 测试经验规则
这篇文章是写给我几年前的程序员的(某种程度上,现在依然如此)——我了解代码测试,但不知道其细节,也不知道它为何重要。希望你读完之后能改变想法,或者至少了解得足够多,让身边的人知道你已经改变了。
虽然我更喜欢第一种选择,但作家不能挑剔。那就开始吧!
不同类型的测试
让我们首先对不同类型的测试进行概述,并尽可能地添加具体示例。
单元测试
单元测试是对程序最小部分进行的最简单测试。它们通常针对函数或对象,确保它们在特定输入下返回预期值。
假设你有一个函数,用于将两个数字相加。测试会确保如果你输入 2 加 2,函数会返回 4。它不会在不同的上下文中测试它,例如作为计算器组件的一部分。单元测试会将功能独立出来,并确保它们在任何上下文中都能正常工作。
单元测试可以确保你的应用基础保持强劲。它帮助我避免了许多徒劳无功的调试,因为我知道应用的核心没有问题,所以我很可能就是按照我之前的方式使用这些核心功能。
这是我为 Ember 中的性格测试编写的单元测试示例。它测试一项管理测试特定功能(例如回答问题和跟踪答案)的服务。
test('it knows when you\'re finished', function(assert) {
let quiz = this.owner.lookup('service:quiz'),
quiz_length = quiz.get('length');
assert.equal(quiz.get('completed'), false, 'the completed property starts as false');
assert.equal(quiz.get('winner'), '', 'it starts with no winning result');
for (let i = 1; i < quiz_length; i++) {
quiz.selectAnswer(i, 'espeon');
}
assert.equal(quiz.get('completed'), true, 'the completed property becomes true');
assert.equal(quiz.get('winner'), 'espeon', 'it learns the winning result');
clearAllCookies();
});
我所做的只是确保X 属性能够正确返回 Y 值。在本例中,它用于服务的completed
和winner
属性。
另一个例子是我编写的一个简单的 Ruby 程序,用于给自己发送动漫壁纸。这个单元测试接收一个对象,该对象负责查找图片的 URL 并检查结果。
describe WallpaperUrlQuery do
let(:query) { WallpaperUrlQuery.new() }
it 'should return an image url' do
image = query.random_image
expect(image).to be_a(String)
expect(image).to include(WallpaperUrlQuery::BASE_URL)
expect(image).to include('.jpg').or include('.png')
end
# ...
end
我不在乎它找到什么具体的图片,也不在乎它去哪里,我只需要确保结果是一个字符串,来自正确的网站,并且带有图片扩展名。现在我可以相信我的查询能够提供我想要的任何上下文的图片。
集成测试
集成测试让事情变得更加复杂,它检查各个单元集成在一起的程度(明白了吗?)。
这并不总是意味着集成测试仅适用于将简单功能组合在一起的组件。根据我的经验,大多数集成测试不仅会测试用户界面,还会测试函数和属性。我认为这些仍然算作“集成”,因为它确保功能和 UI 能够按预期集成。
一个简单的例子是测试 Ember 中制作的下拉菜单,我正在测试:
- 活动类别和
aria-hidden
属性彼此同步 - 单击菜单按钮会触发这两个属性
test('the menu appears when clicked', async function(assert) {
await render(hbs`{{dropdown-container}}`);
const menu = assert.dom('.dropdown-menu__menu');
menu.doesNotHaveClass('dropdown-menu__menu--active');
menu.hasAttribute('aria-hidden', 'true');
await click('.dropdown-menu__button');
menu.hasClass('dropdown-menu__menu--active');
menu.hasAttribute('aria-hidden', 'false');
});
这里的其他集成测试可以确保在单击菜单外部时关闭菜单,或者渲染传入的其他链接。这些都属于“保持各部分集成在一起”的范畴。
验收测试
验收测试的重点从代码应该做什么转移到用户应该做什么。这些测试基于常见的用户任务,例如登录、提交表单、浏览内容以及隐私被跟踪脚本侵犯。这通常使得验收测试成为任何应用程序最高级别的测试,而且往往是最重要的测试。如果用户无法按预期使用应用程序,那么其余的测试都无关紧要。
参加我的 Ember 测验中的验收测试。用户流程的几个部分都与回答一个问题相关:
- 他们可以点击答案吗?
- 是否有正确数量的问题?
- 您能回到之前的问题吗?
- 如果您离开测验页面,您会从上次中断的地方继续吗?
- 问题是否会根据用户的星座调整其含义和价值?
- 有人能解释一下圣徒队未能进入超级碗的不公正现象吗?
我尝试回答以下(大部分)这些问题。这些问题与任何功能或组件无关,都与高层用户流程有关。
test('answering a quiz question', async function(assert) {
await visit('/quiz/1');
await click('[data-test=AnswerItem]:first-of-type')
assert.equal(currentURL(), '/quiz/2', 'You go to the next question');
assert.dom('[data-test=QuestionItem-Active]').exists({ count: 2 }, 'Two questions are available');
await click('[data-test=QuestionList] [data-test=QuestionItem-Active]:first-of-type a');
assert.equal(currentURL(), '/quiz/1', 'You go back to the previous question');
assert.dom('[data-test=QuestionItem-Active]').exists({ count: 2 }, 'The quiz remembers you answered two');
await click('[data-test=QuestionList] [data-test=QuestionItem-Active]:nth-of-type(2) a');
assert.equal(currentURL(), '/quiz/2', 'You can go back to your current question');
assert.dom('[data-test=QuestionItem-Active]').exists({ count: 2 }, 'The quiz still remembers you answered two');
await visit('/quiz');
assert.dom('[data-test=GoToQuiz]').hasText('Resume Quiz', 'The starting prompt asks you to resume the quiz');
clearAllCookies();
});
至于最后两点,我不需要验收测试就知道答案是:
- 不,星座对人类的价值,就如同手相对马的价值一样
- 上帝怒不可遏,末日将至,一旦汤姆·布雷迪赢得第七枚超级碗戒指,我们就会进入狂喜。克苏鲁万岁!
回到正题,验收测试需要完整渲染页面并与之交互,这比简单地导入组件并进行一些断言要难得多。它通常使用无头 Web 浏览器来完成,这种浏览器基本上没有用户界面,允许自动化操作。它还需要一个库来模拟用户交互和 API 请求,这对于许多应用来说可能会很复杂。
但考虑到它们在确保用户始终能够按预期完成重要任务方面发挥的作用,这些额外的工作通常是值得的。缺少验收测试可能会导致灾难,例如 Twitter 更新会意外阻止用户发推文,从而导致服务无法使用。
有趣的是:Ember 开箱即用,所有这些功能都设置好了!万一这影响了你的决定,不妨试试看。
视觉回归测试
视觉回归 (VR) 测试用于检测应用中预期或未预料到的视觉变化。基本流程如下:
- 在运行 VR 测试之前,测试已经有应用程序大部分或全部部分的屏幕截图,例如登录页面。
- 随着 VR 测试的运行,他们会抓取新的屏幕截图,以显示您所做更改后所有页面的外观。
- 然后,测试会比较每个页面的所有“前后”截图,并记录每个变化。如果某些输入字段移动了几个像素,或者整个页面丢失了,测试会进行并排比较,并突出显示差异。
你可能会想:有些更改可能是故意的。如果我尝试删除该页面或添加额外的字段,VR 测试当然会突出显示它们。那么,这样做有什么好处呢?
别怀疑测试,不信的人!最后一步是让人工审核所有更改,并标记意外更改。如果你的 VR 测试只标记了你预期的更改,那么你就批准它们!如果测试发现了你意料之外的更改,你就标记它们,尝试修复,然后再次运行测试,如此反复。
以我的经验来看,VR 测试是最难设置的。我和我现在的经理搜索过可靠的开源 VR 测试工具,但一无所获。大多数工具要么功能不足,要么维护不善。我遇到的最可靠的 VR 测试工具是一款名为Percy的工具,它最近新增了免费选项,所以我建议从它开始。
可访问性测试
如果不提一下可访问性测试,那我真是太失职了。是的,可访问性测试是可以做到的!它无法测试所有东西,但它可以帮助你避免一些常见的错误,比如不合适的标记或低色彩对比度。
我知道有几个工具可以尝试:用于静态网站的Pa11y ,以及用于 Web 应用的aXe或Lighthouse。我们公司发现了一个基于 aXe 构建的可访问性测试助手,ember-a11y-testing,它为所有页面添加了基本的可访问性测试,并且已经发现了很多错误。
您需要的具体可访问性测试人员因项目而异。找到这样的测试人员就像找到一个暗杀标记:虽然很艰难,但最终值得,而且希望事后不会溅出太多血迹。
代码质量测试
代码质量测试之所以脱颖而出,是因为它们不需要你编写任何实际的测试。相反,它们会读取代码库并标记出如下错误:
- 代码重复
- 代码过于复杂
- 不符合样式约定的代码
- 安全风险
代码质量测试还可以对代码随时间的变化进行高级分析。如果某个文件夹下的文件质量随时间变化很大,测试会指出这一点,以便您进行更大规模的重构。或者,如果开发人员在代码中逐渐添加了隐藏的恶意软件,这些恶意软件会将自身注入用户的计算机,测试可以向当地警方和特警队发出警报(这通常是高级功能)。
这些测试,就像 VR 测试一样,可能会对预期的更改发出标记。因此,与 VR 测试一样,一旦标记获得批准,测试就会通过。
为什么测试很重要
在介绍了不同类型的测试之后,我承认我看到它们时的第一反应是“这一切真的那么重要吗?”
如果我能遇见过去这样想的自己,我会打他们一巴掌,在他们耳边低声说几个中奖彩票号码,然后再打他们一巴掌,因为他们又不会去报警。
此外,如果我有时间的话,我可能会告诉他们关心测试的以下原因。
确保基本功能
显而易见的好处是,如果你的测试涵盖了所有基本功能,你就可以随时启动你的应用,因为它仍然有效。用户发现他们依赖的功能突然崩溃是第二令人恼火的事情(第一是发现所有图片都被替换成了尼古拉斯·凯奇)。
这对业务也有好处。支付或上传新内容相关的功能故障可能会导致你的应用无法使用或无法盈利,直到错误被发现为止。谁知道在那之前你会损失多少用户(或金钱)。如果你编写的是真正重要的事情的软件,比如医院管理病人记录,情况会更糟。人们可能会死,而且与任天堂Switch版《塞尔达传说:荒野之息》里的马不同,你无法通过向戴着疯狂面具、被植物困住的女神祈求来召唤它们。
所以,别做那种在马神面前献卢比的开发者。写测试来确保需要的功能仍然有效。
防止错误再次发生
错过一次错误是一回事,再次犯同样的错误就更糟糕了,因为用户认为一个优秀的应用不会重复他们的错误。
测试可以通过一个简单的经验法则来帮助避免这种情况:对于发现的每一个错误,尝试编写一个可以捕获它的测试。
最近,我遇到了这个问题。当时,加载栏组件中出现了一个翻译错误,导致用户无法上传文件。这个问题影响很大,我们很幸运地及时发现了它,但我发现没有测试来确保这些加载状态正常工作。因此,修复错误后,我编写了一个单元测试,以确保加载栏文本和进度的输出符合预期。为了确保安全,我测试了三个级别:空、加载一半和加载满。
现在,再次出现这种情况的可能性大大降低,潜意识里困扰我们梦境的盲点也少了一个。胜利!
节省时间
我的第一份开发工作对跨浏览器测试的要求很高。我甚至一度(我发誓这是真的)在同一个本地服务器上安装了四台笔记本电脑。这些笔记本电脑分别运行着 Chrome、Firefox、Safari,以及一台客户经常使用的旧款笔记本电脑,运行着 Internet Explorer。
每当我们在任何地方做出任何更改,我都必须逐页浏览模式库,并逐一查看每个组件的功能。即使我尽可能快地操作,每次也至少要花 30 分钟才能完成,这真是令人精疲力竭。当天每次收到反馈(或者如果我漏掉了什么),我都得重复一遍,这使得“测试和反馈”环节至少持续了两天。
我觉得我不需要解释你为什么不这么做。测试把这种折磨变成了“只需按下按钮然后等待,就不会激起跳下悬崖的欲望”。这不言自明。
测试经验规则
讲完了测试的“是什么”和“为什么”,我们来聊聊“怎么做”。我不会深入讲解具体的测试框架,因为内容太多了,如果你在搜索的时候记住这些框架,你会学得更好。
但是在编写测试时,需要遵循几条规则。
尽早并经常进行测试
避免出现“我有一个完成的应用,但还没有测试”的情况。这会让添加测试的想法变成一个令人畏惧的庞然大物,最终你会放弃,并且会因为放弃而后悔,因为这会玷污你的声誉(就像在线约会一样)。
每次添加或更新新功能时,都尽量添加测试。添加组件时,请为其添加新的测试。扩展功能意味着扩展测试方法。这样可以更轻松地跟踪需要测试的内容,并使其易于理解。
确保测试按预期通过(和失败)
编写一个能通过的测试并不总是好的。有些情况下,我故意把它改成失败,结果它仍然通过了,我才意识到它有问题。只有当测试不能及时提醒你问题所在时,它才是好的。永不失败的测试更容易让缺陷潜入生产环境。
防止这种情况很简单,只要在编写测试后稍作修改,使其按预期失败即可。如果您要测试某个函数是否输出6
,请检查测试其他数字时是否失败。
更彻底的方法是将这些小小的“失败测试”变成额外的测试。大多数测试库都允许你测试结果不应该是什么样的,以及应该是什么。虽然我倾向于编写更多应该测试的测试,但通常其中会混杂大量不应该测试的测试。
以我的新闻通讯应用为例。查询对象应该在每次查询时返回随机图像,以确保每次查询都能得到不同的图像(排除巧合匹配或上帝之手来捉弄我的情况)。因此,我会测试两个不同的查询是否相等。
it 'should give different images from the same object' do
image1 = query.random_image
image2 = query.random_image
expect(image1).not_to eq(image2)
end
虽然我这里的大多数测试都是寻找文件类型和字符串的匹配,但这个测试通过确保两个东西不匹配来通过。换句话说,我正在测试一种类型的失败。
不要重写程序的逻辑
几周前,我写了一个正则表达式,用于 URL 中的一些基本字符串替换。之后,我添加了一个额外的测试,以确保这种情况不会再次发生(回调是为了防止旧错误!)。在那次 PR 中我犯了很多错误,我的治疗师建议我不要在这里写,其中一个错误就是,为了确保结果匹配,我把同样的正则表达式复制到了测试中。
作为一名聪明的开发人员,在阅读我的帖子后,您可能已经知道这是错误的,原因有两个:
- 正则表达式中的任何错误都不会被捕获,因为错误只会带入测试并认为它是正确的。
- 代码重复!在应用程序中修改代码意味着我可能会忘记修改测试。
正确的方法是删除正则表达式,只测试最终结果。如果我有一个 URL,应用应该返回这个版本的 URL。只有应用自己控制如何生成该结果,而不是测试。测试只是确保它正确执行,至于它如何执行并不重要。
了解数据存根
最后,补充一点:大多数应用程序都会以某种方式连接到 API,而有些组件则专门依赖 API 数据。它们可能会通过 API 请求提取数据,或者发送 POST 请求来更新信息。
我最初处理这些问题时,是像普通测试一样编写它们。但它仅在以下情况下有效:
- 任何自定义 API 都在本地运行,但在 Github 拉取请求的持续集成测试中失败
- API 数据更难预测,并且可能会发生变化,这很容易导致测试失败
- 测试与 API 交互的组件可能会改变实际数据,这会带来很多不利影响,目前我还没有税收激励措施可供列举。
解决方案:数据存根!这意味着创建一个虚假的 API 端点,它会在测试中返回静态数据,这些数据可以进行可预测的匹配和测试。API 的 post 请求也可以返回测试断言,因此它们不会更改数据,而只是确认请求可以发出。
因此,在寻找测试框架时,需要查看它们是否包含或兼容良好的数据存根方法。值得庆幸的是,大多数框架都包含。
无论多么困难,都要让测试发挥作用
我将用最后一条,或许也是最重要的一条建议来结束这篇文章。它很简单:无论测试运行起来多么令人沮丧和抓狂,都要做好一切必要的工作。测试总是值得的。正如《程序员修炼之道》所说,只有运行测试,代码才算真正好。
最近我开始用 React 和 Pokemon API 构建一个 Pokedex,并希望进行一些基本的测试。我最终选择了 Jest 和 Enzyme,但让它们运行起来非常糟糕。我花了一个多小时,遇到了 Babel 兼容性问题,而且它们还因为一些看似随机的原因失败了,我不得不记下错误信息。
到最后,我几乎要因为纯粹的愤怒而掀翻桌子了,而不是像往常那样狂喜,或者原始的混乱欲望。但我最终还是让它们运行起来了,只是像预期的那样失败了,然后我就可以设置单元测试、集成测试和验收测试了。
绝对值得。
在我职业生涯的早期,我经常忽略测试。经验丰富的同事告诉我,这是其他新程序员的通病。所以我恳求年轻的自己,以及其他可怜我这个作者名的新手程序员们,读一读这篇文章:以后再也不要忽视添加测试。测试的重要性不亚于任何语言基础或流行框架。你的同事、用户、股东、跟踪者,以及过去的自己,都会因此而更加感激。
文章来源:https://dev.to/maxwell_dev/the-testing-introduction-i-wish-i-had-2dn