Unit Test
单元测试概念(Unit Testing) 又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
单元测试必要性 随着项目规模的增加,函数、方法、变量都在递增,维护的难度不断加大,以及测试提出的各种bug导致修改代码的时候会将原本整洁的代码变得混乱。 经常出现同一个接口以不同的名称出现在不同的控制器中,这个时候往往会去重构代码,但是重构代码的时候没人会保证自己将万无一失,重构的代码还是正确的,方法一样跑通等等。这个时候就需要单元测试了,单元测试是一个衡量标准,告诉开发人员这么做是否将改变结果。保证重构后的代码的兼容性,减少人力测试的过程,降低维护成本。
Jasmine
Jasmine是一个behavior-driven development ( 行为驱动开发 ) 测试框架, 不依赖于任何其他JavaScript框架, 不依赖DOM, 并且有很简洁的语法让你能够很轻松的编写单元测试。它既可以在html文件中运行,也可以和jsTestDriver整合,在jsTestDriver中运行。
BDD 行为驱动开发,是一种新的敏捷开发方法。相对于TDD(测试驱动开发),它更趋向于需求,需要共同利益者的参与,强调用户故事和行为;是面向开发者、QA、非技术人员或商业参与者共同参与和理解的开发活动,而不是TDD简单地只关注开发者的方法论;
TDD测试驱动开发,是一种不同于传统软件开发流程:开发结束再测试介入的新型开发方法。要求把项目按功能点划分,在编写每个功能点前先编写测试代码,然后再编写使测试通过的功能代码,通过测试来推动整个开发工作。
搭建环境
1.下载源文件
jasmine源文件下载地址
下载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>     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); }); });
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 函数,都可以有自己的
beforeEach ,
afterEach 函数。
在执行每个内层
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 禁用。
被
Disabled 的
Suites 在执行中会被跳过,该
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