Web前端测试与集成:Jasmine/Selenium/Protractor/Jenkins的最佳实践
上QQ阅读APP看书,第一时间看更新

4.4 J asmine的断言

Jasmine的断言很接近于自然语言。断言的写法分为两部分:

   expect.matcher

第一部分expect函数接受一个参数,这个参数代表“实际值”,也就是被测代码的运行结果。

expect需要和第二部分matcher(匹配器)一起使用。匹配器接受“期望值”,将“期望值”与“实际值”进行布尔判断。Jasmine根据匹配器的结果决定测试用例是否通过测试。以前面Calc_spec.js里的断言为例:

   expect(result).toBe(4);

result是“实际值”, toBe是Jasmine内建的一个匹配器,而4则是“期望值”。这个测试用例期望result的值是4。

如果匹配器要执行否定判断,可以在expect和匹配器间加上not,例如:

    expect(false).not.toBe(true);

4.4.1 内置匹配器

Jasmine提供了丰富的内置匹配器。

1.toBe

该内置匹配器的语义是期望“实际值”和“期望值”严格相等(执行JavaScript"==="运算)。例如:

       it('toBe', function () {
           var a = {};
           var b = a;
           var c = {};

           expect(a).toBe(b);
           expect(a).not.toBe(c);
       });

如果想了解某个匹配器如何进行布尔判断,可以查阅jasmine.js文件中相应匹配器的源代码。例如toBe匹配器的代码如下:

   getJasmineRequireObj().toBe = function() {
     function toBe() {
       return {
         compare: function(actual, expected) {
           return {
             pass: actual === expected
           };
         }
       };
     }
     return toBe;
   }

2.toBeCloseTo

该内置匹配器的语义是期望“实际值”和“期望值”足够接近(不一定要相等)。例如:

       it('toBeCloseTo', function () {
           var a = 3.78,
             b = 3.76;
           expect(a).not.toBeCloseTo(b, 2);
           expect(a).toBeCloseTo(b, 1);
       });

所谓“足够接近”,就是把两个数按照指定的精度进行四舍五入后比较是否相等。toBeCloseTo的第二个参数用于指定精度。以上第一个断言里保留两位小数,所以“不接近”,第二个断言里只保留一位小数,所以“足够接近”。

3.toBeDefined

该内置匹配器的语义是期望“实际值”已定义。例如:

       it('toBeDefined', function () {
           var a = {
             foo: 'foo'
           };
           expect(a.foo).toBeDefined();
           expect(a.bar).not.toBeDefined();
       });

4.toBeFalsy

该内置匹配器用于实现布尔测试,使期望“实际值”为falsy。例如:

       it('toBeFalsy', function () {
           var a, foo = 'foo';
           expect(a).toBeFalsy();
           expect(foo).not.toBeFalsy();
       });

JavaScript里除了下列falsy值Mozilla Developer Network. Falsy[OL]. [2016]. https://developer.mozilla.org/en-US/docs/Glossary/Falsy.,其余所有的值都是truthy,包括“0”、“false”、空的function、空的array和空的object。

• false

• null

• undefi ned

• 0

• NaN

• ‘’(空字符串)

• “”(空字符串)

• document.all

5.toBeTruthy

该内置匹配器用于实现布尔测试,使期望“实际值”为truthy,和toBeFalsy相反。例如:

       it('toBeTruthy', function () {
           var a, foo = 'foo';
           expect(foo).toBeTruthy();
           expect(a).not.toBeTruthy();
       });

6.toBeGreaterThan

该内置匹配器的语义是期望“实际值”大于“期望值”。例如:

       it('toBeGreaterThan', function () {
           var a = 3.78,
             b = 3.76;
           expect(a).toBeGreaterThan(b);
           expect(b).not.toBeGreaterThan(a);
       });

7.toBeGreaterThanOrEqual

该内置匹配器的语义是期望“实际值”大于或等于“期望值”。

8.toBeLessThanOrEqual

该内置匹配器的语义是期望“实际值”小于或等于“期望值”。

9.toBeLessThan

该内置匹配器的语义是期望“实际值”小于“期望值”。

10.toBeNaN

该内置匹配器的语义是期望“实际值”是NaN(Not-A-Number)Mozilla Developer Network. NaN[OL]. 2015. https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/NaN.。例如:

       it('toBeNaN', function () {
           expect(0 / 0).toBeNaN();
           expect(parseInt('foo')).toBeNaN();
           expect(5).not.toBeNaN();
       });

11.toBeNull

该内置匹配器的语义是期望“实际值”是null。例如:

       it('toBeNull', function () {
           var a = null;
           var foo = 'foo';
           expect(a).toBeNull();
           expect(foo).not.toBeNull();
       });

12.toBeUndefined

该内置匹配器的语义是期望“实际值”未被定义,和toBeDefined相反。例如:

       it('toBeUndefined', function () {
           var a = {
             foo: 'foo'
           };
           expect(a.foo).not.toBeUndefined();
           expect(a.bar).toBeUndefined();
       });

13.toContain

该内置匹配器的语义是期望“实际值”(数组或对象)包含“期望值”。例如:

       describe('toContain', function () {
           it('finds an item in an Array', function () {
             var a = ['foo', 'bar', 'baz'];
             expect(a).toContain('bar');
             expect(a).not.toContain('quux');
           });
           it('finds a substring', function () {
             var a = 'foo bar baz';
             expect(a).toContain('bar');
             expect(a).not.toContain('quux');
           });
       });

14.toEqual

该内置匹配器的语义是期望两个对象(“实际值”和“期望值”)相等。例如:

       describe('toEqual', function () {
           it('works for simple literals and variables', function () {
             var a = 12;
             expect(a).toEqual(12);
           });
           it('works for objects', function () {
             var foo = {
                 a: 12,
                 b: 41
             };
             var bar = {
                 a: 12,
                 b: 41
             };
             expect(foo).toEqual(bar);
           });
       });

查看Jasmine里toEqual函数的实现,会注意到toEqual函数接受cutomEqualityTesters参数,这个参数是自定义相等检验器。如果用户定义了自己的相等检验器,Jasmine会使用这些相等检验器对“实际值”和“期望值”进行相等比较。例如:

   function toEqual(util, customEqualityTesters) {
       customEqualityTesters = customEqualityTesters || [];
       return {
       compare: function(actual, expected) {
       …
         result.pass = util.equals(actual,
           expected,
           customEqualityTesters);
       …
       }
       };
   }

15.toMatch

该内置匹配器的语义是期望“实际值”能够匹配“期望值”,“期望值”可以是正则表达式,也可以是字符串。例如:

       it('toMatch is for regular expressions', function () {
           var message = "foo bar baz";
           expect(message).toMatch(/bar/);
           expect(message).toMatch("bar");
           expect(message).not.toMatch(/quux/)
       });

16.toThrow

该内置匹配器的语义是期望“实际值”(函数)会抛出异常。例如:

       it('toThrow', function () {
           var foo = function () {
             return 1 + 2;
           };
           var bar = function () {
             return a + 1;
           };
           expect(foo).not.toThrow();
           expect(bar).toThrow();
       });

17.toThrowError

该内置匹配器的语义是期望“实际值”(函数)抛出“期望值”所指定的异常,异常信息可以是字符串、正则表达式、错误类型和错误信息。例如:

       it('toThrowError is for testing a specific thrown exception', function() {
         var foo = function() {
           throw new TypeError("foo bar baz");
         };
         expect(foo).toThrowError("foo bar baz");
         expect(foo).toThrowError(/bar/);
         expect(foo).toThrowError(TypeError);
         expect(foo).toThrowError(TypeError, "foo bar baz");
       });

4.4.2 自定义匹配器(Custom Matcher)

如果内置匹配器无法满足需求的话,可以编写自定义匹配器来封装匹配规则。

Jasmine使用工厂模式创建自定义匹配器。匹配器工厂是一个函数,函数名就是自定义匹配器的名称,也就是暴露给expect调用的名称。它接受两个参数:

• util,给匹配器使用的一组工具函数。

• customEqualityTesters,调用util.equals时所必需。

匹配器工厂返回一个对象,这个对象要包含名为compare的函数,即匹配器的对比函数。这个函数将实现自定义匹配规则。Jasmine在执行匹配时会调用compare函数。以自定义匹配器isBetween为例,示例代码如下:

   var customMatchers = {
       isBetween: function(util, customEqualityTesters){
         return {
             compare:function(actual, min, max) {
                 var result = {
                   pass: false,
                   message: 'Expected ' + actual + ' is not between '
                       + min + ' and ' + max
                 };
                 if(actual >= min && actual <= max){
                   result.pass = true;
                     result.message = 'Expected ' + actual + ' is between '
                       + min + ' and ' + max; ;
                 }
                 return result;
             }
         };
       }
   };

isBetween是匹配器工厂,也是要实现的自定义匹配器的名称。它返回一个对象,这个对象里的compare函数接受的第一个参数是actual,代表“实际值”。后面的参数是可选参数,代表“期望值”。在上面的示例里,期望“实际值”在“期望值”min和max之间。

compare函数必须返回一个结果对象。该对象必须包含一个布尔值类型的pass成员属性,告诉Jasmine匹配结果。在上面的示例里,一旦“实际值”在“期望值”min和max之间,则将pass属性设为true。

如果匹配失败,但是在返回的result对象中包含了message成员属性的话,Jasmine会使用message的值作为错误提示。

通常在beforeEach里使用jasmine.addMatchers函数注册自定义匹配器。该函数接受一个对象参数,这个对象可以包含多个匹配器工厂(在上面的示例里customerMatchers就是这样一个对象,isBetween是其中的一个字段)。例如,使用下面的代码:

       beforeEach(function() {
         jasmine.addMatchers(customMatchers);
       });

就可以在测试用例里使用isBetween自定义匹配器了。

       it('against isBetween', function(){
           expect(8).isBetween(4, 10);
       });

如果用户的自定义匹配器需要控制.not的行为(不是简单的布尔值取反),那么匹配器工厂返回的对象里除了compare函数,还需要包含另外一个函数negativeCompare。这样,Jasmine使用.not的时候会执行negativeCompare函数。

4.4.3 自定义相等检验器(Custom Equality Tester)

在Jasmine里用户可以使用toEqual内置匹配器判断两个对象是否相等。如果内置匹配器的默认规则无法满足需求的话,可以创建自定义相等检验器来使用自己的相等规则。

例如,对于以下示例里的Duck类型,用户希望两个Duck对象相等的条件是canSwim、canWalk和color相等,而canFly或其他属性不属于判断条件。这种情况就需要使用自定义相等检验器。

   var Duck = function () {
       this.canSwim = true;
       this.canWalk = true;
       this.color = 'white';
       this.canFly = false;
   };
   Duck.prototype.swim = function(){
       if(this.canSwim){
         //code
       }
   };
   Duck.prototype.walk = function(){
       if(this.canWalk){
         //code
       }
   };

自定义相等检验器是一个函数,它接受两个参数,也就是要进行比较的对象。自定义相等检验器根据相应条件对这两个对象进行比较,结果返回true或false值。如果自定义相等检验器无法进行判断,则返回undefined。以下示例里,仅对两个对象的canSwim、canWalk和color属性进行比较。

   describe('Custom Equality Test', function () {
       var duckCustomEquality = function (first, second) {
         return first.canSwim == second.canSwim
                   && first.canWalk == second.canWalk
                   && first.color == second.color;
       };
       …
   });

使用自定义相等检验器前需要注册。注册自定义相等检验器通常通过在beforeEach函数里调用jasmine.addCustomEqualityTester函数实现。示例代码如下:

       beforeEach(function () {
           jasmine.addCustomEqualityTester(duckCustomEquality);
       });

注册后就可以比较两个Duck对象了:

       it('against Duck class', function () {
           var d = {
             canSwim: true,
             canWalk: true,
             canClimb: true,
             color: 'white',
             canFly: true
           };
           var duck = new Duck();
           expect(d).toEqual(duck);
           d.canSwim = false;
           expect(d).not.toEqual(duck);
   });

4.4.4 非对称相等检验器(Asymmetric Equality Tester)

自定义相等检验器用来检验两个对象是否相等。如果只需要检验某一个对象是否满足特定条件,而不是两个对象严格相等时,我们可以自定义一个非对称相等检验器。它作为一个“期望值”对象出现,必须包含一个名为asymmetricMatch的方法。Jasmine在使用toEqual比较“实际值”和非对称相等检验器时会调用这个asymmetricMatch方法。

   describe('Asymmetry Match', function() {
     var tester = {
       asymmetricMatch: function(actual) {
         var secondValue = actual.split(', ')[1];
         return secondValue === 'bar';
       }
     };
     it('dives in deep', function() {
       expect('foo, bar, baz, quux').toEqual(tester);
     });
   });

以上示例创建了非对称相等检验器tester,它的asymmetricMatch方法期望输入的“实际值”字符串以逗号分隔后第二个内容为bar。

4.4.5 辅助匹配函数

针对一些常用情况,Jasmine提供辅助函数帮助开发人员进行匹配。这些辅助函数会返回非对称相等检验器(具有asymmetricMatch方法),封装了需要匹配的“期望值”和特殊匹配规则。

1.jasmine.any

通常传给匹配器的“期望值”是一个数字、字符串或对象等具体实例,然后将之和“实际值”做比较。有时候用户希望判断“实际值”是否是某种类型,而不是具体实例,这时可以用jasmine.any封装匹配类型,例如:

       describe('jasmine.any', function () {
           it('matches any value', function () {
             expect({}).toEqual(jasmine.any(Object));
             expect(12).toEqual(jasmine.any(Number));
           });
       });

jasmine.any函数接受构造函数或类名作为参数。这个构造函数或类名被jasmine.any封装成“期望值”,期望“实际值”所具备的类型。以上示例里“实际值”{}是一个Object,12是一个Number,所以测试通过。

2.jasmine.anything

jasmine.anything代表任何存在的值。只要“实际值”不是null或undefined,测试就通过。例如:

       describe('jasmine.anything', function () {
           it('matches anything', function () {
               expect(1).toEqual(jasmine.anything());
           });
       });

3.jasmine.objectContaining

jasmine.objectContaining用来判断“实际值”对象是否包含某个键值对。例如:

       describe('jasmine.objectContaining', function () {
           var foo;
           beforeEach(function () {
             foo = {
                 a: 1,
                 b: 2,
                 bar: 'baz'
             };
           });
           it('matches objects with the expect key/value pairs', function () {
             expect(foo).toEqual(jasmine.objectContaining({
                 bar: 'baz'
             }));
             expect(foo).not.toEqual(jasmine.objectContaining({
                 c: 37
             }));
           });
       });

4.jasmine.arrayContaining

jasmine.arrayContaining用来判断输入的“期望值”是否是“实际值”数组的一部分。例如:

       describe('jasmine.arrayContaining', function () {
           var foo;
           beforeEach(function () {
             foo = [1, 2, 3, 4];
           });
           it('matches arrays with some of the values', function () {
             expect(foo).toEqual(jasmine.arrayContaining([3, 1]));
             expect(foo).not.toEqual(jasmine.arrayContaining([6]));
           });
       });

5.jasmine.stringMatching

jasmine.stringMatching用来判断输入的“期望值”是否是“实际值”字符串的一部分。如果“期望值”是正则表达式,那么就会判断“实际值”是否满足正则表达式。它也可以用来判断对象内的字符串。示例代码如下:

       describe('jasmine.stringMatching', function () {
           it('matches as a regexp', function () {
             expect({ foo: 'bar' }).toEqual({
                 foo: jasmine.stringMatching(/^bar$/)
             });
             expect({ foo: 'foobarbaz' }).toEqual({
                 foo: jasmine.stringMatching('bar')
             });
           });
       });