Unit Test
-
單元測試概念(Unit Testing)
又稱為模塊測試, 是針對(duì)程序模塊(軟件設(shè)計(jì)的最小單位)來進(jìn)行正確性檢驗(yàn)的測試工作。程序單元是應(yīng)用的最小可測試部件蜈缤。在過程化編程中踩窖,一個(gè)單元就是單個(gè)程序翘县、函數(shù)诺擅、過程等市袖;對(duì)于面向?qū)ο缶幊蹋钚卧褪欠椒ㄏ苹ⅲɑ悾ǔ悾┝瓒ⅰ⒊橄箢惛斗恪⒒蛘吲缮悾ㄗ宇悾┲械姆椒ā?/p>
-
單元測試必要性
隨著項(xiàng)目規(guī)模的增加烹玉,函數(shù)续捂、方法孩灯、變量都在遞增,維護(hù)的難度不斷加大僧凰,以及測試提出的各種bug導(dǎo)致修改代碼的時(shí)候會(huì)將原本整潔的代碼變得混亂掂榔。
經(jīng)常出現(xiàn)同一個(gè)接口以不同的名稱出現(xiàn)在不同的控制器中继效,這個(gè)時(shí)候往往會(huì)去重構(gòu)代碼,但是重構(gòu)代碼的時(shí)候沒人會(huì)保證自己將萬無一失装获,重構(gòu)的代碼還是正確的瑞信,方法一樣跑通等等。這個(gè)時(shí)候就需要單元測試了穴豫,單元測試是一個(gè)衡量標(biāo)準(zhǔn)凡简,告訴開發(fā)人員這么做是否將改變結(jié)果。保證重構(gòu)后的代碼的兼容性精肃,減少人力測試的過程秤涩,降低維護(hù)成本。
<h2>Jasmine</h2>
Jasmine是一個(gè)behavior-driven development ( 行為驅(qū)動(dòng)開發(fā) ) 測試框架司抱, 不依賴于任何其他JavaScript框架筐眷, 不依賴DOM, 并且有很簡潔的語法讓你能夠很輕松的編寫單元測試习柠。它既可以在html文件中運(yùn)行匀谣,也可以和jsTestDriver整合,在jsTestDriver中運(yùn)行资溃。
BDD 行為驅(qū)動(dòng)開發(fā)武翎,是一種新的敏捷開發(fā)方法。相對(duì)于TDD(測試驅(qū)動(dòng)開發(fā))肉拓,它更趨向于需求后频,需要共同利益者的參與,強(qiáng)調(diào)用戶故事和行為;是面向開發(fā)者、QA卑惜、非技術(shù)人員或商業(yè)參與者共同參與和理解的開發(fā)活動(dòng)膏执,而不是TDD簡單地只關(guān)注開發(fā)者的方法論;
TDD測試驅(qū)動(dòng)開發(fā)露久,是一種不同于傳統(tǒng)軟件開發(fā)流程:開發(fā)結(jié)束再測試介入的新型開發(fā)方法更米。要求把項(xiàng)目按功能點(diǎn)劃分,在編寫每個(gè)功能點(diǎn)前先編寫測試代碼毫痕,然后再編寫使測試通過的功能代碼征峦,通過測試來推動(dòng)整個(gè)開發(fā)工作。
<h2>搭建環(huán)境</h2>
<h4>1.下載源文件</h4>
下載jasmine-standlone-2.5.0.zip即可消请。這是一個(gè)范例栏笆,但是可以直接使用。運(yùn)行起來如下圖顯示:
<h4>2.使用</h4>
將下載下來的文件夾中l(wèi)ib文件夾下的jasmine-2.5.0文件夾直接拖入你所需要用的項(xiàng)目臊泰。在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>
之后便可以直接創(chuàng)建對(duì)應(yīng)的測試用例js文件了蛉加。
<h2>jasmine基礎(chǔ)語法</h2>
一個(gè)簡單的例子
describe("A suite", function() {
it("contains spec with an expectation", function() {
expect(true).toBe(true);
});
});
<h3>1.兩個(gè)核心方法</h3>
- <h4>describe方法</h4>
describe是jasmine用于描述測試集(Test Suite)的全局函數(shù),作為測試集的開始缸逃,一般有兩個(gè)參數(shù)针饥,字符串和方法。字符串作為特定用例組的名字和標(biāo)題需频。方法是包含實(shí)現(xiàn)用例組的代碼丁眼。一個(gè)測試集合可以包含多個(gè)spec(測試點(diǎn))。
- <h4>it方法</h4>
jasmine中用方法it來開始specs昭殉。it方法和describe方法類似苞七, 同樣有兩個(gè)參數(shù),一個(gè)String饲化,一個(gè)function莽鸭;String用來描述測試點(diǎn)(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.四個(gè)核心概念</h3>
<h4>Jasmine四個(gè)核心概念:</h4>
- 分組(Suites)
- 用例(Specs)
- 期望(Expectations)
- 匹配(Matchers)
-
分組(Suites)
Suites可以理解為一組測試用例硫眨,以函數(shù)
describe(string,function)
封裝,describe函數(shù)接受兩個(gè)參數(shù)巢块,一個(gè)字符串和一個(gè)函數(shù)礁阁。字符串是這個(gè)Suites的名字或標(biāo)題(通常描述下測試內(nèi)容),函數(shù)是實(shí)現(xiàn)Suites的代碼塊族奢。一個(gè)Suite可以包含多個(gè)Specs姥闭,一個(gè)Specs可以包括多個(gè)expect -
用例(Specs)
Specs可以理解為一個(gè)測試用例,使用全局的Jasmin函數(shù)it創(chuàng)建越走。和describe一樣接受兩個(gè)參數(shù)棚品,一個(gè)字符串和一個(gè)函數(shù)靠欢,函數(shù)就是要執(zhí)行的測試代碼,字符串就是測試用例的名字铜跑。一個(gè)Spec可以包含多個(gè)expectations來測試代碼门怪。
-
期望(Expectations)
Expectations由expect 函數(shù)創(chuàng)建。接受一個(gè)參數(shù)锅纺。和Matcher一起聯(lián)用掷空,設(shè)置測試的預(yù)期值。返回ture或false囤锉。
在分組(describe)中可以寫多個(gè)測試用例(it)坦弟,也可以再進(jìn)行分組(describe),在測試用例(it)中定義期望表達(dá)式(expect)和匹配判斷(toBe)官地∧鸢看一個(gè)簡單的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實(shí)現(xiàn)斷言的比較操作,一個(gè)“期望值”與“實(shí)際值”的對(duì)比区丑,如果結(jié)果為true拧粪,則通過測試修陡,反之沧侥,則失敗。每一個(gè)matcher都能通過not執(zhí)行否定判斷魄鸦。
expect(a).toBe(true);//期望變量a為true
expect(a).toEqual(true);//期望變量a等于true
expect(a).toMatch(/reg/);//期望變量a匹配reg正則表達(dá)式宴杀,也可以是字符串
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方法拋出異常,異常信息可以是字符串拾因、正則表達(dá)式旺罢、錯(cuò)誤類型以及錯(cuò)誤類型和錯(cuò)誤信息
expect(a).toThrow();//期望a方法拋出異常
expect(a).toContain(b);//期望a(數(shù)組或者對(duì)象)包含b
自定義Matcher(被稱為Matcher Factories)實(shí)質(zhì)上是一個(gè)函數(shù)(該函數(shù)的參數(shù)可以為空),該函數(shù)返回一個(gè)閉包绢记,該閉包的本質(zhì)是一個(gè)compare函數(shù)扁达,compare函數(shù)接受2個(gè)參數(shù):actual value 和 expected value。
compare函數(shù)必須返回一個(gè)帶pass屬性的結(jié)果Object蠢熄,pass屬性是一個(gè)Boolean值跪解,表示該Matcher的結(jié)果(為true表示該Matcher實(shí)際值與預(yù)期值匹配,為false表示不匹配)签孔,也就是說叉讥,實(shí)際值與預(yù)期值具體的比較操作的結(jié)果,存放于pass屬性中饥追。
其他matchers:
jasmine.any(Class)--傳入構(gòu)造函數(shù)或者類返回?cái)?shù)據(jù)類型作為期望值图仓,返回true表示實(shí)際值和期望值數(shù)據(jù)類型相同:
it("matches any value", function() {
expect({}).toEqual(jasmine.any(Object));
expect(12).toEqual(jasmine.any(Number));
});
jasmine.anything()--如果實(shí)際值不是null或者undefined則返回true:
it("matches anything", function() {
expect(1).toEqual(jasmine.anything());
});
jasmine.objectContaining({key:value})--實(shí)際數(shù)組只要匹配到有包含的數(shù)值就算匹配通過:
foo = {
a: 1,
b: 2,
bar: "baz"
};
expect(foo).toEqual(jasmine.objectContaining({bar: "baz"}));
jasmine.arrayContaining([val1,val2,...])--stringContaining可以匹配字符串的一部分也可以匹配對(duì)象內(nèi)的字符串:
expect({foo: 'bar'}).toEqual({foo: jasmine.stringMatching(/^bar$/)});
expect('foobarbaz').toEqual({foo: jasmine.stringMatching('bar')});
3.Setup和Teardown方法
為了減少重復(fù)性的代碼,jasmine提供了beforeEach但绕、afterEach救崔、beforeAll、afterAll方法。
- beforeEach() :在describe函數(shù)中每個(gè)Spec執(zhí)行之前執(zhí)行六孵;
- afterEach() :在describe函數(shù)中每個(gè)Spec執(zhí)行之后執(zhí)行;
- beforeAll() :在describe函數(shù)中所有的Specs執(zhí)行之前執(zhí)行碳竟,且只執(zhí)行一次
- afterAll () : 在describe函數(shù)中所有的Specs執(zhí)行之后執(zhí)行,且只執(zhí)行一次
describe("A spec (with setup and tear-down)", function () {
var foo;
//beforeAll 在所有的it方法執(zhí)行之前執(zhí)行一次
beforeAll(function () {
foo = 1;
console.log("beforeAll run");
});
//afterAll 在所有的it方法執(zhí)行之后執(zhí)行一次
afterAll(function () {
foo = 0;
console.log("afterAll run");
});
//beforeEach 在每個(gè)it方法執(zhí)行之前都執(zhí)行一次
beforeEach(function () {
console.log("beforeEach run");
});
//afterEach 在每個(gè)it方法執(zhí)行之后都執(zhí)行一次
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);
});
});
結(jié)果如圖所示:
4.describe函數(shù)的嵌套
每個(gè)嵌套的**describe**函數(shù)狸臣,都可以有自己的**beforeEach**莹桅,**afterEach**函數(shù)。
在執(zhí)行每個(gè)內(nèi)層**Spec**時(shí)烛亦,都會(huì)按嵌套的由外及內(nèi)的順序執(zhí)行每個(gè)**beforeEach**函數(shù)诈泼,所以內(nèi)層**Sepc**可以訪問到外層**Sepc**中的**beforeEach**中的數(shù)據(jù)。類似的煤禽,當(dāng)內(nèi)層**Spec**執(zhí)行完成后铐达,會(huì)按由內(nèi)及外的順序執(zhí)行每個(gè)**afterEach**函數(shù)。
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**函數(shù)名之前添加**x**即可將**Suite**禁用瓮孙。
被**Disabled**的**Suites**在執(zhí)行中會(huì)被跳過,該**Suite**的結(jié)果也不會(huì)顯示在結(jié)果集中选脊。
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種方法可以將一個(gè)Spec標(biāo)記為Pending杭抠。被Pending的Spec不會(huì)被執(zhí)行,但是Spec的名字會(huì)在結(jié)果集中顯示恳啥,只是標(biāo)記為Pending偏灿。
- 如果在Spec函數(shù)it的函數(shù)名之前添加x(xit),那么該Spec就會(huì)被標(biāo)記為Pending钝的。
- 一個(gè)沒有定義函數(shù)體的Sepc也會(huì)在結(jié)果集中被標(biāo)記為Pending翁垂。
- 如果在Spec的函數(shù)體中調(diào)用pending()函數(shù),那么該Spec也會(huì)被標(biāo)記為Pending硝桩。pending()函數(shù)接受一個(gè)字符串參數(shù)沿猜,該參數(shù)會(huì)在結(jié)果集中顯示在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具有函數(shù)的追蹤和反追蹤的雙重功能啼肩,這東西就是Spy。Spy能夠存儲(chǔ)任何函數(shù)調(diào)用記錄和傳入的參數(shù)望薄,Spy只存在于describe和it中疟游,在spec執(zhí)行完之后銷毀。
describe("A spy", function() {
var foo, bar = null;
beforeEach(function() {
foo = {
setBar: function(value) {
bar = value;
}
};
spyOn(foo, 'setBar');//給foo對(duì)象的setBar函數(shù)綁定追蹤
foo.setBar(123);
foo.setBar(456, 'another param');
});
it("tracks that the spy was called", function() {
expect(foo.setBar).toHaveBeenCalled();//toHaveBeenCalled用來匹配測試函數(shù)是否被調(diào)用過
});
it("tracks all the arguments of its calls", function() {
expect(foo.setBar).toHaveBeenCalledWith(123);//toHaveBeenCalledWith用來匹配測試函數(shù)被調(diào)用時(shí)的參數(shù)列表
expect(foo.setBar).toHaveBeenCalledWith(456, 'another param');//期望foo.setBar已經(jīng)被調(diào)用過痕支,且傳入?yún)?shù)為[456, 'another param']
});
it("stops all execution on a function", function() {
expect(bar).toBeNull();//用例沒有執(zhí)行foo.setBar,bar為null
});
});
and.callThrough--spy鏈?zhǔn)秸{(diào)用and.callThrough后颁虐,在獲取spy的同時(shí),調(diào)用實(shí)際的函數(shù)卧须。
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();//調(diào)用and.callThrough方法
foo.setBar(123);
fetchedBar = foo.getBar();//因?yàn)閍nd.callThrough另绩,這里執(zhí)行的是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鏈?zhǔn)秸{(diào)用and.returnValue 后,任何時(shí)候調(diào)用該方法都只會(huì)返回指定的值笋籽,
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);//默認(rèn)返回指定的returnValue值
});
});
and.callFake--spy鏈?zhǔn)教砑觓nd.callFake相當(dāng)于用新的方法替換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);//執(zhí)行callFake方法蹦漠,返回1001
});
});
and.throwError--spy鏈?zhǔn)秸{(diào)用and.callError后,任何時(shí)候調(diào)用該方法都會(huì)拋出異常錯(cuò)誤信息:
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");//拋出錯(cuò)誤異常
});
});
and.stub--spy恢復(fù)到原始狀態(tài)车海,不執(zhí)行任何操作笛园。直接看下代碼:
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設(shè)置為原始狀態(tài),and.callThrough無效
bar = null;
foo.setBar(123);//執(zhí)行賦值無效
expect(bar).toBe(null);
});
});
Spy的其他方法
.calls.any():記錄spy是否被訪問過侍芝,如果沒有研铆,則返回false,否則州叠,返回true棵红;
.calls.count():記錄spy被訪問過的次數(shù);
.calls.argsFor(index):返回指定索引的參數(shù)咧栗;
.calls.allArgs():返回所有函數(shù)調(diào)用的參數(shù)記錄數(shù)組逆甜;
.calls.all ():返回所有函數(shù)調(diào)用的上下文、參數(shù)和返回值致板;
.calls.mostRecent():返回最近一次函數(shù)調(diào)用的上下文交煞、參數(shù)和返回值;
.calls.first():返回第一次函數(shù)調(diào)用的上下文可岂、參數(shù)和返回值错敢;
.calls.reset():清除spy的所有調(diào)用記錄;
還有幾種方法缕粹,諸如異步,ajax等等纸淮,我還只是入門平斩,不甚了解,demo的話這之后我弄好會(huì)放上來咽块。
參考:
官方文檔
jasmine測試框架簡介
JavaScript單元測試框架-Jasmine
JavaScript 單元測試框架:Jasmine 初探
web前端開發(fā)七武器—Jasmine入門教程(上)
前端測試-jasmine
開啟JavaScript測試之路--Jasmine