前言
Jasmine 是一個 JavaScript 的測試框架,提供了一系列的 API 用於執行單元測試。在 Angular 開發中常常使用 Jasmine 來進行測試,這篇文章介紹 Jasmine 的基本用法。
Jasmine 是什麼?
Jasmine 是一個 JavaScript 的測試框架,提供了一系列的 API 用於執行單元測試,在 Angular 也常常使用 Jasmine 來進行測試。
安裝 Jasmine
Jasmine 已經預先安裝在 Angular CLI 中,只需要使用 ng test 執行測試就可以了。如果要在其他地方使用的話,可以使用 npm 或 yarn 來安裝 Jasmine。
創建測試套件
一開始我們需要創建一個測試套件,並在其中定義測試用例。可以使用 describe 函數來創建測試套件。
describe('登入頁功能', () => {
.
.
.
});
創建測試用例
在測試套件中,
需要使用 it 函數來創建測試用例,
it 函數需要兩個參數,
描述 和 函數,
其中包含實際的 測試邏輯
describe('登入頁功能', () => {
it('應該檢核帳密輸入', () => {
expect something...
});
});
撰寫斷言
在測試用例中需要撰寫 斷言,
以判斷測試結果是否符合預期,
Jasmine 提供了許多內置的 匹配器 用於撰寫斷言
describe("登入頁功能", () => {
it("應該檢查帳密輸入", () => {
const something = "something";
expect(something).toEqual("something");
});
});
運行測試
接著運行測試套件,
以檢查程式及邏輯是否正常運行,
在 Angular CLI 中,
使用 ng test 來執行單元測試
Jasmine 撰寫規範
編寫 可讀性高 的測試用例描述
將測試用例 分組, 提高可維護性
使用
beforeEach和afterEach, 避免執行重複的操作避免在測試用例之間共享狀態, 每個單元測試應該要獨立運行
Suit & Spec
單元測試應該要這樣寫:
describe('登入頁功能', () => {
it('應該檢核帳密輸入', () => {
expect something...
});
it('若登入成功,應該跳轉到系統頁', () => {
expect something...
});
});
describe 巢狀
可以將 describe 分組:
describe('登入頁功能', () => {
it('應該檢核帳密輸入', () => {
expect something...
});
describe('登入成功後', () => {
it('應該跳轉到系統頁', () => {
expect something...
});
});
});
focus & skip
要執行特定的 describe 與 it 可以使用 f(focus):
describe('登入頁功能', () => { // 不執行
it('應該檢核帳密輸入', () => { // 不執行
expect something...
});
});
fdescribe('登入成功後', () => { // 執行
it('應儲存使用者資料', () => { // 不執行
expect something...
});
fit('應該跳轉到系統頁', () => { // 執行
expect something...
});
});
要跳過特定的 describe 與 it 可以使用 x:
xdescribe('登入頁功能', () => { // 不執行
it('應該檢核帳密輸入', () => { // 不執行
expect something...
});
});
describe('登入成功後', () => { // 執行
xit('應儲存使用者資料', () => { // 不執行
expect something...
});
it('應該跳轉到系統頁', () => { // 執行
expect something...
});
});
如果 xdescribe 遇到 fit 的時候:
xdescribe('登入頁功能', () => { // 不執行
it('應該檢核帳密輸入', () => { // 不執行
expect something...
});
});
xdescribe('登入成功後', () => { // 因為 fit 的關係, 所也會執行
fit('應儲存使用者資料', () => { // 還是會執行!
expect something...
});
it('應該跳轉到系統頁', () => { // 執行
expect something...
});
});
Setup & Teardown
在每個測試用例之前或之後執行某些操作
beforeEach
describe 執行後, 每個 it 被執行前, 執行 beforeEach:
describe("示範", () => {
beforeEach(() => {
// do something ...
});
});
afterEach
每個 it 執行後, 執行 afterEach:
describe("示範", () => {
afterEach(() => {
// do something ...
});
});
beforeAll
describe 執行後, it 被執行前, 調用 beforeEach(只執行一次):
describe("示範", () => {
beforeAll(() => {
// do something ...
});
});
afterAll
全部 it 執行後, 調用 afterAll:
describe("示範", () => {
afterAll(() => {
// do something ...
});
});
Expect
expect 用於撰寫斷言
expect
expect 可以用來預期參數或函式執行後為某數值或某行為:
expect(1).toBe(1);
expect(1).not.toBe(2);
expectAsync
expectAsync 用於預期異步函式執行後為某數值或某行為:
expectAsync(1).toBeResolvedTo(1);
expectAsync(1).not.toBeResolvedTo(2);
Matchers
讓 expect 進行比較和匹配, 檢查 實際結果 與 期望結果, 進行判斷
not
not 用於反轉匹配:
expect(1).not.toBe(2);
nothing
nothing 用於檢查函式沒有回傳值:
expect().nothing();
toBe
toBe 用於檢查兩個變數是否相等:
expect(1).toBe(1);
toEqual
toEqual 用於檢查兩個變數是否相等, 為深度比較:
expect(1).toEqual(1);
toBeCloseTo
toBeCloseTo 用於檢查兩個變數是否相等, 但是會忽略浮點數的誤差:
expect(0.1 + 0.2).not.toBe(0.3); // 0.30000000000000004
expect(0.1 + 0.2).toBeCloseTo(0.3); // 0.3
toBeDefined
toBeDefined 用於檢查變數是否已定義:
expect(1).toBeDefined();
toBeUndefined
toBeUndefined 用於檢查變數是否未定義:
expect(undefined).toBeUndefined();
toBeNaN
toBeNaN 用於檢查變數是否為 NaN:
expect(NaN).toBeNaN();
toBeNull
toBeNull 用於檢查變數是否為 null:
expect(null).toBeNull();
toBeFalse
toBeFalse 用於檢查變數是否為 false:
expect(false).toBeFalse();
toBeTrue
toBeTrue 用於檢查變數是否為 true:
expect(true).toBeTrue();
toBeFalsy
toBeFalsy 用於檢查變數是否為 falsy:
expect(false).toBeFalsy();
toBeTruthy
toBeTruthy 用於檢查變數是否為 truthy:
expect(1).toBeTruthy();
toBeGreaterThan
toBeGreaterThan 用於檢查變數是否大於某數值:
expect(2).toBeGreaterThan(1);
toBeGreaterThanOrEqual
toBeGreaterThanOrEqual 用於檢查變數是否大於等於某數值:
expect(2).toBeGreaterThanOrEqual(1);
expect(2).toBeGreaterThanOrEqual(2);
toBeLessThan
toBeLessThan 用於檢查變數是否小於某數值:
expect(1).toBeLessThan(2);
toBeLessThanOrEqual
toBeLessThanOrEqual 用於檢查變數是否小於等於某數值:
expect(1).toBeLessThanOrEqual(2);
expect(1).toBeLessThanOrEqual(1);
toBeInstanceOf
toBeInstanceOf 用於檢查變數是否為某類別的實例:
class Foo {}
expect(new Foo()).toBeInstanceOf(Foo);
toBeNegativeInfinity
toBeNegativeInfinity 用於檢查變數是否為負無窮大:
expect(Number.NEGATIVE_INFINITY).toBeNegativeInfinity();
toBePositiveInfinity
toBePositiveInfinity 用於檢查變數是否為正無窮大:
expect(Number.POSITIVE_INFINITY).toBePositiveInfinity();
toBeContain
toBeContain 用於檢查陣列是否包含某元素:
expect([1, 2, 3]).toContain(1);
expect([{ v: "a" }, { v: "b" }]).toContain({ v: "a" });
expect("abc").toContain("a");
toHaveBeenCalled
toHaveBeenCalled 用於檢查 spy 方法是否被調用過:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData");
expect(obj.getData).not.toHaveBeenCalled();
obj.getData("b");
expect(obj.getData).toHaveBeenCalled();
toHaveBeenCalledTimes
toHaveBeenCalledTimes 用於檢查 spy 方法被調用的次數:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData");
expect(obj.getData).not.toHaveBeenCalledTimes(1);
obj.getData("b");
expect(obj.getData).toHaveBeenCalledTimes(1);
toHaveBeenCalledWith
toHaveBeenCalledWith 用於檢查 spy 方法被調用時的參數:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData");
expect(obj.getData).not.toHaveBeenCalledWith("b");
obj.getData("b");
expect(obj.getData).toHaveBeenCalledWith("b");
toHaveBeenCalledBefore
toHaveBeenCalledBefore 用於檢查 spy 方法是否在另一個 spy 方法之前被調用:
const obj = {
getData: (key) => "aaaa",
};
const obj2 = {
getData: (key) => "bbbb",
};
spyOn(obj, "getData");
spyOn(obj2, "getData");
obj.getData("b");
obj2.getData("b");
expect(obj.getData).toHaveBeenCalledBefore(obj2.getData);
toHaveBeenCalledAfter
toHaveBeenCalledAfter 用於檢查 spy 方法是否在另一個 spy 方法之後被調用:
const obj = {
getData: (key) => "aaaa",
};
const obj2 = {
getData: (key) => "bbbb",
};
spyOn(obj, "getData");
spyOn(obj2, "getData");
obj.getData("b");
obj2.getData("b");
expect(obj2.getData).toHaveBeenCalledAfter(obj.getData);
toHaveClass
toHaveClass 用於檢查元素是否有某個 class:
const el = document.createElement("div");
el.classList.add("foo");
expect(el).toHaveClass("foo");
toHaveCssStyle
toHaveCssStyle 用於檢查元素是否有某個 css style:
const el = document.createElement("div");
el.style.color = "red";
expect(el).toHaveCssStyle({ color: "red" });
toMatch
toMatch 用於檢查變數是否符合某正規表達式:
expect("abc").toMatch(/abc/);
toMatchObject
toMatchObject 用於檢查變數是否符合某物件:
expect({ a: 1, b: 2 }).toMatchObject({ a: 1 });
toThrow
toThrow 用於檢查函式是否拋出錯誤:
expect(() => {
throw new Error("error");
}).toThrow("error");
toThrowError
toThrowError 用於檢查函式是否拋出 特定錯誤類別 或是 特定錯誤訊息:
const a = () => {
throw new TypeError("foo bar baz");
};
expect(a).toThrowError(TypeError);
expect(a).toThrowError("foo bar baz");
toThrowMatching
toThrowMatching 用於檢查函式是否拋出指定錯誤值:
expect(() => {
throw new TypeError("foo bar baz");
}).toThrowMatching((error) => error.message === "foo bar baz");
withContext
withContext 用於檢查函式是否拋出錯誤時, 有指定的上下文:
expect(() => {
throw new TypeError("foo bar baz");
}).withContext("error");
jasmine.any
jasmine.any 用於檢查變數是否為某類別的實例:
class Foo {}
expect(new Foo()).toEqual(jasmine.any(Foo));
jasmine.anything
jasmine.anything 用於檢查變數是否為 undefined 或 null 以外的任何值:
expect(1).toEqual(jasmine.anything());
expect(null).not.toEqual(jasmine.anything());
expect(undefined).not.toEqual(jasmine.anything());
jasmine.truthy
jasmine.truthy 用於檢查變數是否為 truthy:
expect(1).toEqual(jasmine.truthy());
jasmine.falsy
jasmine.falsy 用於檢查變數是否為 falsy:
expect(0).toEqual(jasmine.falsy());
jasmine.empty
jasmine.empty 用於檢查變數是否為空:
expect([]).toEqual(jasmine.empty());
jasmine.notEmpty
jasmine.notEmpty 用於檢查變數是否不為空:
expect([1]).toEqual(jasmine.notEmpty());
jasmine.arrayContaining
jasmine.arrayContaining 用於檢查陣列是否包含某元素:
expect([1, 2, 3]).toEqual(jasmine.arrayContaining([1]));
jasmine.arrayWithExactContents
jasmine.arrayWithExactContents 用於檢查陣列是否包含某元素, 且元素順序也要相同:
expect([1, 2, 3]).toEqual(jasmine.arrayWithExactContents([1, 2, 3]));
jasmine.mapContaining
jasmine.mapContaining 用於檢查物件是否包含在 Map 裡的 Key & value:
expect(
new Map([
["a", 1],
["b", 2],
])
).toEqual(jasmine.mapContaining(new Map([["a", 1]])));
jasmine.objectContaining
jasmine.objectContaining 用於檢查物件是否包含在 Object 裡的 Key & value:
expect({ a: 1, b: 2 }).toEqual(jasmine.objectContaining({ a: 1 }));
jasmine.setContaining
jasmine.setContaining 用於檢查 Set 是否包含某元素:
expect(new Set([1, 2, 3])).toEqual(jasmine.setContaining(1));
jasmine.stringMatching
jasmine.stringMatching 用於檢查變數是否符合某正規表達式:
expect("abc").toEqual(jasmine.stringMatching(/abc/));
Spies
spy 是一個函式, 可以監聽其他函式的呼叫情況.
spyOn
spyOn 用在原本就 有物件 並且該物件 也有方法, 這樣可以直接 spy 該物件的方法:
const car = {
run:() => { do something ... };
};
spyOn( car , 'car');
jasmine.createSpy
jasmine.createSpy 用在原本就 有物件, 但 不管有無方法, 都可以幫忙建立一個 spy 的方法:
const car = { // 有方法
run:() => { do something ... };
};
car.run = jasmine.createSpy();
const car = {}; // 或者沒有方法也可以使用
car.run = jasmine.createSpy();
jasmine.createSpyObj
jasmine.createSpyObj 用在原本就 沒有物件, 幫建立一個物件和多個 spy 的方法:
const car = jasmine.createSpyObj("car", ["run", "fly"]);
car.run.and.callFake(() => "Here we go!!!");
car.fly.and.callFake(() => "Boom!!!");
spyOnProperty
spyOnProperty spy 物件的 getter 或 setter 方法:
const car = {
_speed: 0,
get speed() {
return this._speed;
},
set speed(value) {
this._speed = value;
},
};
spyOnProperty(car, "speed", "get").and.callFake(() => 100);
spyOnProperty(car, "speed", "set").and.cllFake((200) => console.log('do something ...'));
spyOnAllFunctions
spyOnAllFunctions spy 物件的所有方法:
const car = {
run:() => { do something ... };
fly:() => { do something ... };
};
spyOnAllFunctions(car);
expect(car.run).toHaveBeenCalled();
expect(car.fly).not.toHaveBeenCalled();
Spy.withArgs
withArgs 用於設置 spy 方法的參數
withArgs
withArgs 用在 spy 的方法有多個參數時, 可以指定要 spy 的參數做不同的事情:
const mockData = "bbbb";
const mockData2 = "cccc";
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData")
.withArgs("b")
.and.returnValue(mockData)
.withArgs("c")
.and.returnValue(mockData2);
expect(obj.getData("b")).toBe(mockData);
expect(obj.getData("c")).toBe(mockData2);
Spy.and
and 用在設置 spy 對象的行為
and.callThrough
and.callThrough 會執行原本的方法:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData").and.callThrough();
expect(obj.getData("b")).toBe("aaaa");
and.callFake
and.callFake 會執行自定義的方法:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData").and.callFake(() => "bbbb");
expect(obj.getData("b")).toBe("bbbb");
and.returnValue
and.returnValue 會回傳自定義的值:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData").and.returnValue("bbbb");
expect(obj.getData("b")).toBe("bbbb");
and.returnValues
and.returnValues 會回傳一系列自定義的值:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData").and.returnValues("bbbb", "cccc"); // 按照提供的順序逐個返回
expect(obj.getData("b")).toBe("bbbb");
expect(obj.getData("c")).toBe("cccc");
and.stub
and.stub 用於將 Spy 對象配置為使用原始的樣子, 而不是模擬或是替代行為,
當調用 and.stub, 會清除任何先前的 Spy 設定並恢復對原始對象或方法的調用:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData").and.returnValue("bbbb");
expect(obj.getData("b")).toBe("bbbb");
spyOn(obj, "getData").and.stub(); // 恢復成原本的方法
expect(obj.getData("b")).toBe("aaaa");
and.throwError
and.throwError 會拋出錯誤:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData").and.throwError("error");
expect(() => obj.getData("b")).toThrowError("error");
Spy.calls
calls 用於檢查 spy 方法的調用情況
calls.any
calls.any spy 方法是否被調用過, 會回傳 true 或 false:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData");
expect(obj.getData.calls.any()).toBe(false);
obj.getData("b");
expect(obj.getData.calls.any()).toBe(true);
calls.all
calls.all 回傳全部 spy 方法被調用時的紀錄:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData");
obj.getData("b");
obj.getData("c");
expect(obj.getData.calls.all()).toEqual([
{ object: obj, args: ["b"], returnValue: "aaaa" },
{ object: obj, args: ["c"], returnValue: "aaaa" },
]);
calls.allArgs
calls.allArgs 回傳 spy 方法被調用時的所有參數:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData");
obj.getData("b");
obj.getData("c");
expect(obj.getData.calls.allArgs()).toEqual([["b"], ["c"]]);
calls.argsFor
calls.argsFor 回傳 spy 方法第幾次被調用時的參數:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData");
obj.getData("b");
expect(obj.getData.calls.argsFor(0)).toEqual(["b"]);
calls.count
calls.count 回傳 spy 方法被調用的次數:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData");
expect(obj.getData.calls.count()).toBe(0);
obj.getData("b");
expect(obj.getData.calls.count()).toBe(1);
calls.first
calls.first 回傳 spy 方法第一次被調用時的紀錄:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData");
obj.getData("b");
obj.getData("c");
expect(obj.getData.calls.first()).toEqual({
object: obj,
args: ["b"],
returnValue: "aaaa",
});
calls.mostRecent
calls.mostRecent 回傳 spy 方法最後一次被調用時的紀錄:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData");
obj.getData("b");
obj.getData("c");
expect(obj.getData.calls.mostRecent()).toEqual({
object: obj,
args: ["c"],
returnValue: "aaaa",
});
calls.reset
calls.reset 用於重置 spy 方法的調用情況:
const obj = {
getData: (key) => "aaaa",
};
spyOn(obj, "getData");
obj.getData("b");
obj.getData("c");
expect(obj.getData.calls.count()).toBe(2);
obj.getData.calls.reset();
expect(obj.getData.calls.count()).toBe(0);
Clock
jasmine.clock 用於模擬時間
install
jasmine.clock().install 安裝一個 clock:
beforeEach(() => {
jasmine.clock().install();
});
uninstall
jasmine.clock().uninstall 解除一個 clock:
afterEach(() => {
jasmine.clock().uninstall();
});
tick
jasmine.clock().tick 快轉一段時間:
beforeEach(() => {
jasmine.clock().tick(50);
});
mockDate
jasmine.clock().mockDate mock 現在時間為某某時間:
beforeEach(() => {
jasmine.clock().mockDate(new Date(2013, 9, 23));
});
Done
done 用於異步測試
done
done 用於異步測試, 等異步有反應後, 通知執行驗證:
it("示範測試異步程式碼", (done) => {
let a = 0;
setTimeout(() => {
a = 100;
expect(a).toBe(100);
done(); // 等待callback後,通知驗證 (非快轉,真的等3秒)
}, 3000);
expect(a).toBe(0);
});
使用 done() 是會等 callback 時間的,
所以要注意每個 spec 預設等待驗證時間為 5s,
超過的話 spec 還是報錯,
因此若要調整 spec 的驗證時間,
則可以使用 jasmine.DEFAULT_TIMEOUT_INTERVAL 去修改 spec 等待驗證的時間