web-test
# 单元测试
首先声明一点,长期以来,前端开发的单元测试并不是在前端的开发过程中所必须的,也不是每个前端开发工程师所注意和重视的,甚至扩大到软件开发过程中单元测试这一环也不是在章程上有书面规定所要求的。但是随着每个工程的复杂化、代码的高复用性要求和前端代码模块之间的高内聚低耦合的需求,前端工程中的单元测试流程就显得很有其必要。
# 1.前端单元测试是什么
首先我们要明确测试是什么:
为检测特定的目标是否符合标准而采用专用的工具或者方法进行验证,并最终得出特定的结果。
对于前端开发过程来说,这里的特定目标就是指我们写的代码,而工具就是我们需要用到的测试框架(库)、测试用例等。检测处的结果就是展示测试是否通过或者给出测试报告,这样才能方便问题的排查和后期的修正。
基于测试“是什么”的说法,为便于刚从事前端开发的同行的进阶理解,那我们就列出单元测试它“不是什么”:
需要访问数据库的测试不是单元测试
需要访问网络的测试不是单元测试
需要访问文件系统的测试不是单元测试
— 修改代码的艺术
对于单元测试“不是什么”的引用解释,至此点到为止。鉴于篇幅限制,对于引用内容,我想前端开发的同行们看到后会初步有一个属于自己的理解。
# 2.单元测试的意义以及为什么需要单元测试
2.1 单元测试的意义
对于现在的前端工程,一个标准完整的项目,测试是非常有必要的。很多时候我们只是完成了项目而忽略了项目测试的部分,测试的意义主要在于下面几点:
- TDD(测试驱动开发) 被证明是有效的软件编写原则,它能覆盖更多的功能接口。
- 快速反馈你的功能输出,验证你的想法。
- 保证代码重构的安全性,没有一成不变的代码,测试用例能给你多变的代码结构一个定心丸。
- 易于测试的代码,说明是一个好的设计。做单元测试之前,肯定要实例化一个东西,假如这个东西有很多依赖的话,这个测试构7. 造过程将会非常耗时,会影响你的测试效率,怎么办呢?要依赖分离,一个类尽量保证功能单一,比如视图与功能分离,这样的话,你的代码也便于维护和理解。
2.2 为什么需要单元测试
- 首先是一个前端单元测试的根本性原由:JavaScript 是动态语言,缺少类型检查,编译期间无法定位到错误; JavaScript 宿主的兼容性问题。比如 DOM 操作在不同浏览器上的表现。
- 正确性:测试可以验证代码的正确性,在上线前做到心里有底。
- 自动化:当然手工也可以测试,通过console可以打印出内部信息,但是这是一次性的事情,下次测试还需要从头来过,效率不能得到保证。通过编写测试用例,可以做到一次编写,多次运行。
- 解释性:测试用例用于测试接口、模块的重要性,那么在测试用例中就会涉及如何使用这些API。其他开发人员如果要使用这些API,那阅读测试用例是一种很好地途径,有时比文档说明更清晰。
- 驱动开发,指导设计:代码被测试的前提是代码本身的可测试性,那么要保证代码的可测试性,就需要在开发中注意API的设计,TDD将测试前移就是起到这么一个作用。
- 保证重构:互联网行业产品迭代速度很快,迭代后必然存在代码重构的过程,那怎么才能保证重构后代码的质量呢?有测试用例做后盾,就可以大胆的进行重构。
# 3.如何写单元测试用例
3.1 原则
- 测试代码时,只考虑测试,不考虑内部实现
- 数据尽量模拟现实,越靠近现实越好
- 充分考虑数据的边界条件
- 对重点、复杂、核心代码,重点测试
- 利用AOP(beforeEach、afterEach),减少测试代码数量,避免无用功能
- 测试、功能开发相结合,有利于设计和代码重构
3.2 两个常用的单元测试方法论
在单元测试中,常用的方法论有两个:TDD(测试驱动开发)&BDD(行为驱动开发)
对于之前没听说过前端测试这两个模式的同行可以 *在此了解一下*,篇幅限制此处不再敖述。
3.3 相信你看完之后也有一个自己对TDD和BDD的个人观点,在此我先谈谈我对TDD和BDD的 理解:
# TDD(Test-driven development):
其基本思路是通过测试来推动整个开发的进行。
- 单元测试的首要目的不是为了能够编写出大覆盖率的全部通过的测试代码,而是需要从使用者(调用者)的角度出发,尝试函数逻辑的各种可能性,进而辅助性增强代码质量
- 测试是手段而不是目的。测试的主要目的不是证明代码正确,而是帮助发现错误,包括低级的错误
- 测试要快。快速运行、快速编写
- 测试代码保持简洁
- 不会忽略失败的测试。一旦团队开始接受1个测试的构建失败,那么他们渐渐地适应2、3、4或者更多的失败。在这种情况下,测试集就不再起作用
需要注意的是:
- 一定不能误解了TDD的核心目的!
- 测试不是为了覆盖率和正确率
- 而是作为实例,告诉开发人员要编写什么代码
- 红灯(代码还不完善,测试挂)-> 绿灯(编写代码,测试通过)-> 重构(优化代码并保证测试通过)
TDD的过程是:
- 需求分析,思考实现。考虑如何“使用”产品代码,是一个实例方法还是一个类方法,是从构造函数传参还是从方法调用传参,方法的命名,返回值等。这时其实就是在做设计,而且设计以代码来体现。此时测试为红
- 实现代码让测试为”绿灯“
- 重构,然后重复测试
- 最终符合所有要求即:
- 每个概念都被清晰的表达
- 代码中无自我重复
- 没有多余的东西
- 通过测试
# BDD(Behavior-driven development):
行为驱动开发(BDD),重点是通过与利益相关者(简单说就是客户)的讨论,取得对预期的软件行为的认识,其重点在于沟通
BDD过程是:
- 从业务的角度定义具体的,以及可衡量的目标
- 找到一种可以达到设定目标的、对业务最重要的那些功能的方法
- 然后像故事一样描述出一个个具体可执行的行为。其描述方法基于一些通用词汇,这些词汇具有准确无误的表达能力和一致的含义。例如,
expect
,should
,assert
- 寻找合适语言及方法,对行为进行实现
- 测试人员检验产品运行结果是否符合预期行为。最大程度的交付出符合用户期望的产品,避免表达不一致带来的问题
# 4.Mocha/Karma+Travis.CI的前端测试工作流
以上内容从什么是单元测试谈到单元测试的方法论。那么怎样用常用框架进行单元测试?单元测试的工具环境是什么?单元测试的实际示例是怎样的?
首先应该简单介绍一下Mocha、Karma和Travis.CI
**Mocha:**mocha 是一个功能丰富的前端测试框架。所谓"测试框架",就是运行测试的工具。通过它,可以为JavaScript应用添加测试,从而保证代码的质量。mocha 既可以基于 Node.js 环境运行 也可以在浏览器环境运行。欲了解更多可去 官方网站进行学习。其官方介绍为:
Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases. Hosted on GitHub.
**Karma:**一个基于Node.js的JavaScript测试执行过程管理工具(Test Runner)。该工具可用于测试所有主流Web浏览器,也可集成到CI(Continuous integration)工具,也可和其他代码编辑器一起使用。这个测试工具的一个强大特性就是,它可以监控文件的变化,然后自行执行,通过console.log显示测试结果。Karma的一个强大特性就是,它可以监控一套文件的变换,并立即开始测试已保存的文件,用户无需离开文本编辑器。测试结果通常显示在命令行中,而非代码编辑器。这也就让 Karma 基本可以和任何 JS 编辑器一起使用。
Travis.CI: 提供的是持续集成服务(Continuous Integration,简称 CI)。它绑定 Github 上面的项目,只要有新的代码,就会自动抓取。然后,提供一个运行环境,执行测试,完成构建,还能部署到服务器。
持续集成指的是只要代码有变更,就自动运行构建和测试,反馈运行结果。确保符合预期以后,再将新代码"集成"到主干。
持续集成的好处在于,每次代码的小幅变更,就能看到运行结果,从而不断累积小的变更,而不是在开发周期结束时,一下子合并一大块代码。
对于Travis.CI,建议移步到 阮大大和 廖大大的个人网站上学习,两位老师讲的要比我在这儿写的更清晰。
断言库
基本工具框架介绍完毕后,相信稍微了解点测试的同行都知道,做单元测试是需要写测试脚本的,那么测试脚本就需要用到断言库。”断言“,个人理解即为”用彼代码断定测试此代码的正确性,检验并暴露此代码的错误。“那么对于前端单元测试来说,有以下常用断言库:
看一段代码示例:
expect(add(1, 1)).to.be.equal(2);
这是一句断言代码。
所谓"断言",就是判断源码的实际执行结果与预期结果是否一致,如果不一致就抛出一个错误。上面这句断言的意思是,调用 add(1, 1),结果应该等于 2。所有的测试用例(it 块)都应该含有一句或多句的断言。它是编写测试用例的关键。断言功能由断言库来实现,Mocha 本身不带断言库,所以必须先引入断言库。
引入断言库代码示例:
var expect = require(‘chai’).expect;
断言库有很多种,Mocha 并不限制使用哪一种,它允许你使用你想要的任何断言库。上面代码引入的断言库是 chai,并且指定使用它的 expect 断言风格。下面这些常见的断言库:
# node assert
- assert(value[, message])
- assert.ok(value[, message])
- assert.equal(actual, expect[, message])
- assert.notEqual(actual, expected[, message])
- assert.strictEqual(actual, expect[, message])
- assert.notStrictEqual(actial, expected[, message])
- assert.deepEqual(actual, expect[, message])
- assert.notDeepEqual(actual, expected[, message])
- assert.deepStrictEqual(actual, expect[, message])
- assert.notDeepStrictEqual(actual, expected[, message])
- assert.throws(block[, error][, message])
- assert.doesNotThrow(block[, error][, message])
# assert(value[, message])
断言 value 的值是否为true,这里的等于判断使用的是 而不是 =。message 是断言描述,为可选参数。
|
|
# assert.ok(value[, message])
使用方法同 assert(value[, message])
。
# assert.equal(actual, expect[, message])
预期 actual 与 expect值相等。equal用于比较的 actual 和 expect 是基础类型(string, number, boolearn, null, undefined)的数据。其中的比较使用的是 而不是 =。
|
|
# notEqual(actual, expected[, message])
用法同 assert.equal(actual, expect[, message])
只是对预期结果取反(即不等于)。
# assert.strictEqual(actual, expect[, message])
用法同 assert.equal(actual, expect[, message])
但是内部比较是使用的是 = 而不是 。
# assert.notStrictEqual(actial, expected[, message])
用法同 assert.strictEqual(actual, expect[, message])
只是对预期结果取反(即不严格等于)。
|
|
# assert.deepEqual(actual, expect[, message])
deepEqual 方法用于比较两个对象。比较的过程是比较两个对象的 key 和 value 值是否相同, 比较时用的是 而不是 =。
|
|
# assert.notDeepEqual(actual, expected[, message])
用法同 assert.deepEqual(actual, expect[, message])
只是对预期结果取反(即不严格深等于)。
# assert.deepStrictEqual(actual, expect[, message])
用法同 assert.deepEqual(actual, expect[, message])
但是内部比较是使用的是 = 而不是 。
# assert.notDeepStrictEqual(actual, expected[, message])
用法同 assert.deepStrictEqual(actual, expect[, message])
只是对结果取反(即不严格深等于)。
# assert.throws(block[, error][, message])
错误断言与捕获, 断言指定代码块运行一定会报错或抛出错误。若代码运行未出现错误则会断言失败,断言异常。
|
|
# assert.doesNotThrow(block[, error][, message])
错误断言与捕获, 用法同 throws 类似,只是和 throws 预期结果相反。断言指定代码块运行一定不会报错或抛出错误。若代码运行出现错误则会断言失败,断言异常。
|
|
# Mocha
- 安装mocha
|
|
当然也可以在不在全局安装,只安局部安装在项目中
|
|
- 创建一个测试文件
test.js
|
|
这段文件和简单就是测试 Array
的一个 indexOf()
方法。这里我是用的断言库是 Node 所提供的
Assert
模块里的API。这里断言 -1 等于 数组 [1, 2, 3]
执行 indexOf(-1)
后返回的值,如果测试通过则不会报错,如果有误就会报出错误。
下面我们使用全局安装的 mocha
来运行一下这个文件 mocha test.js
。
下面是返回结果
基础测试用例实例
|
|
Mocha 测试用例主要包含下面几部分:
- describe 定义的测试套件(test suite)
- it 定义的测试用例(test case)
- 测试代码
- 断言部分
说明:每个测试文件中可以有多个测试套件和测试用例。mocha不仅可以在node环境运行, 也可以在浏览器环境运行;在node中运行也可以通过npm i mocha -g
全局安装mocha然后以命令行的方式运行测试用例也是可行的。
这里略微详细介绍下测试脚本写法
Mocha 的作用是运行测试脚本,首先必须学会写测试脚本。所谓"测试脚本",就是用来测试源码的脚本。下面是一个加法模块 add.js 的代码。
|
|
要测试这个加法模块是否正确,就要写测试脚本。通常,测试脚本与所要测试的源码脚本同名,但是后缀名为.test.js(表示测试)或者.spec.js(表示规格)。比如,add.js 的测试脚本名字就是 add.test.js。
|
|
上面这段代码,就是测试脚本,它可以独立执行。测试脚本里面应该包括一个或多个 describe 块,每个 describe 块应该包括一个或多个 it 块。
describe 块称为"测试套件"(test suite),表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称(“加法函数的测试”),第二个参数是一个实际执行的函数。
it 块称为"测试用例"(test case),表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称(“1 加 1 应该等于 2”),第二个参数是一个实际执行的函数。
expect 断言的优点是很接近自然语言,下面是一些例子。
|
|
基本上,expect 断言的写法都是一样的。头部是 expect 方法,尾部是断言方法,比如 equal、a/an、ok、match 等。两者之间使用 to 或 to.be 连接。如果 expect 断言不成立,就会抛出一个错误。事实上,只要不抛出错误,测试用例就算通过。
|
|
上面的这个测试用例,内部没有任何代码,由于没有抛出了错误,所以还是会通过。
# Karma
基于 karma 测试常用的一些模块
# 模块安装
|
|
# 配置
这里的配置主要关注的是karma.conf.js
的相关配置。如果要使用 karma 和 mocha 最好通过npm install karma-cli -g
全局安装karma-cli
。
具体配置配置说明
需要注意的两个字段:
- singleRun: 如果值为 true, 则在浏览器运行完测试后会自动退出关闭浏览器窗口。singleRun的值我们可以更具运行环境来动态赋值, 可以启动命令中添加
NODE_ENV
变量。 - browsers: 浏览器配置(可以配置多个浏览器); 如果浏览器无法启动需要进行相关浏览器的配置。设置自启动浏览器时候如果浏览器启动失败可能需要设置为
--no-sandbox
模式。
|
|
或者
|
|
# Travis.CI集成自动化测试
在github创建并完成一个可以待测试的项目。这里的完成是指需要完成基本的项目功能,和测试用例代码。
配置travis-ci能识别读取的配置文件,这样travis-ci接入的时候才能够知道测试时的一些配置。
github 和 travis-ci 是个站点,换句话说就是两个东西如果能打通呢。需要用户登录 travis-ci 并授权访问到你的 github 项目并进行相关的项目设置。
接入完成后就可以根据自己的需要来运行写好的测试代码,也可以设置定期任务去跑测试。
# 项目创建、完善项目功能和测试代码。
- 项目需求: 实现一个求和方法
- 测试: 通过
mocha
来测试完成的求和方法。
下面是项目结构,项目创建完成后通过 npm i mocha -D
安装 mocha
模块。然后在本地运行 npm test
看是否能够测试通过。如果能够测试通过则说明我们的可以继续下一步了。
# 创建 travis-ci 测试配置文件
创建
travis-ci配置文件 .travis.yml
, 文件内容。更多关于配置文件的说明在travis官网可查询
|
|
至此基本完成了项目开发和测试代码编写的过程,下一步就可以接入 travis-ci测试了。
# 接入 travis-ci
通过GitHub登录 travis-ci 的官网 www.travis-ci.org/
找到GitHub上刚才创建的需要测试的项目,并开启测试
查看测试过程,及时发现问题。
查看测试状态是否通过测试,如果未通过及时排查问题反复修改;如果通过可以在项目文档中添加一个测试通过的标识。
# 总结
但是同时,在我个人观点范畴内,至少目前我还是坚持开发为主测试为辅的流程,对于像TDD这种单元测试指导开发流程,目前并不推崇。个人认为,这是一个很有创新性的方法论,也并不是现在完全不可行,个人认为只是可行的范畴还不够宽,可行的条件要求还很严苛。所以相对于TDD,测试主导开发,对于目前准备进阶的前端开发者,个人更建议,了解某种以后会使用的新趋势和技术是有必要的,但作为技术人应该在学习新的、前卫的技术的同时不可迷失自我一味追求新技术,更重要的是要磨练当下的主流技能。相比于未来的单元测试主导开发流程,倒不如在目前这个时间节点精进基础开发流程,比如让自己的JS代码更专注于模块化和功能化的实现,这样的同时也会让单元测试更有效率,真正发挥目前单元测试对前端工程化的作用。
# A/B 测试
所谓 A/B 测试,简单来说,就是为同一个目标制定两个方案(比如两个页面),让一部分用户使用 A 方案,另一部分用户使用 B 方案,记录下用户的使用情况,看哪个方案更符合设计目标。
# 基本概念
网站设计中,我们经常会面临多个设计方案的选择,比如某个按钮是用红色还是用蓝色,是放左边还是放右边。传统的解决方法通常是集体讨论表决,或者由某位专家或领导来拍板。虽然传统解决办法多数情况下也是有效的,但A/B 测试(A/B Testing)可能是解决这类问题的一个更好的方法。
所谓 A/B 测试,简单来说,就是为同一个目标制定两个方案(比如两个页面),让一部分用户使用 A 方案,另一部分用户使用 B 方案,记录下用户的使用情况,看哪个方案更符合设计目标。当然,在实际操作过程之中还有许多需要注意的细节。
A/B 测试最核心的思想,即:
- 多个方案并行测试;
- 每个方案只有一个变量不同;
- 以某种规则优胜劣汰。
需要特别留意的是第 2 点,它暗示了 A/B 测试的应用范围,——必须是单变量。
另外,虽然 A/B 测试名字中只包含 A、B ,但并不是说它只能用于比较两个方案的好坏,事实上,你完全可以设计多个方案进行测试,比如ABC测试,“A/B 测试”这个名字只是一个习惯的叫法。
回到网站设计,一般来说,每个设计方案应该大体上是相同的,只是某一个地方有所不同,比如某处排版、文案、图片、颜色等。然后对不同的用户展示不同的方案。
要注意,不同的用户在他的一次浏览过程中,看到的应该一直是同一个方案。比如他一开始看到的是 A 方案,则在此次会话中应该一直向他展示 A 方案,而不能一会儿让他看 A 方案,一会儿让他看 B 方案。
同时,还需要注意控制访问各个版本的人数,大多数情况下我们会希望将访问者平均分配到各个不同的版本上。要做到这些很简单,根据 cookie (比如 cookie 会话ID的最后一位数字)决定展示哪个版本就是一个不错的方法。
要实现 A/B 测试,我们需要做以下几个工作:
1、开发两个(或多个)不同的版本并部署;
2、收集数据;
3、分析数据,得出结果。
# 实践方法
从左到右,3条较粗的竖线代表了 A/B 测试中的3个关键角色:客户端(Client)、服务器(Server)、数据层(Data)。从上到下代表了3种访问形式:
- 无 A/B 测试的普通访问流程(Non AB test)
- 基于后端分流的 A/B 测试访问流程(Back-end AB test)
- 基于前端分流的 A/B 测试访问流程(Front-end AB test)。
A/B 测试需要将多个不同的版本展现给不同的用户,即需要一个“分流”的环节。从上图中我们可以看到,分流可以在客户端做,也可以在服务器端做。
传统的 A/B 测试一般是在服务端分流的,即基于后端的 A/B 测试(Back-end AB test),当用户的请求到达服务器时,服务器根据一定的规则,给不同的用户返回不同的版本,同时记录数据的工作也在服务端完成。
基于后端的 A/B 测试技术实现上稍微简单一些,**不过缺点是收集到的数据通常是比较宏观的PV(Page View)信息。**虽然可以进行比较复杂的宏观行为分析,但要想知道用户在某个版本的页面上的具体行为往往就无能为力了。
**基于前端的 A/B 测试则可以比较精确地记录下用户在页面上的每一个行为。**它的特点是,利用前端 Java 方法,在客户端进行分流,同时,可以用 Java 记录下用户的鼠标行为(甚至键盘行为,如果需要的话),直接发送到服务器记录。
下面,我将重点介绍一下我们在基于前端的 A/B 测试上的一些实践。
首先遇到的问题是如何分流的问题。对于大部分需求来说,我们希望各个版本的访问人数平均分配。可以根据某一个 Cookie ID 来划分用户,比如“123.180.140.*.1267882109577.3”,可以根据这个 Cookie ID 的最后一位(在本例中是“3”)来划分人群,比如单数的显示 A 版本,偶数的显示 B 版本。
正确展示对应的版本后,就要开始采集需要的数据了。当前版本有多少 PV (Page Views,访问量),如果需要记录这个数据的话,在正确版本加载完成之时就要发送一个打点信息。不过很多需求中,具体版本的 PV 的精确数值可能不是很重要,而且要收集这个信息需要多一次打点操作,所以一般情况下这个数据是可选的。
必须的数据是测试区域内用户的点击信息。当用户在测试区域点击了鼠标左键(无论这个点击是点击在链接、文字、图片还是空白处),我们就需要发送一条对应的打点信息到打点服务器。一般来说,这个打点信息至少需要包含以下数据:
- 当前 A/B 测试以及版本标识
- 点击事件的位置
- 点击时间戳(客户端时间)
- 当前点中的URL(如果点在非超链接区域,此项为空)
- 用户标识(比如 Cookie ID)
- 用户浏览器信息
# 应用例子
用AB测试的核心思想分析下:
- 两个方案并行测试(符合);
- 每个方案只有一个变量不同(按钮的文案不同,符合);
- 以某种规则优胜劣汰(最终转化率的高低,符合)
注意:有人认为按钮的点击率是最终的衡量指标,其实不是,有时候点击率提高了,但转化率反而会降低,所以这里最终转化率是衡量指标。
举个例子:你的按钮文案是“点击领一百元红包”,点击率自然会提高,但用户点击后发现并没有一百元,就走了,导致转化率下降。
言归正传,结果数据显示,A版本的转化率为5.8%,B版本的转化率为8.2%。在没做这个测试之前,你知道“加入学习”好还是“立即参加”好吗?
这几乎是一个完美的 A/B 测试案例:有明确的测试目标,清晰的衡量标准(订单转化率),以及完美的结果数字。