前言:前端领域的自动化测试一直是前端同学的一个特殊命题。一方面,其实大家都知道自动化测试的好处。如果你做了什么改动,只需要跑一次测试用例就可以知道之前的逻辑有没有改动,改动的时候也会更有把握。另一方面,前端本身是独一无二的,活动页面从需求审核到正式上线,可能一周内就完成了。这个迭代速度和写测试用例是在折磨自己。但其实自动化测试也是前端工程中非常重要的一环。即使是快速迭代的活动页面,也会有常用的工具函数和SDK。对这部分代码进行测试用例的完善是很有必要和有意义的。对于一些流量巨大、长期存在的页面,我们甚至需要各种测试场景。但是,由于这两种情况的存在,其实很多前端同学对自动化测试的认识是相当空白的。它有哪些分类?推荐的做法是什么?有哪些框架和程序?本文的目的是进行一个基本的扫盲。至少看完之后,你就会知道项目应该怎么写测试用例,哪些场景应该写测试用例。单元测试与集成测试单元测试(UnitTesting)顾名思义,其中的测试用例应该针对代码中的每一个单元。比如在前端代码中,每个工具方法都可以看作一个单元,对应一个独立的测试Example。但是这么说并不意味着你必须编写非常细粒度的代码——折磨自己不好吗?我通常使用“功能单元”的方法来确定粒度。例如,在薯条生产线上,清洗-去皮-切片-包装是四个完全独立的功能单元。你可能会疑惑,这四个功能单元明明是有依赖关系的,为什么完全独立呢?这是因为在单元测试中,一个非常重要的步骤就是模拟当前测试单元的外部依赖。比如我在测试去皮功能时,会直接给出“cleanedpotatoes”,然后勾选“peelingAfterthepotato”,而没有实际调用前后的功能单元。常见的模拟操作可以分为Fake、Stub、Mock、Spy,下面我们会详细介绍。一种常见的情况是,根据外部依赖的性能,会在工具方法中执行不同的代码分支(if/else、try/catch、resolve/reject等)。这时候我们要做的就是通过修改外部依赖的性能来检查工具方法内部各个代码分支的执行情况。例如,fetch成功返回时调用processData方法,fetch失败时调用reportError方法。这时,你可以篡改fetch的实现,然后检查processData和reportError方法是否被调用(注意这两个方法也需要被mock(Stub/Spy)才能被检查调用)。当然,完全模拟所有外部依赖是最理想的情况。在很多情况下,一个工具方法可能有很多外部依赖。这时候可以省略哪些可以确定没有副作用(比如logger等纯函数),或者与核心逻辑相关的。无关的部分。我们知道测试用例也可以依次检查代码,这种效果基本上在单元测试阶段最为明显。比如你很容易发现某个功能单元耦合性太强,或者外部依赖会导致代码走错分支之类的事情。目前推荐的单元测试方案主要有Jest、Mocha、Sinon、Jasime、AVA等几种,各有优缺点,这里不做比较。但需要注意的是,一套完整的能够满足实际需求的单元测试方案通常意味着需要包含几个功能:(2)),而Sinon提供了类似NodeJsasserts模块风格的断言(``sinon.assert.pass(1+1===2)`),而Mocha没有绑定断言库,可以使用断言模块或Chai进行断言。此外,断言包括几种不同的风格,我们也在下面进行解释。用例集合,我们在写测试用例的时候,也需要根据功能单元来区分。常见的方式是描述收集一个功能单元,并在内部使用它/测试来验证功能单元的每个逻辑分支。如:describe('Utils.Reporter',()=>{it('shouldreporterrorwhenxxx',()=>{})it('shouldshowwarningswhenxxx',()=>{})})模拟函数(Stub、FakeTimers等),包括对象的Spy、函数的Stub、模块的Mock,都属于模拟的范畴。测试覆盖率报告,该功能一般通过istanbul(版本1.0、2.0更名为nyc)或c8实现,其原理包括代码插桩和V8引擎内置函数的使用,这里不再赘述.另一种常见的场景是以其他语言格式(例如JUnit)输出覆盖率报告。社区也通过Reporters机制支持这些测试框架。如果你之前对这些单元测试方案不是很熟悉,那我推荐你学习一下Vitest,antfu的作品,特点是速度快(毕竟是基于Vite的),对TypeScript和ESModule的支持很好,我目前正在将所有单元测试迁移到Vitest,Vitest也有自己的UI界面,这样你就可以享受编写测试并看着它们一一通过的乐趣。如果说单元测试是测试单个功能单元,那么集成测试(IntegrationTesting)显然是测试多个功能单元的协作。但需要注意的是,多个功能单元的协同并不意味着对整个系统(流水线)进行完整的功能测试。通常我们也会把几个功能单元分散组合成为系统的某个部分,比如cleaning-Peeling作为预处理功能需要判断一筐土豆能否正确变成干净的去皮土豆,slice-包装是一个核心功能,需要确定去皮的土豆可以变成冷冻薯条。编写集成测试,其实我们仍然只需要使用单元测试方案,因为集成测试本质上就是同时测试多个功能单元,我们验证的范围也相应扩大了。但是,集成测试的维度划分并没有明确的界限。你可以像上面那样将预处理功能作为一个系统部分,也可以将整个流水线作为一个系统部分(以及供应链部分、烹饪部分和服务部分。),根据你的实际业务场景。Mock、Fake和Stub通常具有有限的测试用例运行时间。比如我们不想真正发起网络请求,不想和数据库交互,不想操作DOMAPI。这时,我们会使用一系列的模拟方法来具体模拟一个交互对象,通过修改其行为来检查是否执行了预期的处理逻辑。这种模拟行为通常被直接称为Mock,但实际上,由于被模拟对象的类型和注入的模拟逻辑,更准确的描述是将这些行为分为三大类。第一个是最常用的Stub。假设我们正在为UserService编写单元测试。里面注入的PrismaService负责读写数据库。我们可以使用一个PrismaServiceStub来替换实际的服务,并在其中提供相应的PrismaService.user.findUnique。方法,然后当我们调用UserService.queryUser时,我们可以检查是否使用预期的输入参数调用了PrismaServiceStub上的相应方法,以及输出参数是否按预期处理并返回。Spy也可以认为是Stub的一种,但更强调“是否按预期调用”的过程。我们甚至可以只监视一个对象而不提供模拟实现(例如控制台之类的API)。而如果我们不想替换PrismaService,而是想让它真正读写数据,而不是真正的数据库,我们可以提供一个Fake数据库——比如一个对象,这样数据库的读写就变成了除了读写内存对象,它变得更快更稳定。这是假的。另一个常见的假冒场景是定时器,常见的单元测试框架为假冒定时器提供功能支持。其实Mock和Stub很相似,只是Mock更像是一个“预期的输入参数”,并不关注返回值。我个人的理解是项目中fixtures文件夹下的各种对象和json都是典型的Mock。当然,Mock、Stub、Spy还是很相似的,区别我们也不必去琢磨,因为它们本质上都是模拟。断言:expect、assert、should我们常见的断言包括expect和assert形式。NodeJs提供了一个原生的asserts模块供你编写一些简单的断言。你也可以在实际代码中使用断言来确保逻辑正确运行,而expectForms通常只存在于测试用例中。比如检查一个函数调用,比较两个对象,两种风格如下:expect(mockFn).toBeCalledWith("linbudu");assert.pass(mockFn.calls[0].arg==="linbudu");期望(obj1).toEqual(obj2);assert.equal(obj1,obj2);通常,我个人比较喜欢命令式风格明显的expect断言,而除了这两种风格之外,其实还有一个shouldformchain风格的断言,它是这样写的:mockFn.should.be.called();obj1.should.equal(obj2);值得一提的是,以上三种断言风格均在Chai断言库Support中实现,感兴趣的不妨一试。前端页面的组件测试和E2E测试。单元测试和集成测试是前端和后端应用程序中的常见概念。完成基本功能单元的测试后,我们需要更进一步,关注领域内的具体功能,比如从前端的角度看一个组件的UI和功能,看一个组件的响应从后端角度对各种奇怪的输入参数进行接口。在如今的前端项目中,组件化应该是最明显的趋势,那么组件维度测试自然是相当有必要的。以React组件为例,我们可以模拟这个组件的输入参数,观察实际渲染的UI组件是否正确,通过快照检查组件实际渲染是否一致。目前使用的组件测试方案通常都是绑定在框架上的,比如React下的@testing-library/react和Enzyme,Vue下的@vue/test-utils,Svelte下的@testing-library/svelte,因为本质上我们是在渲染这个组件隔离并模拟框架行为以验证其性能。在组件测试方案中,我推荐@testing-library/react(也包括@testing-library/react-hooks),Enzyme的API比较复杂,应该不再维护(或者维护水平堪忧)。使用它编写的测试用例如下所示:import*asReactfrom'react'functionHiddenMessage({children}){const[showMessage,setShowMessage]=React.useState(false)return(
显示消息setShowMessage(e.target.checked)}checked={showMessage}/>{showMessage?children:null}
)}exportdefaultHiddenMessageimport*asReactfrom'react'import{render,fireEvent,screen}from'@testing-library/react'importHiddenMessagefrom'../hidden-message'test('显示子项时thecheckboxischecked',()=>{consttestMessage='TestMessage'//渲染组件模拟render(
{testMessage})//根据模糊查询expect验证DOM元素是否存在(screen.queryByText(testMessage)).toBeNull()//FireEvent.click(screen.getByLabelText(/show/i))//验证结果是否符合预期expect(screen.getByText(testMessage)).toBeInTheDocument()})单元测试、集成测试、组件测试,看似我们已经完美地使用了自动化测试,从不同的场景和维度来验证功能,但实际上,我们还缺少一个很重要的维度——用户视角当程序最终交付验收时,我们可爱的测试同学会来检查所有的功能和链接,即使你写了海量的测试用例,你仍然可能会发现很多问题是视角不同。作为一个程序开发人员,你对程序的控制流程和各个分支都有清楚的认识,所以你在写测试用例的时候其实更像是上帝视角。从用户的角度来看,其实我们只需要屏蔽掉程序内部的所有感知,使用这个程序就可以了。这样的测试称为端到端测试(End-to-EndTesting,E2E),它不再关注内部功能单元的细节,而是从外部完全还原一个真实的用户视角,比如在前端应用,用户登录——搜索商品-加入购物车-编辑商品-查看商品的一系列交互,谁在乎你的登录背后隐藏了多少权限,商品上架的层次有多精细设计好了,只要这个过程不能顺利进行,那么你的系统就是有问题的。由于端到端测试是模拟用户行为,我们需要做的就是利用用户的环境来运行系统。比如对于前端页面来说,其实就是浏览器(更准确的说是浏览器内核),对于后端服务来说,就是客户端。以Cypress的功能为例,看看我们是如何模拟用户行为的:在前端领域编写E2E测试。常见的端到端测试框架主要有Puppeteer、Cypress、Playwright、Selenium。它们各有优缺点,适用场景也不同。我们将在下面对它们进行比较。与其他测试场景的重要区别之一是E2E测试可以由测试学生编写(例如支持Python和Java的Selenium)。在产品迭代的同时,测试同学会根据功能点的变化对测试用例进行相应的改进,同时保证之前所有功能的测试用例不受影响。Puppeteer、Cypress、Playwright三者选择目前常用的前端E2E测试有Puppeteer、Cypress、Playwright,这可能会让你难以选择。你应该选择哪一个?如果我选了一个,写了发现不符合要求怎么办?在这一部分中,我们将简要介绍它们。先说结论:Puppeteer用于非常简单的场景(需要Jest-Puppeteer),Cypress用于PC应用,Playwright用于移动应用。那我们就一一解释吧。第一个是傀儡师。说真的,它不应该用于E2E测试,因为它真的只是一个无头浏览器。如果你想用它来写爬虫之类的,没问题。如果你想让别人给你做E2E,一方面,它只支持Chrome+Chromium内核,另一方面,他们没有断言库,所以你必须带一个Jest-Puppeteer。但是如果真的是非常简单的场景,还是可以使用Puppeteer,加上NodeJs的基础断言库,自动判断一些页面功能是没有问题的。然后是Cypress,其实从他们的Slogan就能感受到他们的场景:Fast,easyandreliabletestingforanythingthatrunninginabrowser,attentiontoinabrowser,其实他们提供的是headlessbrowser,assertion,GUI,andWeb下的各种API,然后用浏览器就可以完全模拟所有的行为。同时,Cypress还通过代码检测支持覆盖率报告相关的能力。需要注意的是Cypress只支持浏览器维度的配置,比如chrome(也支持chromium)、edge、firefox。因此,如果你更专注于检查你的应用程序在移动端的性能,你实际上应该使用Playwright。为什么?Playwright支持同时运行多个项目。这些项目可以配置使用浏览器内核(测试PC和桌面场景),也可以配置使用内置设备预设来测试它们在移动端的性能。这些预设包括视口大小、默认浏览器内核(chromium、webkit、safari等)等,参考官网示例配置://playwright.config.tsimport{typePlaywrightTestConfig,devices}from'@playwright/test';constconfig:PlaywrightTestConfig={projects:[{name:'DesktopChromium',use:{browserName:'chromium',viewport:{width:1280,height:720},},},{name:'DesktopSafari',使用:{browserName:'webkit',viewport:{width:1280,height:720},}},{name:'DesktopFirefox',使用:{browserName:'firefox',viewport:{width:1280,height:720},}},{name:'MobileChrome',use:devices['Pixel5'],},{name:'MobileSafari',use:devices['iPhone12'],},],};导出默认配置;在我的团队中,目前我正在基于Playwright构建端到端的测试用例,以更方便快捷地保证核心应用的页面功能。最后,如果你开发的不是前端应用而是UI组件库,那么你可以使用StoryBook(基于Jest和Playwright)提供的测试能力,这样你就可以获得基于StoryBookNote的组件可视化文档,还可以获得自动生成的端到端测试用例:后端服务端到端测试和压力测试至于后端服务测试,由于没有深入实践,这里简单介绍一下NodeAPI端到端测试,压力测试。上面我们已经提到,对于后端服务来说,用户其实就是各个客户端,我们只需要模拟客户端,向API发起请求,模拟登录状态信息和各种参数,然后查看最终返回的结果是否符合预期。在这个过程中,我们不应该也不需要关心API是由哪个Controller承担的,调用了哪些Services,经过了哪些Middleware。冒充客户的方法就简单多了。常用的方法是使用supertest。另外,通常后端服务的E2E测试也应该尽可能模拟完整的交互过程:上传商品-编辑商品-列表商品-移除商品-...,但这个过程并不像前端。另外,在后端服务中如何mock端到端的测试也是不同的。如果想尽可能模拟用户,可以使用专用的测试环境数据库,但是测试的执行并不完全稳定。如果你想保持简单,你可以在单元测试和集成测试中模拟外部依赖。另外,一些NodeJs框架也直接提供原生测试支持,比如@nestjs/testing、@midwayjs/mock等。另外一个针对后端服务的特殊测试场景是压力测试,可以等同于特定时间的性能测试.模拟大规模用户访问,测试服务性能。本质上,压力测试不是测试API的逻辑,而是测试API所在服务器的性能和负载均衡相关逻辑。对于压力测试,可以简单的使用脚本开启多线程并发请求,也可以使用ApacheBench、Webbench、wrk(wrk2)等测试工具,或者npm社区也有autocannon这样的实现。压测下,我们主要关注几个指标:RPS,RequestPerSecond,更俗称QPS,QueryPerSecond,代表到达服务器的请求数。并发用户数CL,ConcurrencyLevel,与RPS不同,并发用户数代表的是等待处理的未完成请求。比如假设某个magicAPI的请求处理速度非常快,每个请求的处理时间无限接近于0,那么即使它的RPS可能达到百万级,并发数也很低(关闭to0)——因为它处理得很快,几乎不需要同时处理两个请求。TransactionspersecondTPS,TransactionsPerSecond,TPS与QPS有些相似,但它关注的事务其实是一个比请求-响应过程更具体的过程。比如访问server/index.html,实际上访问的是server/index.css和server/index.js文件,那么这个过程实际上只会记录为一个事务,但是会记录为三个query。响应时间RT,ResponseTime,一个请求从进入到带走响应的耗时,这个耗时包括等待时间-处理时间-IO读写时间-响应到达时间。除了这些指标,我们还会关注服务器当前的性能指标,比如内存和CPU使用率,驻留集(RSS,当前进程分配的物理内存,包括堆、栈和执行代码段,等),也可以使用NodeJs提供的--prof--prof-process等启动参数,或者使用heapdump提供的内存快照打印功能,帮助分析NodeAPI的性能。End除了上面介绍的自动化测试类,其实还有前端页面性能测试(比如基于LightHouse,PerformanceAPI),主要关注各种“第一”的指标,比如首屏绘制,交互时间,maximumcontentdrawing等等基于axe-core的无障碍测试(AccessibilityTesting),重点关注网页的无障碍,以及一些比较少见的场景,比如基于Needle的CSS测试,基于Coffee的命令行应用测试,以及混沌工程概念Chaostesting等。这些概念要么在社区中已经有大量高质量的介绍文章,要么我还没有深入了解,这里就不赘述了。另外,为了进一步保证页面的功能稳定性,监控平台(白屏、JSError、404)的存在也是比较有意义的,但是对于这部分功能的解决方案已经太多了,Sentry在社区,以及各大厂搭建的平台等,这里不再赘述。在这篇不长不短的小文中,我们基本了解了前端开发者会接触到的所有自动化测试类型,包括它们的使用场景、实践方法、可选的库/框架。看完全文,如果你恰好正在开发一个“值得花力气去写测试”的应用,不妨想想是否有部分满足你的需求。