JS测试之Jasmine

Mar 26, 2017


Jasmine是面向行为驱动开发(BDD)的Javascript单元测试框架,它不依赖于任何其他Javascript框架,也不依赖浏览器、DOM。

Jasmine和BDD有什么关系

关于什么是行为驱动开发(BDD),我们暂时不必要太深入的了解其概念。我们只需要了解我们可以在测试中使用”通用语言”来定义系统/系统的行为。

所谓”通用语言”,我们可以理解为我们的自然语言。 一个BDD”通用语言”“风格的测试描述:”如果给定两个数字,传入自动以的Add方法,其得到的结果将是两个数字之和”。

基于Jasmine框架开发的测试用例,其实只是BDD”通用语言”描述的单元测试用例。其不同于严格意义上的BDD测试用例,也不同通常理解的Junit测试等。 也许例子的对比会是最好的说明:

  • BDD自动化测试框架Cucumber的测试用例,这个用例是开发人员、产品经理乃至销售人员都能看懂的:

    Feature: Addition
      In order to avoid silly mistakes
      As a math idiot
      I want to be told the sum of two numbers
    
      Scenario: Add two numbers
      Given I have entered 50 into the calculator
      And I have entered 70 into the calculator
      When I press add
      Then the result should be 120 on the screen
    
  • Jasmine的测试:

    describe('JavaScript addition operator', function () {
        it('adds two numbers together', function () {
            expect(1 + 2).toEqual(3);
        });
    });
    
  • Java Junit测试:

    public class Test {
        @org.junit.Test
        public void testAdd() throws Exception {
            Assert.assertEquals(3, 1+2);
        }
    }
    

Node.js后台运行Jasmine测试

  • 安装jasmine npm install -g jasmine.

  • 初始化 jasmine init, 将会生成一个spec/support/jasmin.json的文件:

    {
      "spec_dir": "spec",
      "spec_files": [
        "**/*[sS]pec.js"
      ],
      "helpers": [
        "helpers/**/*.js"
      ],
      "stopSpecOnExpectationFailure": false,
      "random": false
    }
    
  • 使用 jasmine examples 创建测试用例的例子,此命令将生成Player, Song类以及PlaySpec, 还自定义了一个Matcher: toBePlaying。为了使例子更简单,我们也可以自己在spec下创建一个文件AdditionSpec.js:

    describe('JavaScript addition operator', function () {
        it('adds two numbers together', function () {
            expect(1 + 2).toEqual(3);
        });
    });
    
  • 运行 jasmine 就可以看到测试结果了。

浏览器前端运行Jasmine测试

在上一步创建的测试用例的基础上,与spec的同目录创建specRunner.html:

  <!DOCTYPE html>
  <html>
  <head>
    <meta charset="utf-8">
    <title>Jasmine Spec Runner v2.5.2</title>
    <link rel="shortcut icon" type="image/png" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine_favicon.png">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine-html.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/boot.js"></script>

    <script src="spec/AdditionSpec.js"></script>
  </head>
  <body>
  </body>
  </html>

直接在浏览器打开specRunner.html就可以看到AdditionSpec.js的测试结果。

图1 Jasmine浏览器测试结果
图1 Jasmine浏览器测试结果

源码分析

在jasmine源码的lib/spec/jasmine.js(4000+行代码)里,定义了describe,it等函数,并将这些函数其绑定到相应的环境(浏览器环境是window上,Node.js环境在global上)。

  • 在jasmine.js中,其首先定义了一个函数getJasmineRequireObj,在这个函数调用中返回几乎所有jasmine的对象。

    var getJasmineRequireObj = (function (jasmineGlobal) {
      var jasmineRequire;
    
      if (typeof module !== 'undefined' && module.exports && typeof exports !== 'undefined') {
        if (typeof global !== 'undefined') {
          jasmineGlobal = global;
        } else {
          jasmineGlobal = {};
        }
        jasmineRequire = exports;
      } else {
        if (typeof window !== 'undefined' && typeof window.toString === 'function' && window.toString() === '[object GjsGlobal]') {
          jasmineGlobal = window;
        }
        jasmineRequire = jasmineGlobal.jasmineRequire = jasmineGlobal.jasmineRequire || {};
      }
    
      function getJasmineRequire() {
        return jasmineRequire;
      }
    
      getJasmineRequire().core = function(jRequire) {
        var j$ = {};
          .....
        return j$;
      };
    
      return getJasmineRequire;
    })(this);
    
  • Jasmine将describe, it, expect 等方法绑定到了window或者global上, describe的实现如:

    this.describe = function(description, specDefinitions) {
          ensureIsFunction(specDefinitions, 'describe');
          var suite = suiteFactory(description);
          if (specDefinitions.length > 0) {
            throw new Error('describe does not expect any arguments');
          }
          if (currentDeclarationSuite.markedPending) {
            suite.pend();
          }
          addSpecsToSuite(suite, specDefinitions);
          return suite;
        };
    
  • Jasmin中定义QueueRunner来执行所有测试用例并统计测试结果,其定义了execute和run方法。

  • Jasmine 2.0版本后在需要使用boot.js來初始化以及运行测试。

    window.jasmine = jasmineRequire.core(jasmineRequire);
    jasmineRequire.html(jasmine);
    var env = jasmine.getEnv();
    ....
    window.onload = function() {
        if (currentWindowOnload) {
          currentWindowOnload();
        }
        htmlReporter.initialize();
        env.execute();
      };
    

Jasmine常用函数

  • describe里可以定义一系列测试用例; xdescribe定义的测试用例不会被执行;如果在测试文件中有定义fdescribe,则表示只执行fdescribe下定义的测试用例。

  • it, xit, fit

  • 如果在Spec的函数体中调用 pending() 函数,那么该Spec也会被标记为Pending。 pending() 函数接受一个字符串参数

  • beforeEach, afterEach, beforeAll, afterAll

  • expect, toBe, not.toBe, toEqual, toBeDefined, toBeNull, toContain, toThrow, toThrowError, toMatch

  • jasmine.clock()用来模拟操纵时间, 要想使用jasmine.clock(),先调用jasmine.clock().install告诉Jasmine你想要在spec或者suite操作时间,当你不需要使用时,务必调用jasmine.clock().uninstall来恢复时间状态。jasmine.clock().tick(milliseconds)来控制时间前进。

  • Spy用来追踪函数的调用历史信息(是否被调用、调用参数列表、被请求次数等)。当在一个对象上使用spyOn方法后即可模拟调用对象上的函数,此时对所有函数的调用是不会执行实际代码的。

    • spyOn(foo, “setBar”)将在foo对象上添加spy。

    • spyOn().and.returnValue(),由于Spy是模拟函数的调用,因此我们也可以强制指定函数的返回值。

    • spyOn().and.callThrough(),告诉Jasmine我们除了要完成对函数调用的跟踪,同时也需要执行实际的代码。

    • spyOn().and.callFake(),与returnValue相似,callFake则更进一步,直接通过指定一个假的自定义函数来执行。

    • spyOn().and.throwError(),模拟异常的抛出

  • Jasmine支持测试需要执行异步操作的specs,调用beforeEach , it , 和afterEach 的时候,可以带一个可选的参数done ,当spec执行完成之后需要调用done 来告诉Jasmine异步操作已经完成。默认Jasmine的超时时间是5s,可以通过全局的jasmine.DEFAULT_TIMEOUT_INTERVAL设置。

参考资料