JavaScript单元测试基础

软件单元测试可分为测试单元的构建,和测试程序的编写,两步。一般软件测试是指测试程序的编写,默认可用的测试单元,这个默认条件对【JS单元测试】来说常常不充分。JS单元测试则有一个构建测试单元的前置任务,本文简单介绍了使用了【代码重构(refactoring)】的技术,来完成这项前置工作,最后还试编写了一个测试程序,引出【单元测试框架】的概念。本文略译自《Introduction To JavaScript Unit Testing

Introduction To JavaScript Unit Testing

你可能意识到「软件测试」在软件生产中(包括JS开发)的好处;从软件测试活动的角度看(有别软件功能开发的软件构建任务),前端JS的运行模式先天不具可测试性。

JS代码「缺乏可测试性」

在前端的生产环境里,最大的问题是前端JS代码「缺乏可测试性」,这主要因为JS代码散落到程序中各个页面当中,需要在网络中下载到浏览器运行,JS代码会依赖服务端逻辑,又与HTML有关联。更糟的是,JS代码还会与HTML标签混合在一起,例如inline events handlers。

EM:软件功能测试只是一种特殊的“使用”,一种有计划的“功能使用”,“使用”——将程序功能运行起来就是具有可测试性了。然而,JS程序形式很松散,运行条件比较费准备成本。

EM:writing a uint test? build test uint first! 准备一个可测试的单元,是编写测试程序的前提。准备测试单元,和编写测试程序任务不同。

JS代码「缺乏可测试性」,和没有完整的测试单元,在不使用「DOM操作库」时表现特别明显,想一想直在HTML标签定义事件处理函数,比通过DOM API绑定要容易得多。幸好,越来越多的开发者使用像jQuery这样的库来对DOM操作进行抽象,将DOM操作代码独立成一个scripts(scripts标签,或者scripts文件)。不过,将代码独立成文件并不代表这些代码「具备可测试性」。独立只是一个可测试的一个基础条件。

理想的测试单元

那么,到底「测试单元」是指什么呢?在理想的情况下,「测试单元」是一个纯功能函数,并且你很方便的“使用它”——给出计算输入,产生一个固定的计算输出。「理想的测试单元」因为很容易“使用”,所以也很容易“测试”。而大多数实际的代码中,代码与环境有关联,我们必须处理副作用(side effects),例如JS代码输入输出都要处理DOM的(不是纯粹的输出一些简单值)。当然,如果我们掌握了「测试单元和测试活动」的实质,手动去构建测试单元也就不难。

创建测试单元

由此可见,由简入繁,学习JS单元测试(手动构建具有测试性的单元)从零开始会更容易一些。不过,本文并不讲从零开始,而是讲如何从现在有代码中抽取出可测试的单元,从中测试出可能存在的bugs。

从现有的代码中抽取出部分代码,改变它的形态但不改它的计算行为,这个过程叫【代码重构(refactoring)】。代码重构是改善现有代码设计的有效办法,然而对于复杂的代码,重构有损害代码功能的风险。原地测试是最安全的,但是可能不具有可测试性,重构测试能提高可测试性,则有损害的风险,具体怎么做,取决于你的软件测试知识和经验。

EM:你对软件本体的认识,测试活动操作的认识。

理论暂到此,我们来看一个实例,看如何测试一个依赖页面输出的JS代码块。

一个依赖页面输出的JS代码块

页面上有一系列的帖子(posts),每个帖子都显示了具体的发表时间,代码任务是,将所有帖子的具体发表时间转换为一个相对的更友好的时间描述,像“5 days ago”:

Mangled date examples

这段「测试程序」代码是一段针对prettyDate特制(ad-hoc )的,其中包含了全套通用测试框架(testing framework)的形式部分,例如测试程序主体(test),测验输入(then)输出(result),测验的参考“预期答案”(expected),对输出的断言操作(!==),测试报告等。从代码看到整个测试的流程(测试程序的执行):一次测验会用输入(then)调用功能函数,接收输出(result)并进行断言(!==expected),最后记录测试结果用于报告。一次测试由多个测验组成。

这段特制的测试框架代码脱离了对DOM的依赖,例如使用控制台作为测试输出,它可运行在非浏览器环境中,例如 Node.js 或 Rhino。

如果测试过程有发现“不通过”,它详细的报告哪一条“答案”(expected)不对,和实际测试的结果(actual result )。在测试最后,测试框架会汇总所有结果,包含测试次数(total),测试通过量(passed)和未通过量(failed)。

如果全部测验都通过了,你将会在控制台看如下报告输出:

Of 6 tests, 0 failed, 6 passed.

要试下看看不通过的样子,你可故意调一些错误,报告输出是这样的:

Expected 2 day ago, but was 2 days ago.

Of 6 tests, 1 failed, 5 passed.

这段特制的测试实验只是用来验证一些关键概念,证明一个测试运行器( test runner)只有数行代码而已,在实际生产中,我们会使用全套的测试框架(unit testing framework),来【编写测试程序】,使用它提供的运行器,报告工具进行【程序单元测试】。

裸男
Nakeman.cn 2023 Build by Gatsby and Tailwind, Deploy on Netlify.