JavaScript 作为 web 端使用最广泛的编程语言,它是动态语言,缺乏静态类型检查,所以在代码编译期间,很难发现像 变量名写错 , 调用不存在的方法 等错误,除非在运行时才能暴露出来,所以非常有必要有一个测试工具来验证你的代码。
karma 就是在这样的背景下产生的, 它是一个 runner , 旨在帮助开发者简单而又快速的进行自动化单元测试, 目前已经用在很多大型的项目, google 和 油Tube 这些公司都在用它, 在 npm 官方网站上, 它也是一个比较流行的 npm 模块。
下面会讲述 karma 的由来, 以及现有解决方案的比较, 最后会谈及 karma 的设计与实现。
为什么需要测试
下面从两方面来说明
从语言角度
- JavaScript 是动态语言, 缺少类型检查,编译期间无法定位到错误
- JavaScript 宿主的兼容性问题, 比如 DOM 操作在不同浏览器上的表现
从工程角度
- TDD 被证明是有效的软件编写原则,它能覆盖更多的功能接口
- 测试可以 快速反馈 你的功能输出,验证你的想法
- 测试可以保证 代码重构的安全性 , 没有一成不变的代码 ,测试用例能给你多变的代码结构一个定心丸。
- 测试用例其实可以看成 代码 API 使用文档 ,而且还是定时更新的,对 API 的使用者有很大的帮助
- 易于测试的代码,说明是一个好的设计,这里可以稍微说明一下
- 做单元测试之前,肯定要实例化一个东西,假如这个东西有很多依赖的话,这个测试构造过程将会非常耗时,会影响你的测试效率,怎么办呢?要依赖分离,一个类尽量保证功能单一,比如 视图 与 功能 分离,这样的话,你的代码也便于维护和理解。
目前我们面临的现状
相信现在前端开发者对测试都不是很依赖,因为缺少好用的工具,甚至都不会去写测试。
其它语言像 Java, Python ,C# , 测试工具基本都会集成到 IDE 中,基本都能马上看到测试结果,而且像 C++ 这种编译性语言,在 编译 期间基本都会找到代码里的错误
当我们把焦点放在前端开发中的 JavaScript 上来时,上面语言的一些优点完全没有了,关键就是缺少一个流程控制的测试工具。
karma 的目标
- 在真实环境中测试
- 支持远程控制
- 执行速度快
- 可以跟第三方 IDE 进行交互
- 支持 ci 服务
- 高扩展性,支持插件开发
- 支持调试
已有的解决方案
Selenium
Selenium 是一个完整的测试工具,它是一个比较成熟的古老测试工具之一,它比较适合用来做高级测试,比如 e2e 测试。
它的核心思想就是 代理注入 , Selenium 通过代理打开浏览器 URL, 有一个后台的 server 在监听,当有请求时,会往浏览器中注入脚本,之后就通过这个脚本来跟 server 端通讯。
开发者可以使用 Selenium 客户端 API 来编写单测, 比如 跳转 URL 或者 单击按钮
Selenium 客户端有多种实现,比如 Java , Python , Ruby ,开发者可以使用自己熟悉的语言来编写单例
WebDriver/Selenium 2
WebDriver 是基于 HTTP 协议来跟浏览器通讯的, Selenium 2 实现了完整的 WebDriver 协议
WebDriver 可以调用更底层的 DOM API, 像 Safari 不支持这种协议的话,就采用 Selenium 1 那种 代理注入 的方式通讯
Mocha
Mocha 既是测试框架,也是一个测试 runner ,它主要用在 Node.js 里的单元测试,当然也可以用在浏览器端,不过得手动配置各种适配脚本。
JsTestDriver
JsTestDriver 是直接在浏览器里执行的,所以可以直接调用 DOM 和浏览器端 API, 不过先得开启一个服务端程序, 它的核心是一个 runner 程序,当运行单测时,runner 会通知服务端来更新浏览器端,比如重新加载 js 脚本,返回测试结果到服务端。
HTML Runners
大部分的测试框架,像 Jasmine 和 QUnit 都包含一个 HTML runner, 开发者要自己维护加载的前端资源, 手动刷新页面, 测试结果以 HTML 形式显示
对比
下面主要比较下,上面这些工具框架的优缺点
-
Selenium 和 WebDriver 不支持直接访问 JavaScript 代码能力,只是在浏览器里执行,通过各种命令来操作,它的最大优点就是可以访问原生 DOM 事件,这个是 JavaScript 里是很难模拟的,所以这非常适合做 e2e 测试, WebDriver 基本是 e2e 测试的标准了。
-
HTML runners 仅仅是一个测试框架,可以在浏览器端执行然后输出结果,它缺少一种流程管理机制,而且跟第三方编辑器以及 CI Servers 都不能进行通讯
-
Mocha 提供了一种很好的测试控制,但是它比较适合用在 Node.js 环境里,浏览端运行的话,也会产生 HTML runners 类似的问题
-
JsTestDriver 是最接近完美的测试工具了,它可以直接访问浏览器端 API , 而且也有管理工具,可以在命令行里进行控制,但是它有两个主要的问题
- 代码设计有缺陷,不稳定以及不容易扩展
- 没有文件监听,以及文件预处理功能,这些都很影响使用的无缝接入
- Karma 就是对 JsTestDriver 的改进,提供了文件监听以及预处理能力, 文件监听 很重要,它方便与各种编辑器接入, 减少用户运行单测的成本,直接保存文件就可以了, 下面是一张各种工具的对比图
Karma 设计分析
karma 设计目标主要有下面四点:
- 高效
- 扩展性
- 运行在真实设备
- 无缝的使用流程
karma 是一个典型的 C/S 程序,包含 client 和 server ,通讯方式基于 Http ,通常情况下,客户端和服务端基本都运行在开发者本地机器上。
一个服务端实例对应一个项目,假如想同时运行多个项目,得同时开启多个服务端实例。
Server
Server 是框架的主要组成部分之一,它内部保存了所有的程序运行状态,比如 client 连接,当前运行的单测文件,根据这些数据状态,它提供了下面几个功能, 下图是 server 的结构
- 监听文件
- 与 client 进行通讯
- 向开发者输出测试结果
- 提供 client 端所需的资源文件
注意:连接 server 的 client 浏览器有多种类型,比如 PC,iphone,TV 端等
Server 端运行在开发者机器上,根据测试配置文件,它能快速的访问本地测试文件
下面主要说下 Server 端的 4 个组成部分:
1. Manager
Manager 的主要责任就是跟 client 进行通讯,比如广播信号通知 client 开始测试以及收集 client 返回的测试结果
Manager 内部维护了两个数据模型:
- 一个用来保存所有连接的 client 实例(包含浏览器名称和版本号,以及一个标明它是闲置还是当前正在运行的状态)
- 一个用来保存当前测试状态(多少测试成功和多少测试失败)
作为通讯网关,它会利用这两种数据模型去通知服务端其它组成部分,比如测试完成之后,通知输出结果展示。
2. Web Server
基于 connect 的一个 server , 主要是提供访问本地静态资源用的,这里的资源包含:JS 测试框架,断言库,测试用例以及它的依赖等。
3. Reporter
利用上面的测试数据模型,reporter 展示输出结果,输出端包含本地命令行,文件或者一个 ci server。
4. File System Watcher
watcher 主要是监听本地文件改变,内部维护了一个数据模型,包含所有测试相关的文件,它能保证 Web Server 拉取的静态资源都是最新的,同时也能保证文件访问成本以及网络成本,永远只加载修改的文件。
Client
Client 是测试文件真正运行的地方,比如一个 PC,iphone,tablet 端的浏览器,通常情况下跟 server 是同一个物理机,当然也可以运行在不同的机器,通过 HTTP 来通讯
多个 Client 可以与一个 Server 进行通讯
1. Manager
这里主要是跟 server 进行消息通讯,以及与其它 client 组成部分进行交互,比如测试框架 mocha
2. Testing Framework
测试框架不是系统的一部分, karma 灵活支持第三方测试框架,以插件的形式接入。
3. Tests and Code under Test
这里包含用户所有的测试相关文件,它是通过 web-server 模块来获取,测试文件由 test framework 来执行。
Communication Protocal
主要描述 server 与 client 的通讯时机
1. Client to Server Messages
- 当 client 连接以后,会向 server 发送一个唯一标识 id , 以及它的 name 和 version
- 当一个单测文件测试完成之后,会向 server 发送一条消息,消息会包含成功或者失败等
- 当所有单测文件测试完成之后,会向 server 发送一条消息
- 当有 error 产生时,不管是在 client 启动时还是测试代码运行时,都会向 server 发送一条消息
2. Server to Client Messages
- 当 server 决定开始运行单测时,会向所有 client 发送一条消息
Karma 实现分析
Karma 是用 JavaScript 实现的, server 端运行在 Node.js 环境下, client 运行在浏览器环境下, 为什么选择使用 Node.js ,下面是作者的几点看法:
- 支持多个平台
- 用一种编程语言来解决 server 和 client 端的实现
- 它是一个多产的平台( npm 上有很多不错的模块)
- 想深入探索异步编程
上面啰嗦了这么多,终于要说到正题了,下面主要说下 server 和 client 的核心实现
Server
这部分主要说下 server 的核心实现,下图中实线代表直接方法调用,虚线代表通过事件通讯
如图 server 的组成部分
1. File System Watcher
文件监听是基于 chokidar npm 模块来实现的,它是对 Node.js 底层 API 的封装(fs.watch 和 fs.watchFile), 系统启动时, chokidar 根据配置文件会对 glob 形式的文件字符串进行监听
chokidar 屏蔽了不同系统之间的差异问题,而且提供了一些 Node.js 底层 API 不支持的功能,比如对整个文件目录进行监听
2. File System Model
文件模型主要是为了提高访问本地文件以及网络文件的性能, 它包含跟测试相关文件的原数据描述,这些文件是通过 karma.conf.js 配置文件里的 files 字段, glob 形式的文件字符串, 比如 js/src/*.js , test/**/*.js ,来绑定监听的。
当你 添加 , 删除 , 修改 文件,都会触发文件模型里定义的 file_list_modified 事件,该事件有两个 观察者
- web server , 接收到最新的文件数据列表
- manager , 刷新浏览器,重新执行测试文件
内部主要有两个类, FileList , File , 前者包含多个 File 实例, File 单个实例会保存测试文件的一些基本数据,比如路径等。
如下图描述
3. Web Server
这块是基于 connect 来实现的,然后系统提供不同的 handlers 来解决不同的请求,主要有下面四种 handle :
- karma client files(跟 server 端通讯的 JS 文件)
- testing framework and adapter
- source and test files
- proxy
当 web server 请种各种文件时,需要跟上面的 fs model 进行交互,拿到具体的文件原数据之后,做最合适的请求响应
4. Reporter
reporter 会接收来自 client 端的请求,然后展示相对应的单测成功或者失败消息,这块 karma 支持以插件的形式扩展自己想要的 reporter 效果。
Client
client 是单测最终运行的地方,类似一个 web app , 跟 server 端通讯利用 socket.io , 执行单测在一个独立的 iframe 中。下面是它的结构图
1. Manager
client manager 是对 socket.io 调用的一个简单的包装,它提供了一些 API , 给 test framework adapter 来调用, 方便与 server 端通讯, 下面是它的一些 API 列表
manager 运行在 HTML 里的主窗口内,它提供的 API 会暴露给 iframe 里的 test framework adapter 来调用
2. Iframe
client 包含一个独立的 iframe , 它会加载框架自带的 context.html 文件,头部会引用一堆以 script 标签的资源文件,这些文件来源于你的配置文件,通过 web server 里的 handle 来构建完整的 context.html 文件内容。
用独立的 iframe 来执行单测有很多好处,不会污染全局的 window 对象, 方便文件更新后, 重新执行单测
3. Adapter
这是第三方测试框架对应的 karma 适配器,本身不是框架的一部分,是以插件的形式加载到 HTML 里的, 这样就可以保证, 无论什么测试框架, karma 都可以无缝接入, 想要系统适应不同框架的问题,就得提供一套约束出来,想要开发一个适配器,至少得实现 __karma__.start 方法, 这个方法最终会被 manager 来调来, 它是执行本地单测的入口方法,下面是 Jasmine 测试框架的适配器代码
Communication Between Server and Client
client 和 server 端通讯采用 socket.io
- client 端会发送这些消息
- server 端会发送这些消息
Dependency Injection Framework
karma 框架是基于 di 开发的, di 这个 npm 模块也是 karma 作者自己开发的,是一个依赖注入库,这里主要应用了设计模式里的 ioc 原则, 简单的说, 就是函数的参数不是手动实例,而是通过依赖注入进去的,这样可以极大提高系统的扩展性以及灵活性, 而且代码本身更容易测试了,推荐大家在开发复杂系统时,使用这种设计思想。