使用 Karma + Jasmine 构建 Web 测试环境

发表于:2017-11-08 15:59

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:刘 刚    来源:IBM developerWorks中国

  简介
  现在,越来越多的 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。

21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号