web学习笔记01-Jasmine-Unit-Test

Unit Test

  • 单元测试概念(Unit Testing)


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

  • 单元测试必要性


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

Jasmine

    Jasmine是一个behavior-driven development ( 行为驱动开发 ) 测试框架, 不依赖于任何其他JavaScript框架, 不依赖DOM, 并且有很简洁的语法让你能够很轻松的编写单元测试。它既可以在html文件中运行,也可以和jsTestDriver整合,在jsTestDriver中运行。

  • BDD 行为驱动开发,是一种新的敏捷开发方法。相对于TDD(测试驱动开发),它更趋向于需求,需要共同利益者的参与,强调用户故事和行为;是面向开发者、QA、非技术人员或商业参与者共同参与和理解的开发活动,而不是TDD简单地只关注开发者的方法论;

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

搭建环境

1.下载源文件

jasmine源文件下载地址

图1.png

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

2.使用


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

1
2
3
4
5
6
<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文件了。



jasmine基础语法


一个简单的例子


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
describe("A suite", function() {  
it("contains spec with an expectation", function() {
expect(true).toBe(true);
});
});

```

<h3>1.两个核心方法</h3>
- <h4>describe方法</h4>
&#160;&#160;&#160;&#160;describe是jasmine用于描述测试集(Test Suite)的全局函数,作为测试集的开始,一般有两个参数,字符串和方法。字符串作为特定用例组的名字和标题。方法是包含实现用例组的代码。一个测试集合可以包含多个spec(测试点)。


- <h4>it方法</h4>
&#160;&#160;&#160;&#160;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);
});
});


2.四个核心概念


Jasmine四个核心概念:


- 分组(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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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执行否定判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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表示实际值和期望值数据类型相同:

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


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

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


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

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


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

1
2
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执行之后执行,且只执行一次



结果如图所示:

结果

4.describe函数的嵌套



    每个嵌套的describe函数,都可以有自己的beforeEachafterEach函数。
    在执行每个内层Spec时,都会按嵌套的由外及内的顺序执行每个beforeEach函数,所以内层Sepc可以访问到外层Sepc中的beforeEach中的数据。类似的,当内层Spec执行完成后,会按由内及外的顺序执行每个afterEach函数。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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禁用。
    被DisabledSuites在执行中会被跳过,该Suite的结果也不会显示在结果集中。

1
2
3
4
5
6
7
8
9
10
11
12
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的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
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执行完之后销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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的同时,调用实际的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 后,任何时候调用该方法都只会返回指定的值,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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后,任何时候调用该方法都会抛出异常错误信息:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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恢复到原始状态,不执行任何操作。直接看下代码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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的其他方法


1
2
3
4
5
6
7
8
.calls.any():记录spy是否被访问过,如果没有,则返回false,否则,返回true;
.calls.count():记录spy被访问过的次数;
.calls.argsFor(index):返回指定索引的参数;
.calls.allArgs():返回所有函数调用的参数记录数组;
.calls.all ():返回所有函数调用的上下文、参数和返回值;
.calls.mostRecent():返回最近一次函数调用的上下文、参数和返回值;
.calls.first():返回第一次函数调用的上下文、参数和返回值;
.calls.reset():清除spy的所有调用记录;

参考:


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