[Day25] 测试一定要写好写满?时间有限怎麽办?

既然要写测试,就先来了解前端常见的几种测试类型,从最大家最常听到的单元测试(Unit Testing)、到会整合不同 API 或元件互动的整合测试(Integration Testing),最後则是模拟使用者操作的 End to End Testing。

不同测试类型会搭配不同的测试工具,以单位测试来说,目前在前端最常用的是由 Facebook 推出的 Jest,它是 Node-based 的执行器,主要用来进行元件或函式的单元测试(unit test);若有需要针对页面上的元素或 React 元件进行整合测试,笔者自己习惯用的是 test-library 上的 react-testing-library。最後,若是需要的进行更贴近浏览器环境的 end-to-end 测试的话,则可以使用 cypresspuppeteer

测试的不同类型

根据不同的测试目的,会分成不同的测试类型,简单来说可以分成三类,但笔者认为这三类的区隔并非一定壁垒分明的,是其中一种就不会是另一种。不论是哪一类型的测试,核心概念都是透过「预期结果(expect)」和「真实结果」去做比对,看看得到的真实结果是不是如同开发者所预期的,因此,如果你连预期会是如何都无法想像的话,那是完全没办法进行测试的

单元测试(Unit Test)

测试的对象会是程序码中最小的单元,通常会是自己撰写的函式(function)、方法(method)、类别(classes)等等,也就是你预期这个 input 进去後,应该会得到什麽样的 output。举例来说,根据 LeetCode 的题目写出 function 後,LeetCode 就会对你写的 function 做许多的验证,确保你写的 function 在各种情况下都能满足题目的需求,而针对个别 function 进行测试的情况就是所谓的单元测试(Unit Test)。

这部分前端来说最常使到的工具是由 Facebook 推出的 Jest;後端则很常使用 mochajs 搭配 chai

整合测试(Integration Test)

整合测试顾名思义就是需要「整合」,这表示测试的过程不是单一函式就能满足,过程中可能会需要呼叫 API 获取资料、使用其他的 library、或者和 DOM 进行整合,预期 DOM 上应该会呈现特定的 element。以 React Component 的测试来说,就比较接近这类型的测试,因为在 React Component 中,可能会去 fetch API 取得资料,取得资料後需要将资料呈现在 DOM 上。这时候如果是撰写整合测试的话,就需要写 mock data 来假设 API 回传的资料内容,并在取得假资料後,检测 DOM 有没有如同预期的呈现出 element;这个过程中,也可以以程序的方式模拟使用者点击、输入内容的动作。

这部分以 React 来说最常提到的应该是 Testing Library 搭配 React Testing Library;或 enzyme

End-to-end (E2E) tests

相较於 Unit Test 是测试单一逻辑、Integration Test 是测试整合多个逻辑下的情况、End-to-end (E2E) 则算是最贴近去模拟使用者操做实际产品的过程,透过 E2E Test,你可以撰写使用者操作的流程,并透过浏览器画面实际看到页面被操作的过程,你可以想像成有一个使用者真的打开了浏览器,从浏览器输入网址,接着进入网页後进行後续对应的流程。

这部分前端最常听到的是 cypress 或是 Google 推出的 Puppeteer

不同测试类型的考量

不同类型的测试自然对应到不同的使用时机和情境,也不是说总是把测试补到最齐最满一定是最好的做法,简单来说,资源有限而测试案例常常是无穷的,为了在有限的资源中(开发能力、开发时程)能确保程序码的品质,取舍要做哪些测试往往是更实际的。

在上述不同的测试类型中,Unit Test 可以算是第一道防线,也是通过测试後,比较不会因为其他功能更动而会坏掉的。你可以想像有一个用来验证表单栏位是否为空的 function,这个 function 的验证逻辑一旦写好後,并不会栏位名称不同、或表单的 UI 改动後,验证的逻辑就有不同。

但以同样确认表单栏位必填的功能来说,如果你做的是 integration test 或 E2E test,就有可能因为画面改变、後端 API 回传的资料改变,而导致测试结果失败,因为在 integration test 或 E2E test 中,除了会想要需要验证使用者该栏位是否漏填外,可能还会同时检查漏填时,画面应该要跳出的提示讯息。这时候,一旦 UI 修改後,最後提示的文字内容有异动(例如,原本显示 Please enter your name,UI 修改後希望显示 Require to enter your name),或者是 API 回传的资料有变更时,都可能导致 integration test 和 E2E test 有错误。

Unit Test

另外,Unit Test 作为第一道防线,也表示这通常是最容易找到问题「核心」的地方。举例来说,如果想要验证的是登入功能,使用的是 E2E Test,这时当 E2E Test 失败,但若没有搭配 Unit Test 的话,你就会像一般的使用者一样只知道无法成功登入,但却不知道为什麽不能登入;但若有搭配 Unit Test 的话,则会比较容易发现无法成功登入的原因。

不同的测试除了容易发现的问题不同之外,执行的时间(execution time)也不同,E2E Test 通常在测试上会花上的时间也做多,投入的开发成本(Development Cost)也最高,因为只要设计或 UI 一有变动,E2E Test 必然会需要修改。

然而,虽然 E2E Test 的开发成本、投入时间相对来说都比 Unit Test 来得高,但这并不表示 E2E Test 就不重要或不值得都入,因为很多时候,使用者在操作时之所以会碰到问题,是因为各个模组之间的交互作用导致,也就是个别模组独立运作时是没问题的,但一但整合再一起就有开发者意想不到的情况;又或者,是使用者实际的操作下可能产生的问题,例如,使用者因为忘了某些资讯而先按了上一页後,接着在回到下一页(原本的页面)时,可能会因为快取等状况而发生一些在做 Unit Test 时意想不到的情况。

Testing Pyramid

上面的这些概念可以整理成一个经典的「测试金三角(Testing Pyramid)」,上面提到三种不同的测试类型它们分别位於该三角形中的不同位置,在三角形中越上方所需耗费的成本越高、执行的时间通常会更长,且需求有任何变动通常都需要修改测试,但却更有机会找到意料之外的错误。

testing pyramid

图片来源:Automation Panda

时间有限该先写哪一种?

以笔者的角度来说,并非所有的 function 或 component 都一定要写一个对应的测试,毕竟时间和资源有限的情况下势必要有所取舍。当产品还没开发出来,下个月的薪水都还没着落时,却还在一一写每个 function 或 component 的 test case 时,自己可能也很难安心。

在时间资源有限的情况下,我会鼓励先做「单元测试」,它们对於程序可维护性的提升都能有相当的帮助,特别是会被多个不同开发者或多个不同模组使用到的共用函式,昨天提到,撰写测试的好处包括「测试程序本身就有辅助文件的效果」,因为在测试的程序中提供了许多范例让其他开发者可以更好理解这个方法的使用,此外未来若有重构或需求变更时,可以让你自己在程序码在改动後保有一定程度的信心。

除了单元测试外,若你的应用程序本身包含相当复杂的商业逻辑,这些复杂的商业逻辑,不是单纯那种「使用者登入後就看不到『登入按钮』」这麽直觉的逻辑,而是其他人看了程序码後第一眼可能也无法理解为什麽的部分,例如,使用者看到的价格取决於使用者的等级、年龄、性别等等,一般人第一时间也无法马上理解的商业逻辑,这种则会很建议可以使用整合测试或 E2E 测试来保护起来。如此,一开始开发好後就可以用测试检验自己是否有依照规格正确实作了商业逻辑,提升对自己程序的信心,再来则是在未来功能添加、需求改动、或是程序重构时,不会因为不小心忽略而改掉了原本的逻辑而不自知。

参考资料


<<:  【Day 25】Go 与 Python gRPC 小练习

>>:  DAY25 在React中加入CSS

Day13 javascript 类型转换

JavaScript 变数可以转换为新变数或其他资料类型,就目前我所知道的大概可以分成两种: 1.通...

[Angular] Day5. Lifecycle hooks

在 Angular 的 Component 中有一个生命周期,当 Angular 实例化这个 Com...

Day 05-选择React & Redux

!前提小补充! UI: User Interface(使用者介面),设计页面,须注意到网页页面使用的...

Day24 jQuery 基本教学(四)

CSS 与特效 JQ 的特效主要是协助快速控制 CSS,包含控制了你的 display 做显示或隐藏...

20 - Traces - 观察应用程序的效能瓶颈 (4/6) - 使用 APM Server 来收集 APM 数据

Traces - 观察应用程序的效能瓶颈 系列文章 (1/6) - Elastic APM 基本介绍...