Jasmine Unit Test

Unit Test

  • 单元测试概念(Unit Testing)

    又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

  • 单元测试必要性

    随着项目规模的增加,函数、方法、变量都在递增,维护的难度不断加大,以及测试提出的各种bug导致修改代码的时候会将原本整洁的代码变得混乱。
    经常出现同一个接口以不同的名称出现在不同的控制器中,这个时候往往会去重构代码,但是重构代码的时候没人会保证自己将万无一失,重构的代码还是正确的,方法一样跑通等等。这个时候就需要单元测试了,单元测试是一个衡量标准,告诉开发人员这么做是否将改变结果。保证重构后的代码的兼容性,减少人力测试的过程,降低维护成本。

<h2>Jasmine</h2>

Jasmine是一个behavior-driven development ( 行为驱动开发 ) 测试框架, 不依赖于任何其他JavaScript框架, 不依赖DOM, 并且有很简洁的语法让你能够很轻松的编写单元测试。它既可以在html文件中运行,也可以和jsTestDriver整合,在jsTestDriver中运行。
  • BDD 行为驱动开发,是一种新的敏捷开发方法。相对于TDD(测试驱动开发),它更趋向于需求,需要共同利益者的参与,强调用户故事和行为;是面向开发者、QA、非技术人员或商业参与者共同参与和理解的开发活动,而不是TDD简单地只关注开发者的方法论;

  • TDD测试驱动开发,是一种不同于传统软件开发流程:开发结束再测试介入的新型开发方法。要求把项目按功能点划分,在编写每个功能点前先编写测试代码,然后再编写使测试通过的功能代码,通过测试来推动整个开发工作。

<h2>搭建环境</h2>

<h4>1.下载源文件</h4>

jasmine源文件下载地址

图1.png
下载jasmine-standlone-2.5.0.zip即可。这是一个范例,但是可以直接使用。运行起来如下图显示:
运行图

<h4>2.使用</h4>
将下载下来的文件夹中lib文件夹下的jasmine-2.5.0文件夹直接拖入你所需要用的项目。在index.html 中引入下面几句

  <link rel="shortcut icon" type="image/png" href="lib/jasmine-2.5.0/jasmine_favicon.png">
  <link rel="stylesheet" href="lib/jasmine-2.5.0/jasmine.css">

  <script src="lib/jasmine-2.5.0/jasmine.js"></script>
  <script src="lib/jasmine-2.5.0/jasmine-html.js"></script>
  <script src="lib/jasmine-2.5.0/boot.js"></script>

之后便可以直接创建对应的测试用例js文件了。

<h2>jasmine基础语法</h2>
一个简单的例子

describe("A suite", function() {  
    it("contains spec with an expectation", function() {  
        expect(true).toBe(true);  
    });  
}); 

<h3>1.两个核心方法</h3>

  • <h4>describe方法</h4>
    describe是jasmine用于描述测试集(Test Suite)的全局函数,作为测试集的开始,一般有两个参数,字符串和方法。字符串作为特定用例组的名字和标题。方法是包含实现用例组的代码。一个测试集合可以包含多个spec(测试点)。
  • <h4>it方法</h4>
    jasmine中用方法it来开始specs。it方法和describe方法类似, 同样有两个参数,一个String,一个function;String用来描述测试点(spec),function是具体的测试代码。

示例代码

describe("This is an exmaple suite", function() {
  it("contains spec with an expectation", function() {
    expect(true).toBe(true);
    expect(false).toBe(false);
    expect(false).not.toBe(true);
  });
});

<h3>2.四个核心概念</h3>
<h4>Jasmine四个核心概念:</h4>

  • 分组(Suites)
  • 用例(Specs)
  • 期望(Expectations)
  • 匹配(Matchers)
  • 分组(Suites)

    Suites可以理解为一组测试用例,以函数describe(string,function)封装,describe函数接受两个参数,一个字符串和一个函数。字符串是这个Suites的名字或标题(通常描述下测试内容),函数是实现Suites的代码块。一个Suite可以包含多个Specs,一个Specs可以包括多个expect

  • 用例(Specs)

    Specs可以理解为一个测试用例,使用全局的Jasmin函数it创建。和describe一样接受两个参数,一个字符串和一个函数,函数就是要执行的测试代码,字符串就是测试用例的名字。一个Spec可以包含多个expectations来测试代码。

  • 期望(Expectations)

    Expectations由expect 函数创建。接受一个参数。和Matcher一起联用,设置测试的预期值。返回ture或false。
    在分组(describe)中可以写多个测试用例(it),也可以再进行分组(describe),在测试用例(it)中定义期望表达式(expect)和匹配判断(toBe)。看一个简单的Demo:

describe("A suite", function() {//suites
    var a;
    it("A spec", function() {//spec
      a = true;
      expect(a).toBe(true);//expectations
    });
    
    describe("a suite", function() {//inner suites
           it("a spec", function() {//spec
           expect(a).toBe(true);//expectations
        });
  });
});

  • 匹配(Matchers)

    Matcher实现断言的比较操作,一个“期望值”与“实际值”的对比,如果结果为true,则通过测试,反之,则失败。每一个matcher都能通过not执行否定判断。
expect(a).toBe(true);//期望变量a为true
expect(a).toEqual(true);//期望变量a等于true
expect(a).toMatch(/reg/);//期望变量a匹配reg正则表达式,也可以是字符串
expect(a.foo).toBeDefined();//期望a.foo已定义
expect(a.foo).toBeUndefined();//期望a.foo未定义
expect(a).toBeNull();//期望变量a为null
expect(a.isMale).toBeTruthy();//期望a.isMale为真
expect(a.isMale).toBeFalsy();//期望a.isMale为假
expect(true).toEqual(true);//期望true等于true
expect(a).toBeLessThan(b);//期望a小于b
expect(a).toBeGreaterThan(b);//期望a大于b
expect(a).toThrowError(/reg/);//期望a方法抛出异常,异常信息可以是字符串、正则表达式、错误类型以及错误类型和错误信息
expect(a).toThrow();//期望a方法抛出异常
expect(a).toContain(b);//期望a(数组或者对象)包含b
自定义Matcher(被称为Matcher Factories)实质上是一个函数(该函数的参数可以为空),该函数返回一个闭包,该闭包的本质是一个compare函数,compare函数接受2个参数:actual value 和 expected value。  
compare函数必须返回一个带pass属性的结果Object,pass属性是一个Boolean值,表示该Matcher的结果(为true表示该Matcher实际值与预期值匹配,为false表示不匹配),也就是说,实际值与预期值具体的比较操作的结果,存放于pass属性中。  
其他matchers:

jasmine.any(Class)--传入构造函数或者类返回数据类型作为期望值,返回true表示实际值和期望值数据类型相同:

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

jasmine.anything()--如果实际值不是null或者undefined则返回true:

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

jasmine.objectContaining({key:value})--实际数组只要匹配到有包含的数值就算匹配通过:

foo = {
      a: 1,
      b: 2,
      bar: "baz"
};
expect(foo).toEqual(jasmine.objectContaining({bar: "baz"}));

jasmine.arrayContaining([val1,val2,...])--stringContaining可以匹配字符串的一部分也可以匹配对象内的字符串:

expect({foo: 'bar'}).toEqual({foo: jasmine.stringMatching(/^bar$/)});
expect('foobarbaz').toEqual({foo: jasmine.stringMatching('bar')});

3.Setup和Teardown方法

为了减少重复性的代码,jasmine提供了beforeEach、afterEach、beforeAll、afterAll方法。 
  • beforeEach() :在describe函数中每个Spec执行之前执行;
  • afterEach() :在describe函数中每个Spec执行之后执行;
  • beforeAll() :在describe函数中所有的Specs执行之前执行,且只执行一次
  • afterAll () : 在describe函数中所有的Specs执行之后执行,且只执行一次
describe("A spec (with setup and tear-down)", function () {
    var foo;    
    //beforeAll 在所有的it方法执行之前执行一次
    beforeAll(function () { 
           foo = 1;       
          console.log("beforeAll run");   
   });    

   //afterAll 在所有的it方法执行之后执行一次
    afterAll(function () {       
        foo = 0;        
        console.log("afterAll run");   
    });   

  //beforeEach 在每个it方法执行之前都执行一次
   beforeEach(function () {        
        console.log("beforeEach run");   
   });    

 //afterEach 在每个it方法执行之后都执行一次
  afterEach(function () {        
      console.log("afterEach run");    
  });    

  it("is just a function,so it can contain any code", function () {       
       expect(foo).toEqual(1);    
  });   

  it("can have more than one expectation", function () {
       expect(foo).toEqual(1);
       expect(true).toEqual(true);    
  });
});

结果如图所示:

结果

4.describe函数的嵌套

每个嵌套的**describe**函数,都可以有自己的**beforeEach**,**afterEach**函数。  
在执行每个内层**Spec**时,都会按嵌套的由外及内的顺序执行每个**beforeEach**函数,所以内层**Sepc**可以访问到外层**Sepc**中的**beforeEach**中的数据。类似的,当内层**Spec**执行完成后,会按由内及外的顺序执行每个**afterEach**函数。
describe("A spec", function() {
  var foo;

  beforeEach(function() {
    foo = 0;
    foo += 1;
  });

  afterEach(function() {
    foo = 0;
  });

  it("is just a function, so it can contain any code", function() {
    expect(foo).toEqual(1);
  });

  it("can have more than one expectation", function() {
    expect(foo).toEqual(1);
    expect(true).toEqual(true);
  });

  describe("nested inside a second describe", function() {
    var bar;

    beforeEach(function() {
      bar = 1;
    });

    it("can reference both scopes as needed", function() {
      expect(foo).toEqual(bar);
    });
  });
});

5.禁用Suites,挂起Specs

**Suites**可以被**Disabled**。在**describe**函数名之前添加**x**即可将**Suite**禁用。  
被**Disabled**的**Suites**在执行中会被跳过,该**Suite**的结果也不会显示在结果集中。  
xdescribe("A spec", function() {
  var foo;

  beforeEach(function() {
    foo = 0;
    foo += 1;
  });

  it("is just a function, so it can contain any code", function() {
    expect(foo).toEqual(1);
  });
});
有3种方法可以将一个Spec标记为Pending。被Pending的Spec不会被执行,但是Spec的名字会在结果集中显示,只是标记为Pending。
  • 如果在Spec函数it的函数名之前添加x(xit),那么该Spec就会被标记为Pending。
  • 一个没有定义函数体的Sepc也会在结果集中被标记为Pending。
  • 如果在Spec的函数体中调用pending()函数,那么该Spec也会被标记为Pending。pending()函数接受一个字符串参数,该参数会在结果集中显示在PENDING WITH MESSAGE:之后,作为为何被Pending的原因。
describe("Pending specs", function() {

  xit("can be declared 'xit'", function() {
    expect(true).toBe(false);
  });

  it("can be declared with 'it' but without a function");
  
  it("can be declared by calling 'pending' in the spec body", function() {
    expect(true).toBe(false);
    pending('this is why it is pending');
  });
});

6.Spy追踪

Jasmine具有函数的追踪和反追踪的双重功能,这东西就是Spy。Spy能够存储任何函数调用记录和传入的参数,Spy只存在于describe和it中,在spec执行完之后销毁。

describe("A spy", function() {
  var foo, bar = null;
  beforeEach(function() {
    foo = {
      setBar: function(value) {
        bar = value;
      }
    };
    spyOn(foo, 'setBar');//给foo对象的setBar函数绑定追踪
    foo.setBar(123);
    foo.setBar(456, 'another param');
  });
  it("tracks that the spy was called", function() {
    expect(foo.setBar).toHaveBeenCalled();//toHaveBeenCalled用来匹配测试函数是否被调用过
  });
  it("tracks all the arguments of its calls", function() {
    expect(foo.setBar).toHaveBeenCalledWith(123);//toHaveBeenCalledWith用来匹配测试函数被调用时的参数列表
    expect(foo.setBar).toHaveBeenCalledWith(456, 'another param');//期望foo.setBar已经被调用过,且传入参数为[456, 'another param']
  });
  it("stops all execution on a function", function() {
    expect(bar).toBeNull();//用例没有执行foo.setBar,bar为null
  });
});

and.callThrough--spy链式调用and.callThrough后,在获取spy的同时,调用实际的函数。

describe("A spy, when configured to call through", function() {
  var foo, bar, fetchedBar;
  beforeEach(function() {
    foo = {
      setBar: function(value) {
        bar = value;
      },
      getBar: function() {
        return bar;
      }
    };
    spyOn(foo, 'getBar').and.callThrough();//调用and.callThrough方法
    foo.setBar(123);
    fetchedBar = foo.getBar();//因为and.callThrough,这里执行的是foo.getBar方法,而不是spy的方法
  });
  it("tracks that the spy was called", function() {
    expect(foo.getBar).toHaveBeenCalled();
  });
  it("should not effect other functions", function() {
    expect(bar).toEqual(123);
  });
  it("when called returns the requested value", function() {
    expect(fetchedBar).toEqual(123);
  });
});

and.returnValue--spy链式调用and.returnValue 后,任何时候调用该方法都只会返回指定的值,

describe("A spy, when configured to fake a return value", function() {
  var foo, bar, fetchedBar;
  beforeEach(function() {
    foo = {
      setBar: function(value) {
        bar = value;
      },
      getBar: function() {
        return bar;
      }
    };
    spyOn(foo, "getBar").and.returnValue(745);//指定返回值为745
    foo.setBar(123);
    fetchedBar = foo.getBar();
  });
  it("tracks that the spy was called", function() {
    expect(foo.getBar).toHaveBeenCalled();
  });
  it("should not effect other functions", function() {
    expect(bar).toEqual(123);
  });
  it("when called returns the requested value", function() {
    expect(fetchedBar).toEqual(745);//默认返回指定的returnValue值
  });
});

and.callFake--spy链式添加and.callFake相当于用新的方法替换spy的方法

describe("A spy, when configured with an alternate implementation", function() {
  var foo, bar, fetchedBar;
  beforeEach(function() {
    foo = {
      setBar: function(value) {
        bar = value;
      },
      getBar: function() {
        return bar;
      }
    };
    spyOn(foo, "getBar").and.callFake(function() {//指定callFake方法
      return 1001;
    });
    foo.setBar(123);
    fetchedBar = foo.getBar();
  });
  it("tracks that the spy was called", function() {
    expect(foo.getBar).toHaveBeenCalled();
  });
  it("should not effect other functions", function() {
    expect(bar).toEqual(123);
  });
  it("when called returns the requested value", function() {
    expect(fetchedBar).toEqual(1001);//执行callFake方法,返回1001
  });
});

and.throwError--spy链式调用and.callError后,任何时候调用该方法都会抛出异常错误信息:

describe("A spy, when configured to throw an error", function() {
  var foo, bar;
  beforeEach(function() {
    foo = {
      setBar: function(value) {
        bar = value;
      }
    };
    spyOn(foo, "setBar").and.throwError("error");//指定throwError
  });
  it("throws the value", function() {
    expect(function() {
      foo.setBar(123)
    }).toThrowError("error");//抛出错误异常
  });
});

and.stub--spy恢复到原始状态,不执行任何操作。直接看下代码:

describe("A spy", function() {
  var foo, bar = null;
  beforeEach(function() {
    foo = {
      setBar: function(value) {
        bar = value;
      }
    };
    spyOn(foo, 'setBar').and.callThrough();
  });
  it("can call through and then stub in the same spec", function() {
    foo.setBar(123);
    expect(bar).toEqual(123);
    foo.setBar.and.stub();//把foo.setBar设置为原始状态,and.callThrough无效
    bar = null;
    foo.setBar(123);//执行赋值无效
    expect(bar).toBe(null);
  });
});

Spy的其他方法

.calls.any():记录spy是否被访问过,如果没有,则返回false,否则,返回true;
.calls.count():记录spy被访问过的次数;
.calls.argsFor(index):返回指定索引的参数;
.calls.allArgs():返回所有函数调用的参数记录数组;
.calls.all ():返回所有函数调用的上下文、参数和返回值;
.calls.mostRecent():返回最近一次函数调用的上下文、参数和返回值;
.calls.first():返回第一次函数调用的上下文、参数和返回值;
.calls.reset():清除spy的所有调用记录;
还有几种方法,诸如异步,ajax等等,我还只是入门,不甚了解,demo的话这之后我弄好会放上来。

参考:

官方文档
jasmine测试框架简介
JavaScript单元测试框架-Jasmine
JavaScript 单元测试框架:Jasmine 初探
web前端开发七武器—Jasmine入门教程(上)
前端测试-jasmine
开启JavaScript测试之路--Jasmine

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容