前端测试一直是前端工程化中很重要的话题,但是很多人往往对测试产生误解,认为测试不仅没有什么用而且还浪费时间,或者测试应该让测试人员来做,自己应该专注于开发。所以,文章开头会先从"软件工程语境下的软件测试"的角度,介绍软件测试的定义、作用及其分类,让读者正确认识测试,明确自身在软件测试阶段中的定位,以及在软件测试过程中所承担的职责和所应完成的任务。

在理解软件测试的定义及作用之后,就要开始入门前端测试了,在这一部分我介绍了许多常用的自动化测试基础知识,比如断言、模拟,还介绍了单元测试框架 Jest 和最新的 Vitest 的基本使用并进行了较深入的比较。

最后是前端测试的实战部分,我演示了如何测试一个地址列表小应用,先介绍进行组件测试时要使用的组件挂载库 Vue Test Utils 和 Vue Testing Library,然后重点介绍了进行组件测试时的测试原则、测试技巧和一些注意事项。

本文篇幅较长,全程高能,建议收藏慢慢观看。

软件工程语境下的软件测试

什么是软件测试

什么是软件测试?要回答这个问题,我们首先需要先明确为什么要进行软件测试?答案很简单,就是为了保证软件质量。对于软件来说,不论采用什么技术和什么方法来进行开发,软件产品中都或多或少会存在一些错误和问题。采用先进的开发方式、完善的开发过程,可以减少错误的引入,但是不可能完全杜绝软件中的错误,而这些错误就需要通过测试来找出。因此,软件测试是软件质量保证的关键步骤。

关于软件测试的定义,一直有正反两方面的争辩。正面的观点是:软件测试是使用人工或自动手段来运行或测定某个系统的过程,目的在于检验它是否满足规定的需求或是弄清预期结果与实际结果之间的差别。这个观点明确地提出了软件测试是以检验是否满足需求为目标。

而反面的观点是:测试是为了发现错误而执行一个程序或系统的过程,测试就是为了发现缺陷,而不是证明程序无错误,如果没有发现程序中的问题,并不能说明问题就不存在,而是还没发现软件中潜在的问题。该观点认为,一个成功的测试必须是发现了软件问题的测试,否则测试就没有价值。

这正反两面的观点是从不同的角度看问题,一方面通过测试来保证质量,检验软件是否满足需求,另一方面由于测试不能证明软件没有丝毫错误,所以要尽可能找出不能正常工作的地方。在具体的应用场景中,软件测试应该在这两者之间取得平衡,或者有所侧重。

软件测试与软件开发的关系

介绍完了为什么需要软件测试以及什么是软件测试,我们明确了软件测试的定义及其作用。在这一小节,我们探讨软件测试和软件开发的关系,研究软件测试在软件工程中所扮演的角色。

在人们的刻板印象中,软件测试的活动似乎仅仅发生在编码完成之后,被认为是一种检验产品的手段,成为软件生命周期的最后一项活动而进行。在著名的软件瀑布模型中,软件测试处在编程阶段的下游,位于维护阶段的上游,先有编程、后有测试,测试的位置被放得很清楚。瀑布模型中的测试只有等到程序完成了之后才会执行,强调测试仅仅是对程序的检验:

然而瀑布模型属于传统的软件工程,存在较大的局限性,与软件开发的迭代思想、敏捷方法存在冲突,也不符合当今软件工程的实际需求。实际上,软件测试贯穿着整个软件生命周期,从需求评审、设计评审开始,软件测试就介入到软件的开发活动中。例如,通过对需求定义的阅读、讨论和审查,不仅可以发现需求定义的问题,还可以了解产品的设计特性、用户的真正需求,从而确定测试目标、准备测试用例并策划测试活动。

同理,在软件设计阶段,通过了解系统是如何实现的、构建在什么运行环境中等问题,可以衡量系统的可测试性、检查系统的设计是否符合系统的可靠性要求。

因此,软件测试和软件开发在整个软件生命周期中是相互协作,共同工作的。在软件的项目启动时,软件测试的工作便随之开始了。V 模型很好地反映了软件测试和软件开发之间的关系:

如图所示,左边是软件定义和实现的过程,右边是对左边所构造的结果进行检验的过程,即测试与开发之间是一对一的关系,通过对开发工作成果的检验,来确认其是否满足规定的要求。

你可能会对 V 模型右边的各种测试类型有些疑惑,像功能测试、验收测试等测试都属于软件测试的分类。

软件测试的分类

软件测试可以从不同角度进行分类,例如根据测试的方法进行分类,也可以根据测试的目标和测试的阶段进行分类。如图所示,是软件测试的三维空间:

对于前端程序员来说,我们应该对其中的单元测试、集成测试和系统测试较为熟悉,这三个层次其实是按照被测试的对象或测试阶段划分的。具体内容在后面几节会介绍。

功能测试也称正确性测试,用于验证每个功能是否按照事先定义的要求正常工作,比如我们前端程序员写的大部分单元测试就属于功能测试。而其他目的的测试,如压力测试、兼容性测试和安全性测试则一般交给专业的测试人员负责。

回归测试是为保证软件中新的变化(如增加、修改代码)不会对原有功能的正常使用有影响和进行的测试。比如我们将新代码提交到版本控制库后在 CI/CD 管道运行测试脚本的行为,就属于回归测试。

此外,还有四类测试需要我们特别注意:

静态测试和动态测试

根据程序是否运行,测试可以分为静态测试和动态测试。

静态测试包括对软件产品的需求和设计规格说明书的评审,对程序代码的审查以及静态分析等。比如我们在编写完代码之后一般都会简单地检查所写的代码,通过观察程序的控制流或走向来分析其行为是否符合预期,这种静态分析便属于静态测试。

此外,使用 TypeScript 可以做到在编码时静态分析代码并进行类型检查,从而发现并提示程序中隐藏的类型错误,这个过程也属于静态测试。如果没有使用 TypeScript 等强类型语言,我们通常都需要在代码中使用 typeof 等关键字进行类型判断来避免这类类型错误,并在单元测试中创建并运行相应的测试用例来确保代码对类型的判断符合预期。所以从测试这一角度来看,TypeScript 这类语言或工具在某种程度上也解放了程序员的双手,让我们不必编写复杂且麻烦的判断程序类型处理是否正常的测试用例,专注于对程序的功能测试。

当然,使用像 ESlint、Prettier 这类的 linter 或 formatter 也属于静态测试,用以检查代码的格式、风格是否符合团队规范。

像这类使用工具对代码进行静态分析,检查代码是否符合需求的过程,属于自动化测试,所使用的工具称为测试工具。后面我们将聚焦于如何使用 Jest、Vitest 等测试工具或技术进行自动化测试及编写测试代码。

动态测试是通过运行程序发现错误,通过观察代码运行过程来获取系统行为、内存、堆栈及测试覆盖率等各方面的信息,来判断系统是否存在问题,或者通过有效的测试用例对应的输入输出关系来分析被测程序的运行情况,来发现缺陷。当写完一个组件后,我们都会让代码在浏览器上跑起来,观察组件的渲染效果或运行结果判断是否符合预期,这种行为就属于动态测试。

自动化测试和手工测试

刚才提到了自动化测试,这一小节我们来详细地介绍自动化测试。

软件测试是一项艰苦的工作,需要投入大量的时间和精力,据统计,软件测试会占用整个开发时间的 40%。但是,软件测试工作具有比较大的重复性。我们知道,在软件发布或新代码提交之前,都会进行多轮回归测试,也就是说,大量的测试用例会被重复执行很多遍,然而这个时候所进行的测试仅仅是为了验证所提交的功能或代码不会对已经实现的代码造成影响,所以找到缺陷的可能性一般很小。尽管执行大量的回归测试的效率低,但又是十分必要的。所以,自动化测试产生了。

自动化测试是相对手工测试而存在的概念,由手工逐个运行测试用例的操作过程被测试工具或系统自动执行的过程所代替。自动化测试是软件测试中提高测试效率、覆盖率和可靠性的重要手段,是软件测试不可分割的一部分。

自动化测试是把以人为驱动的测试行为转化为机器执行的一种过程,即模拟手工测试步骤,通过执行由程序语言编制的测试脚本,自动地完成软件的单元测试、功能测试、负载测试等工作。

对前端程序员来说,除了借助 TypeScript、ESlint 等静态测试工具进行自动化测试外,还可以使用 Jest、Vitest、Mocha 等单元测试工具和 Cypress、Playwright 等端到端测试工具来进行自动化测试。

白盒测试和黑盒测试

根据是针对软件系统的内部结构,还是针对软件系统的外部表现行为采取的测试方法,分别被称为白盒测试方法和黑盒测试方法。

白盒测试,也称为逻辑驱动测试或结构化测试,是已知产品的内部工作过程,清楚其程序结构和语句,按照程序内部的结构测试程序,测试程序内部的变量状态、逻辑结构、运行路径等,检验程序中的每条通路是否都能按预定要求正常工作,检查程序内部动作或运行是否符合设计规格要求。

有写过单元测试的同学可能知道在完成测试代码的编写后我们通常都会跑一次代码覆盖测试,根据生成的代码覆盖率报告来判断所编写的测试是否充足。这里的代码覆盖率是通过运行的测试代码所覆盖源代码的分支、函数和语句等的程度占源代码的比值来得到的。如果代码覆盖率未达到要求,我们就需要为未覆盖到的代码编写一个或多个测试用例来提高覆盖率,这种测试方法就可以称为白盒测试。

此外,刚才提到的 TypeScript、ESlint 等测试工具也可以说是一种白盒测试工具。

黑盒测试,也称为数据驱动测试,在测试时,把程序看作一个不能打开的黑盒子,在完全不考虑程序内部结构和内部特性的情况下针对软件直接进行测试。

黑盒测试不关注软件内部结构,而着眼于程序外部用户界面,关注软件的输入和输出,关注用户的需求,直接获得用户体验,从用户的角度或扮演用户角色来验证软件功能。

作为前端程序员,我们所使用的测试方法绝大多数都应该使用黑盒测试,具体的原因和做法可以看下文。

单元测试、集成测试和系统测试

软件系统是由许多单元构成的,这些单元可能是一个对象、类或函数,也可能是一个更大的单元,组件或模块。要保证软件系统的质量,首先就要保证构成系统的单元的质量,也就是要开展单元测试。通过充分的单元测试,发现并修正单元中的问题,从而为系统的质量打下基础。

单元测试的大部分工作应该由开发人员完成,但是很多开发人员只把注意力放在编程上,把代码写出来,而不愿在测试上花费时间,让测试人员去进行测试。需要明确的是,如果没有做好单元测试,软件在集成阶段及后续的测试阶段会发现更多的、各种各样的错误,大量的时间将被花费在跟踪那些隐藏在独立单元内的、简单的错误上面,导致整个项目的工期增长,提高软件成本。

作为软件开发人员,一定要明确一点:软件存在的错误发现得越早,修改和维护的费用就越低,难度也越小,而单元测试就是早期抓住这些错误的最好时机。

单元测试强调被测试对象的独立性,被测的独立单元将与程序的其他部分隔离开,以避免其他单元对该单元的影响。例如将被测模块与其父模块和子模块隔离开,单独进行测试。但是将其依赖隔离开的话又可能导致被测模块无法正常工作,这时候就需要用到前端单元测试中经常使用的 Mock,即模拟,具体内容可以看下文。

在软件开发中,经常会遇到这样的情况:单元测试时能确认每个模块都能单独工作,但这些模块集成在一起之后会出现有些模块不能正常工作的问题。仔细思考便可以知道,这主要是因为模块集成到一起后相互调用时的接口出现问题,如接口参数不匹配、传递错误数据等问题。这时就需要进行集成测试。集成测试是将已通过测试的单元按设计要求集成起来再进行的测试,以检查这些单元之间的接口是否存在问题。

在进行集成测试时,需要选择集成模式,即按照怎样的策略进行集成。集成测试基本可以概括为以下两种:

非渐增式测试模式:先分别测试每个模块,再把所有模块按设计要求放在一起结合成所要的程序进行测试;* 渐增式测试模式:把下一个要测试的模块同已经测试好的模块结合起来进行测试,测试是在模块一个一个的扩展下进行,测试的范围也逐步增大。在实际工作中,一般采用渐增式测试模式,具体的实践有自顶向下、自底向上、混合策略等。当然,具体情况具体分析。

经过集成测试之后,分散开发的模块被集成起来,构成相对完整的体系,其中各模块间接口存在的种种问题基本都已消除,此时就可以进入系统测试阶段。

系统测试是将经过集成测试过后的软件,作为计算机系统的一个部分,与计算机硬件、数据和平台等系统元素结合起来,在真实运行环境下对计算机系统进行一系列的严格有效的测试来发现软件的潜在问题,保证系统的正常运行。

系统测试分为功能测试和非功能性测试。

系统级功能测试不仅要考虑模块之间的相互作用,而且要考虑系统的应用环境,而且要模拟用户完成从头到尾,即端到端的业务测试, 确保系统可以完成事先设计的功能,满足用户的实际业务需求。

系统非功能性测试是在实际运行环境或模拟实际运行环境上,针对系统的非功能特性所进行的测试,包括负载测试、性能测试、安全测试等。

测试驱动开发

在敏捷方法中,提出测试驱动开发(Test Driven Development,TDD),即测试在先、编码在后的开发方法。TDD 有别于以往的先编码后测试的开发过程,而是在编程之前,先写测试脚本或设计测试用例。这种强调"测试先行“的模式,可以使开发人员对所写的代码有足够的信心,同时也有勇气进行重构。

TDD 的具体实施过程是,在打算添加某个新功能时,先不急着写功能代码,而是将各种特定条件、使用场景等想清楚,然后为待编写的代码先些一段测试用例,接着使用一些测试工具运行这段测试用例,运行的结果自然是失败,此时利用测试工具的错误信息,了解代码没有通过测试的原因,然后有针对性地逐步添加代码,接着再运行测试,不断地修改、添加代码,直至测试用例通过。

TDD 使得开发人员不能再像过去那样随意写代码,要求写的每行代码都是有效的代码。而在此之前,即使代码写完了,编程工作也还没结束,因为还没进行单元测试,经过单元测试后可能还会出现错误,需要再次进行修正。TDD 在于预设各种应用场景、前提条件,促进开发人员思考,写出更完善、更高质量的代码,提高工作效率。

此外,TDD 还可以确保测试的独立性,使测试用例的设计不受实现思维的影响,确保测试的客观和全面。

对于抽象能力高,在编写代码前喜欢先进行各种场景预设、思考前提条件的程序员来说,TDD 无疑是一种福音,但如果你抽象能力不足或急着实现功能,也不必强求,在完成功能之后及时补充单元测试就行。

在软件工程语境下的软件测试的所有相关内容和概念就介绍到这里,软件测试作为软件工程中重要的组成部分,在软件开发中发挥着至关重要的作用,贯穿软件的整个生命周期。理解软件测试的定义、作用及其分类,可以使作为程序员的我们明确自身在软件测试阶段中的定位,了解自身在软件测试过程中所承担的职责和所应完成的任务。希望你能好好理解这些内容。

现在,让我们将目光从软件工程中的测试转移到前端开发的测试中,作为前端程序员,应该做哪些测试以及怎样进行测试呢?

前端程序员所要进行的测试

作为前端开发人员,当构建一个 Web 或其他类型的应用时,从被测试对象的角度,可以进行以下三类测试:

单元测试。前面提到,单元测试的大部分工作应该由开发人员完成,前端程序员也是如此。我们需要对单个独立的函数、类或一个组合式函数、hook 进行测试,将其与应用的其他部分隔离开来。而且应该进行功能测试,侧重于被测单元在功能上的正确性,而非进行兼容性测试、性能测试等其他测试。而且,由于前端应用的特殊性,为了创建一个与外界隔离的环境,我们往往需要模拟应用环境的很大一部分,如第三方模块、网络请求等;* 组件测试。如今大多数 Web 应用都会使用 Vue、React 这类提倡组件化开发的框架进行开发,因此对所编写的组件进行测试在前端测试中应当占据比较大的比重。组件测试需要检查组件是否正常挂载或渲染、是否可以正常交互,以及表现是否符合预期;* 端到端(E2E)测试。当完成单元测试和组件测试之后,我们还需要进行端到端测试,将整个应用部署到实际运行环境或模拟真实运行环境下运行,从用户的视角对整个应用进行从头到尾、端到端的业务测试,确保应用表现符合预定需求。端到端测试除了测试用户界面的真实交互效果和逻辑行为外,还会发起真实的网络请求,向后端的数据库获取数据,并且在真实浏览器上运行,捕捉应用运行出错时的快照、视频等信息。端到端测试可以说是一种系统功能测试。当然,(自动化的)端到端测试也不是非要做,也不是非要前端做,在实际开发过程中还应结合实际情况选择合适的测试方案。除了进行以上三种功能测试外,前端程序员还可进行性能测试,如借助浏览器的 LightHouse、Performance 功能检测页面的渲染、交互性能。还可进行兼容性测试等其他测试。由于不是本文重点内容,就不进行介绍了。

对一个庞大的应用进行单元测试、组件测试和端到端测试,往往需要设计大量的测试用例,执行多次且重复的测试,要想大幅缩短在测试上所花费的时间,自然就需要用到自动化测试,通过使用测试工具、编写测试脚本来提高测试效率,所幸前端领域经过这么多年的发展,在社区上早已出现了很多优秀的开源测试工具。接下来,我将介绍如何利用测试工具进行自动化测试,编写测试脚本,让你全面地入门自动化测试。

前端自动化测试入门

如果现在要你测试以下这个函数,你要怎么做?

function sum(a, b) {return a + b

}

第一步自然是设计测试用例,比如输入 1 和 2,这个函数会输出 3。设计好测试用例之后,当然就要让这个函数跑起来,传入 1 和 2,打印函数返回值看看是否为 3。于是可以写出以下这段测试代码:

console.log(sum(1, 2))

然后运行这段代码,检查打印结果是否为 3。这样,一个测试代码就完成了。当然,这个函数过于简单,用静态测试的方法也能进行测试,这里只是方便举例。除此之外,这段代码运行起来还都需要人工观察运行结果来检验测试成果,这其实也不属于自动化测试的范畴。

当类似的测试做多了之后我们就可以发现一个规律,大多数测试用例,都是设计一个或多个输入数据,以及对应的输出数据,通过传入这些输入数据时被测代码是否产生或返回这些输出数据来判断被测代码是否运行正常,这个判断的过程就叫作断言(assertion)。

断言

Node 的 assert 模块就提供了进行断言的 API,比如使用 equal 方法对上述的 sum 函数进行断言,可以这样:

assert.equal(sum(1, 2), 3)

运行这段代码,如果 sum 函数的实现不符合预期,equal 方法就会抛出一个 AssertionError 错误,并打印详细的错误原因。

除了 Node 提供的 assert 模块外,社区还出现了很多断言库,提供了多样的断言风格,最具代表性的当属 Chai 和 Jest。

Chai

Chai 提供了三种不同的断言风格供用户选择。

assert

assert 风格与 Node 的 assert 模块类似,但是提供了更多 API,并且可以在浏览器上运行:

const assert = require('chai').assert

const foo = 'bar'

assert.typeOf(foo, 'string') // without optional message

assert.typeOf(foo, 'string', 'foo is a string') // with optional message

assert.equal(foo, 'bar', 'foo equal `bar`')

assert 风格的 API 允许使用者传入一个可选的描述断言行为的字符串到最后一个参数,当断言失败后错误信息中就会显示这个字符串。

BDD

BDD 风格提供两类断言:expect 和 should,两者都支持链式调用的语法让使用者可以用一种贴近自然语言的方式进行断言。使用方式如下:

// expect:

const expect = require('chai').expect

const foo = 'bar'

expect(foo).to.be.a('string')

expect(foo).to.equal('bar')

// should:

const should = require('chai').should()

const foo = 'bar'

foo.should.be.a('string')

foo.should.equal('bar')

foo.should.have.lengthOf(3)

仔细观察这两类 API 的使用方式就可以看出差别:使用 expect 时只需将待测结果包裹进 expect() 函数便可进行链式调用,而使用 should 语法时则只需调用 should() 方法就可直接在待测结果上进行链式调用,其原理也很明显:调用should() 函数后在对象的原型上添加了 should() 方法的定义。

Jest

Jest 风格的 API 与 Chai 的 expect 语法类似,但是不提供链式调用,而是直接调用一个方法进行断言:

expect(2 + 2).toBe(4)

expect('How time flies').toContain('time')

expect({a: 1}).not.toEqual({b: 2})

如上例所示,像 toBe()、toEqual 这类对待测内容的某个方面进行断言的方法,称为匹配器(Matcher)。常用的匹配器有 toBe、toEqul、toContain 等等。可以查阅 Jest 的匹配器 API 文档 了解更多内容,匹配器的数量不多,也就不到 40 个,相信你可以轻松搞定,这里就不赘述了。

使用 Jest

通过对单元测试最基本的步骤,即断言的介绍,我们了解了三种断言风格及相应的 API,在具备该编写单元测试的基本能力之后,我们来正式地学习如何使用自动化测试工具来进行单元测试,以 Jest 为例。

Jest 除了是一种断言风格之外,还是一个用于单元测试的测试框架,具备运行测试脚本的能力。它对普通的 JS 项目无需配置,开箱即用,同时支持快照测试、并行测试等优秀能力。

我们来尝试一下使用 Jest 进行单元测试。首先安装 Jest:

npm install jest -D

安装完毕后,我们新建一个 __tests__ 目录,然后创建一个 sum.spec.js 文件。默认情况下当运行测试时 Jest 会自动搜索并运行 __tests__ 目录下的所有 .js, .jsx, .ts 文件和根目录下所有带有 .test or .spec 后缀的文件,所以我们不必手动设置测试文件的位置。

在 sum.spec.js 文件下我们可以输入以下测试代码:

function sum(a, b) {return a + b

}

describe("sum", () => {test("输入 1 和 2,输出 3", () => {expect(sum(1, 2)).toBe(3)})

})

写好测试代码之后,输入以下命令就可以启动 Jest 来运行测试:

npx jest

测试运行完毕后,Jest 就会在控制台输出以下内容表明测试通过:

精彩链接

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。