使用 Karma + Jasmine 构建 Web 测试环境
简介
现在,越来越多的 Web 框架涌现了出来,令人眼花缭乱。随着浏览器的不断演化,一些前端框架开始尝试将服务器端的功能与技术在浏览器端实现,比如 AngularJS , ExtJS 这样的 MVC 框架等等。随着前端代码的复杂性越来越高,以及代码量的突然陡增,如何来保证代码的正确性?如何在 Web 端编写并运行测试用例? Jasmine 测试框架的出现,无疑缓解了开发者们的忧虑。
然而,Web 的开发总是有它的特殊性。例如,页面往往需要运行在多种设备(桌面,移动,掌上等)之上,那么如何在这些设备上测试代码呢?Karma 测试平台,就是来提供这样的解决方案的。
作为程序员,不光要能够编写测试用例,还需要了解如何运行它们。本文围绕着上面提出的这些问题,向大家介绍这两款优秀的测试框架与运行平台。
Jasmine 简介
Jasmine 框架介绍
Jasmine是一个针对 JavaScript 的行为驱动开发的测试框架,不依赖于任何其他的 JavaScript 框架或者文档对象模型(DOM),最新版本改进了对 Node.js 的支持,同时还做了一些提升内部质量的工作。
图 1. Jasmine 框架
它的主要特点:
- 不依赖于任何其它的 JavaScript 框架
- 不需要 DOM
- 结构简单
- 可以运行在 Node.JS 或者 Html 中
- 基于行为驱动开发 Jasmine
项目Git 仓库地址
获取安装包
可以在开源社区网站下载最新的 Jasmine 安装包, 目前的 Standalone 的最新版本是 2.8.0. 下载地址
也可以使用 npm 命令安装(需要先安装 Node.JS)
npm install --save-dev jasmine
配置安装
将下载后的.zip 文件包解压缩,如下的目录结构:
图 2. 目录结构
目录结构
其中 lib 文件夹中包含 Jasmine 的源代码。SpecRunner.html 是 Jasmine 的一个完整示例,用浏览器打开 SpecRunner.html,即可看到执行的结果。
Jasmine 的基本概念
Suites & Specs
Suites 用来表示测试集的概念,它由很多的测试用例构成。Specs 就是组成它的具体的测试用例,每一个 Spec 就是一个测试用例,用于测试应用中的某个功能。
- Jasmine 使用全局函数 describe 来描述测试集( Suites )。通常来说它有 2 个参数:字符串和方法。字符串就是特定测试集( Suite )的名字或者标题,而方法是实现 Suite 的具体代码。
- Jasmine 使用全局函数it 来描述测试用例(Specs)。和 describe 类似,it 也有 2 个参数:字符串和方法。字符串是对特定测试用例(Spec)的描述,而方法是具体的测试代码。
Expectations
Jasmine 使用断言(Expectation)的方式来执行测试结果,每个 expectation 可以是 true 或者 false。如果 it 方法中的所有测试结果都是 true,则通过测试。反之,有任何一个断言是 false,则测试失败。
- Expectations 由方法 expect 来定义,它的参数是一个具体的值,这个值被称为实际值。同时,在 expect 方法后需要跟某个匹配方法(Matcher),而匹配方法中的值称为期望值。
Matchers
每个 Matcher 的返回结果是一个布尔值,它的作用就是在实际值和期望值之间作比较。同时它负责通知 Jasmine,此次 expectation 的执行结果。Jasmine 会裁定相应的测试用例是通过还是失败。任何的 Matcher 在调用方法之前,都可以使用 not 来"装饰",从而改变匹配结果。
Setup and Teardown
为了使每一个测试用例,都可以重复的执行 setup 与 teardown 代码,Jasmine 提供了全局的 beforeEach 和 afterEach 方法。beforeEach 方法会在每一个测试用例执行前运行,而 afterEach 方法在每个测试用例执行后被调用。
如果在测试用例中使用到多个相同的变量,我们可以在全局的 describe 代码块中定义这些变量,而将变量的初始化代码放在 beforeEach 方法里,并在 afterEach 方法中重置这些变量的值。
嵌套的 describe
Jasmine 支持 describe 嵌套。很显然,这个时候的测试集呈现树状组织结构。而 Jasmine 执行时会遍历树状结构,按顺序执行每个 beforeEach 方法,it方法,以及对应的 afterEach 方法。
跳过测试代码块
Suites 和 Specs 分别可以用 xdescribe 和 xit 方法来禁用。运行时,这些 Suites 和 Specs 会被跳过,也不会在结果中出现。
使用 Jasmine 编写测试用例
下面,我们从一个实际的需求出发,学习如何使用这些方法来编写测试用例。
在开始之前,我们需要先定义应用模型(Model),模型的作用除了定义数据结构,也定义了测试用例的范围。也可以说,这些测试用例其实就是围绕着这些模型展开的。
清单 1. Mode.js
function Student(id, name){
this.id = id;
this.name = name;
this.age = -1;
this.teacher = null;
}
function Teacher(id, name){
this.id = id;
this.name = name;
this.age = -1;
}
function SchoolService(){
this.getTeachers = function(){
var teachers = [];
for(var i=0;i<5;i++){
var teacher = new Teacher();
teacher.id = "01" + i;
teacher.name = "teacher" + i;
teachers.push(teacher);
}
return teachers;
}
this.getStudents = function(){
var students = [];
for(var i=0;i<10;i++){
var student = new Student();
student.id = "00" + i;
student.name = "student" + i;
var teacher = new Teacher();
var tId = Math.ceil(i/2);
teacher.id = "01" + tId;
teacher.name = "teacher" + tId;
student.teacher = teacher;
students.push(student);
}
return students;
}
this.getTeacher = function(tId){
var r = null;
var teachers = this.getTeachers();
for(var i=0;i<teachers.length;i++){
var teacher = teachers[i];
if(teacher.id == tId){
r = teacher;
break;
}
}
return r;
}
this.getStudent = function(sId){
var r = null;
var students = this.getStudents();
for(var i=0;i<students.length;i++){
var student = students[i];
if(student.id == sId){
r = student;
break;
}
}
return r;
}
this.getStudents = function(tId){
var r = [];
var students = this.getStudents();
for(var i=0;i<students.length;i++){
var student = students[i];
if(student.teacher && student.teacher.id == tId){
r.push(student);
}
}
return r;
}
}
这里,我们分别定义了 Teacher 与 Student 类,以及对外提供的 SchoolService 类。不难看出 SchoolService 类就是我们需要重点测试的对象。下面,我们来编写测试用例
首先,我们需要测试 getTeachers() 方法
清单 2. 测试 getTeachers 方法
describe("A test suite for SchoolService", function() {
var schoolService = new SchoolService();
it("Spec test 1, test the getTeachers function", function() {
var teachers = schoolService.getTeachers();
expect(teachers).not.toBe(null);
expect(teachers.length).toEqual(5);
});
it("Spec test 2: test the getStudents function", function() {
var students = schoolService.getStudents();
expect(students).not.toBe(null);
expect(students.length).toEqual(10);
});
});
接下来,使用较为复杂的 Matchers 测试 getTeacher() 方法
清单 3. 测试 getTeacher() 方法
describe("A test suite for SchoolService", function() {
var schoolService = new SchoolService();
it("Spec test 1, test the getTeachers function", function() {
var teachers = schoolService.getTeachers();
expect(teachers).not.toBe(null);
expect(teachers.length).toEqual(5);
});
it("Spec test 2: test the getStudents function", function() {
var students = schoolService.getStudents();
expect(students).not.toBe(null);
expect(students.length).toEqual(10);
});
it("Spec test 3: test the getTeacher function", function() {
var teacher = schoolService.getTeacher("011");
expect(teacher).not.toBe(null);
expect(teacher.name).toMatch(/teacher/);
expect(teacher.name).toMatch("teacher");
expect(teacher.name).not.toMatch(/people/);
var teacher6 = schoolService.getTeacher("016");
expect(teacher6).toBe(null);
});
});
这时,我们发现每个方法内 teachers 与 students 对象总是重复的出现。因此我们考虑使用 beforeEach / afterEach 方法来优化测试代码
清单 4. 使用 beforeEach / afterEach 方法
describe("A test suite for SchoolService", function() {
var schoolService = new SchoolService();
var teachers = [];
var students = [];
beforeEach(function() {
teachers = schoolService.getTeachers();
students = schoolService.getStudents();
});
it("Spec test 1, test the getTeachers function", function() {
expect(teachers).not.toBe(null);
expect(teachers.length).toEqual(5);
});
it("Spec test 2: test the getStudents function", function() {
expect(students).not.toBe(null);
expect(students.length).toEqual(10);
});
it("Spec test 3: test the getTeacher function", function() {
var teacher = schoolService.getTeacher("011");
expect(teacher).not.toBe(null);
expect(teacher.name).toMatch(/teacher/);
expect(teacher.name).toMatch("teacher");
expect(teacher.name).not.toMatch(/people/);
var teacher6 = schoolService.getTeacher("016");
expect(teacher6).toBe(null);
});
afterEach(function() {
teachers = [];
students = [];
});
});
更进一步,如果能够将测试用例中的 Teacher 部分与 Student 分开,代码的逻辑就会显得更加整齐,于是,我们想到了使用嵌套测试
清单 5. 使用嵌套测试
describe("A test suite for SchoolService", function() {
//上面的代码与清单4相同
describe("A nested test suite", function() {
it("Spec test 4: test the getStudent function", function() {
var student10 = schoolService.getStudent("0010");
expect(student10).toBe(null);
var student9 = schoolService.getStudent("009");
var teacher = student9.teacher;
expect(teacher).toBeDefined();
});
it("Spec test 5: test the getStudentsByTeacher function", function() {
var students = schoolService.getStudentsByTeacher("014");
expect(students).not.toBe(null);
expect(students.length).toEqual(2);
var idArray = [];
idArray.push(students[0].id);
idArray.push(students[1].id);
expect(idArray).toContain("007", "008");
});
});
});
而且,一旦不再需要测试 Student 部分,我们只需要将 describe 更改成 xdescribe 即可。这个时候,Jasmine 在运行时就会忽略这部分的测试代码。(如果希望忽略 it 方法,则需要将它改成 xit)
Jasmine 中的高级用法 - Spies
Jasmine 还为我们提供了一些其他的方法,用于函数的元数据测试,Timeout 测试,异步调用测试等等。不过,这些方法属于 Jasmine 的高级用法,本文并不打算将它们一一列举。而是将重点放在了最有特点的 Spies 上,至于其他方法大家可以参看 Jasmine 官方网站。
Spy 用于模拟函数的调用,并且记录被调用的次数以及传递的参数,我们将这样的测试称为函数的元数据测试。例如
清单 6. Jasmine 中的 Spies
describe("A test suite for Spies", function() {
var schoolService = new SchoolService();
var teachers = null;
beforeEach(function() {
spyOn(schoolService, "getTeachers");
teachers = schoolService.getTeachers();
});
it("Spec test 1, tracks that the spy was called", function() {
expect(schoolService.getTeachers).toHaveBeenCalledTimes(1);
expect(schoolService.getTeachers).toHaveBeenCalled();
});
it("Spec test 2, tracks that the spy was called x times", function() {
teachers = schoolService.getTeachers();
expect(schoolService.getTeachers).toHaveBeenCalledTimes(2);
});
it("Spec test 3, tracks all the arguments of its calls", function() {
expect(schoolService.getTeachers).toHaveBeenCalledWith() ;
});
it("Spec test4, stops all execution on a function", function() {
expect(teachers).not.toBeDefined();
});
});
清单 7. 使用 Spies 模拟返回值
describe("A test suite for Spies", function() {
var schoolService = new SchoolService();
var teachers = null;
beforeEach(function() {
spyOn(schoolService, "getTeachers").and.returnValue({id: "016", name: "teacher6"});;
teachers = schoolService.getTeachers();
});
it("Spec test 5, when called returns the requested value", function() {
expect(schoolService.getTeachers).toHaveBeenCalledTimes(1);
expect(teachers).toBeDefined();
expect(teachers.id).toEqual("016");
});
});
Jasmine 是用 JavaScript 实现的,所以它也必须在 JavaScript 的环境中运行,最简单的环境也就是一个 Web 页面。所有的 spec 都可以在这个页面中运行,这个页面就叫做 Runner。
清单 8. 运行Jasmine
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Jasmine Spec Runner v2.8.0</title>
<link rel="shortcut icon" type="image/png"
href="lib/jasmine-2.8.0/jasmine_favicon.png">
<link rel="stylesheet" href="lib/jasmine-2.8.0/jasmine.css">
<script src="lib/jasmine-2.8.0/jasmine.js"></script>
<script src="lib/jasmine-2.8.0/jasmine-html.js"></script>
<script src="lib/jasmine-2.8.0/boot.js"></script>
<!-- include source files here... -->
<script src="src/model.js"></script>
<!-- include spec files here... -->
<script src="spec/spec-test.js"></script>
</head>
<body>
</body>
</html>
其中 boot.js 文件,用于初始化 Jasmine 环境。
Karma 简介
Karma 框架介绍
Karma 是一个基于 Node.js 的 JavaScript 测试执行过程管理工具(Test Runner)。该工具可用于测试所有主流 Web 浏览器,也可以集成到 CI(Continuous integration)工具,还可以和其他代码编辑器一起使用。
Karma 会监控配置文件中所指定的每一个文件,每当文件发生改变,它都会向测试服务器发送信号,来通知所有的浏览器再次运行测试代码。此时,浏览器会重新加载源文件,并执行测试代码。其结果会传递回服务器,并以某种形式显示给开发者。
访问浏览器执行结果,可通过以下的方式
- 手工方式 - 通过浏览器,访问 URL 地址 - http://localhost:9876/
- 自动方式 - 让 karma 来启动对应的浏览器
安装 Karma
第一步:安装 Node.js
安装相应版本的 Node.js,并却保 Node Package Manager(NPM) 可以正常运行(默认情况下,NPM 随 Node.js 一起安装)。
通过执行 node --version 命令,检查 Node.js 是否安装成功,以及运行 npm version 命令,察看 NPM 是否运行正常。
第二步:安装 Karma
1. 全局安装
命令 $npm install -g karma
安装 Karma 命令会到全局的 node_modules 目录下,我们可以在任何位置直接运行 karma 命令。
npm install -g karma-cli
此命令用来安装 karma-cli,它会在当前目录下寻找 karma 的可执行文件。这样我们就可以在一个系统内运行多个版本的 Karma。
2. 本地安装
命令 $npm install karma --save-dev
安装 Karma 命令到当前 node_modules 目录下,此时,如果需要执行 karma 命令,就需要这样 $ ./node_modules/.bin/karma
安装 plugins
访问 Karma 官网关于 plugins 部分,这里有众多的 Karma 插件可以选择安装。下面是一些常用的插件
- karma-chrome-launcher
- karma-coverage
- karma–jasmine
- karma–firefox-launcher
- karma-ie-launcher
Karma 的配置
基本配置
命令 $ karma init karma.conf.js
执行命令后,会被问到一系列的问题
- What testing framework do you want to use?
默认情况下 Jasmine, Mocha, QUnit 都已经被安装了,这里我们可以直接使用它们的名称。如果在应用中用到了其它的测试框架,那就需要我们安装它们所对应的插件,并在配置文件中标注它们(详见 karma.conf.js 中的 plugins 项)
- Do you want to use Require.js?
Require.js 是异步加载规范(AMD)的实现。常被作为基础代码库,应用在了很多的项目与框架之中,例如 Dojo, AngularJs 等、
- Do you want to capture a browser automatically?
选择需要运行测试用例的浏览器。需要注意的就是,必须保证所对应的浏览器插件已经安装成功。
- What is the location of your source and test files?
选择测试用例所在的目录位置。Karma 支持通配符的方式配置文件或目录,例如 *.js, test/**/*.js 等。如果目录或文件使用相对位置,要清楚地是,此时的路径是相对于当前运行 karma 命令时所在的目录。
- Should any of the files included by the previous patterns be excluded?
目录中不包括的那些文件。
- Do you want Karma to watch all the files and run tests on change?
是否需要 Karma 自动监听文件?并且文件一旦被修改,就重新运行测试用例?
最终 Karma 生成如下的配置文件(karma.conf.js),如下
清单 10. karma.conf.js
module.exports = function(config) {
config.set({
// base path, that will be used to resolve files and exclude
basePath: '../..',
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: ['test/client/mocks.js', 'static/karma.src.js', 'test/client/*.spec.js'],
// list of files to exclude
exclude: [],
// use dots reporter, as travis terminal does not support escaping sequences
// possible values: 'dots', 'progress'
reporters: ['progress', 'junit'],
// will be resolved to basePath (in the same way as files/exclude patterns)
junitReporter: {outputFile: 'test-results.xml'},
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR
//|| config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// Start these browsers, currently available:
// - Chrome, ChromeCanary, Firefox, Opera, Safari (only Mac), PhantomJS,
//IE (only Windows)
browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'],
// If browser does not capture in given timeout [ms], kill it
captureTimeout: 20000,
// Auto run tests on start (when browsers are captured) and exit
singleRun: false,
// report which specs are slower than 500ms
reportSlowerThan: 500,
// compile coffee scripts
preprocessors: {'**/*.coffee': 'coffee'},
plugins: [
'karma-jasmine',
'karma-chrome-launcher',
'karma-firefox-launcher',
'karma-junit-reporter'
]
});
};
配置项详解
配置文件的作用就是让 Karma 了解项目的结构,通过 karma init 这样的命令只是生成了基本的配置文件。但还有一些配置项也是需要注意的
Karma 的配置文件支持使用 JavaScript, CoffeeScript 或者 TypeScript 语言来编写。如果没有在执行 karma 命令时指定对应的配置文件,那么它会按照下面的顺序依次去寻找并加载配置文件
- ./karma.conf.js
- ./karma.conf.coffee
- ./karma.conf.ts
- ./.config/karma.conf.js
- ./.config/karma.conf.coffee
- ./.config/karma.conf.ts
其实编写配置文件,与编写 Node.js 的模块并没有什么区别。
清单 11. karma.conf.js
module.exports = function(config) {
config.set({
basePath: '../..',
frameworks: ['jasmine'],
//...
});
};
除了上面的这些基本的配置外,如果需要对 Karma 环境进行自定义配置,例如显示测试的覆盖率等等,还需要了解 karma 的一些配置项的详细使用方法,例如 preprocessors, plugins, browsers, files。
Files - 文件项
Karma 使用 minimatch 库来匹配文件。由于 minimatch 本身以方便灵活而著称,同时它的文件表达式又非常的简洁。在配置文件中,下面的这几个部分都会用到这种表达式
- exclude
- files
- preprocessors
例如
- **/*.js: 表示在所有的子目录内,以"js"为后缀的那些文件
- **/!(jquery).js: 和上面的含义相同,但不包括"jquery.js"文件
- **/(foo|bar).js: 表示在所有子目录内,以"foo.js"或者"bar.js"为名称的那些文件
详细的配置项可以参看Karma 官网关于 file 部分
我们以 files 配置项为例,来详细的了解文件匹配的具体用法
files
使用 files 选项,用于告诉 Karma 哪些文件会被项目使用,而哪些文件含有测试用例,以及需要测试。值得注意的是,文件配置的顺序就是浏览器引用它们时的顺序。
匹配模式
- 所有的相对位置,都是针对 basePath 而言的.
- basePath 也可以使用相对位置定义,此时它是相对于配置文件所在的位置
- Karma 使用 globa 库来解决文件的位置问题。它支持使用 minimatch 表达式,例如 test/unit/**/*.spec.js.
匹配顺序
- 模式的顺序决定了文件包含在浏览器中的顺序
- 如果多个文件被匹配到,文件按照按字母顺序排序
- 每个文件都包含一次。如果多个模式匹配同一个文件,则文件包含在第一个匹配到的模式中
完整的式例
清单 12. files
files: [
// Detailed pattern to include a file. Similarly other options can be used
{
pattern: 'lib/angular.js',
watched: false
},
// Prefer to have watched false for library files. No need to watch them for changes
// simple pattern to load the needed testfiles
// equal to {pattern: 'test/unit/*.spec.js',
//watched: true, served: true, included: true}
'test/unit/*.spec.js',
// this file gets served but will be ignored by the watcher
// note if html2js preprocessor is active, reference as
//`window.__html__['compiled/index.html']`
{
pattern: 'compiled/index.html',
watched: false
},
// this file only gets watched and is otherwise ignored
{
pattern: 'app/index.html',
included: false,
served: false
},
// this file will be served on demand from disk and will be ignored by the watcher
{
pattern: 'compiled/app.js.map',
included: false,
served: true,
watched: false,
nocache:true
}
],
详细解释,请参看 Karma 官网关于 included, watched, served 的解释
Browsers - 浏览器配置
捕获浏览器的行为总是令人感到很沮丧的,无疑这样的工作会耗费大量的开发时间。然而如果使用 Karma,这一切都会变得异常简单。原因是,Karma 都帮你自动完成了!
配置浏览器,我们只需要在配置文件中正确的设置 browsers 项(例如 browsers: ['Chrome']),Karma 后管理这些浏览器,包括启动与关闭它们。
Karma 支持的浏览器
- Chrome and Chrome Canary
- Firefox
- Safari
- PhantomJS
- JSDOM
- Opera
- Internet Explorer
- SauceLabs
- BrowserStack
- many more
我们以 Firefox 浏览器为例,首先你需要安装相应的插件
# Install the launcher first with NPM:
$ npm install karma-firefox-launcher --save-dev
接下在,在配置文件中加入新支持的浏览器名称
清单 13. browsers
module.exports = function(config) {
config.set({
browsers : ['Chrome', 'Firefox']
});
};
默认情况下,在配置文件中 browsers 项是没有被配置的(也就是说它的值是空)
当然,如果你希望使用其他的设备(如 tablet,手机等)来测试的话,只需在设备中打开对应的的浏览器,并访问 http://<hostname>:<port> (默认端口9876)。
另外,可以通过设置<BROWSER>_BIN 环境变量来替换浏览器路径。例如,在 Linux 下修改 Firefox 浏览器路径
# Changing the path to the Firefox binary
$ export FIREFOX_BIN=/usr/local/bin/my-firefox-build
详细解释,请参看 Karma 官网关于浏览器选项的详细配置
Preprocessors 配置
Preprocessors 定义的方法,会在文件被浏览器运行前执行(有点类似 AOP(Aspect-Oriented Programming)的概念)。
Preprocessors 的配置方法如下:
清单 14. preprocessors
preprocessors: {
'**/*.coffee': ['coffee'],
'**/*.tea': ['coffee'],
'**/*.html': ['html2js']
},
注意:
在这里,有多个文件表达式都配置了 "coffee" 这个 processor。在 Karma 看来,processor 与表达式的关系,有点类似于数据库里的一对多的关系。也就是说,一个 processor 可以被用于多个文件表达式。而且大多数的 Preprocessors 实现,都需要按照 plugins 的形式加载。
一些经常会用到的 preprocessors,如下
下面我们以 CoffeScript preprocessor 为例,看看如何在配置文件中使用它
首先我们需要先安装 karma-coffee-preprocessor
# Install it first with NPM
$ npm install karma-coffee-preprocessor --save-dev
接下来在配置文件中,我们需要这样.
清单 15. preprocessors
module.exports = function(config) {
config.set({
preprocessors: {
'**/*.coffee': ['coffee']
}
});
};
一些 preprocessors 还支持配置化,例如
清单 16. Preprocessors 配置化
coffeePreprocessor: {
options: {
bare: false
}
}
或者可以通过自定义的 preprocessor 的方式,例如
清单 17. Preprocessors 配置化
customPreprocessors: {
bare_coffee: {
base: 'coffee',
options: {bare: true}
}
}
最小匹配 minimatch
在配置 preprocessors 时用到的 key 值,是用来过滤 files 中配置的那些文件。(关于 files 的配置,前面已经介绍过了)
下面我们来详细了解一下过滤的执行步骤
- 首先,展开文件路径。所有的文件路径都会被展开成绝对路径(依靠 basePath 的位置)。
- 接下来,将文件路径与配置的 key 值进行匹配(需要使用到 minimatch 库)。
我们以 /my/absolute/path/to/test/unit/file.coffee 路径为例,如果 key 值是是 **/*.coffee 的化,匹配就会成功,但如果 key 值是 *.coffee,匹配则会失败。而文件匹配失败,preprocessor 自然也就不会被执行。
执行顺序
- 如果一个文件只匹配到了 preprocessors 配置对象的某一个 key 值,那么 Karma 会按照文件当中所描述的顺序依次执行 preprocessors。例如
清单 18. Minimatch 执行顺序
preprocessors: {
'*.js': ['a', 'b']
}
这时,karma 会先执行"a",然后执行"b"
- 如果一个文件匹配到多个 key 值,Karma 会按照某种合理的顺序执行 preprocessors,例如
清单 19. Minimatch 执行顺序
preprocessors: {
'*.js': ['a', 'b'],
'a.*': ['b', 'c']
}
对于a.js这个文件, karma 会先执行 'a',再执行 'b',最后执行 'c'.
- 但如果两个列表中的配置有矛盾(也许是配置错误), 例如:
清单 20. Minimatch 执行顺序
preprocessors: {
'*.js': ['a', 'b'],
'a.*': ['b', 'a']
}
那么,此时的 Karma 会随机的挑选一个配置顺序来执行。又比如
清单 21. Minimatch 执行顺序
preprocessors: {
'*.js': ['a', 'b', 'c'],
'a.*': ['c', 'b', 'd']
}
可以确定的是'a'会第一个被执行,'d'会最后一个被执行,而对于'b'和 'c',它们的执行顺序是随机的.
启动 Karma
命令
$ karma start karma.conf.js
有一些配置属性还可以在执行 karma 命令时,以参数的形式进行覆盖,例如
karma start my.conf.js --log-level debug --single-run
此时,Karma 会自动打开浏览器,并运行相应的测试用例,而执行结果会输出到控制台。
图 1. 图片标题示例
Karma 中的 Reporters
在默认的情况下,Karma 会将测试的执行结果显示在控制台上。而有些时候,我们可能更希望使用其他的输出格式,将结果展示出来。或者让 Karma 帮我们做更多的事情,比如显示代码的覆盖率等等,此时,我们就需要 Karma 的报表功能。
Karma 使用 Reporters 来显示执行结果。我们以 karma-html-reporter 为例
首先,我们需要下载 karma-html-reporter
npm install karma-htmlfile-reporter --save-dev
或者,你也可以全局安装
npm install -g karma-htmlfile-reporter
接下来,修改配置文件
清单 22. karma.conf.js
module.exports = function(config) {
config.set({
..... .....
reporters: ['progress', 'html'],
htmlReporter: {
outputFile: 'test-units.html',
// Optional
pageTitle: 'Unit Tests',
subPageTitle: 'A sample project description',
groupSuites: true,
useCompactStyle: true,
useLegacyStyle: true
}
});
};
执行命令
karma start --reporters html 或者 karma start karma.conf.js
在浏览器正常运行测试用例后,会在执行目录下生成 test-units.html 文件。使用浏览器打开 test-units.html 文件
图 2. 图片标题示例
总结
Karma 与 AngularJS 有着密不可分的联系,也进一步证明了其作为测试运行平台的能力。它出色的架构设计以及灵活的扩展性,也越来越引起众多 JavaScript 社区的关注。
它的特性让人印象深刻
- 支持多种测试框架
- 多样的结果报表(Reports)展示
- 在多种浏览器上运行测试用例
- 支持多种持续集成框架
- 简单,快速而有趣
这些特性,使它区别于其他的测试框架与平台,势必会成为开发者不可或缺的工具之一。
参考资源
- 了解更多关于Jasmine的使用方法,可访问 Jasmine 官网。
- 了解更多关于Karma的使用方法,可访问 Karma 官网。
- 了解与使用Karma的Reporters,可以访问这里。
- 通过查看文章"How to create a custom karma reporter",了解如何编写Reporter。