前言

Jasmine 是一個 JavaScript 的測試框架,提供了一系列的 API 用於執行單元測試。在 Angular 開發中常常使用 Jasmine 來進行測試,這篇文章介紹 Jasmine 的基本用法。

Jasmine 是什麼?

Jasmine 是一個 JavaScript 的測試框架,提供了一系列的 API 用於執行單元測試,在 Angular 也常常使用 Jasmine 來進行測試。

安裝 Jasmine

Jasmine 已經預先安裝在 Angular CLI 中,只需要使用 ng test 執行測試就可以了。如果要在其他地方使用的話,可以使用 npmyarn 來安裝 Jasmine。

創建測試套件

一開始我們需要創建一個測試套件,並在其中定義測試用例。可以使用 describe 函數來創建測試套件。

describe('登入頁功能', () => {
  .
  .
  .
});

創建測試用例

在測試套件中,

需要使用 it 函數來創建測試用例,

it 函數需要兩個參數,

描述函數,

其中包含實際的 測試邏輯

describe('登入頁功能', () => {
  it('應該檢核帳密輸入', () => {
    expect something...
  });
});

撰寫斷言

在測試用例中需要撰寫 斷言,

以判斷測試結果是否符合預期,

Jasmine 提供了許多內置的 匹配器 用於撰寫斷言

describe("登入頁功能", () => {
  it("應該檢查帳密輸入", () => {
    const something = "something";
    expect(something).toEqual("something");
  });
});

運行測試

接著運行測試套件,

以檢查程式及邏輯是否正常運行,

在 Angular CLI 中,

使用 ng test 來執行單元測試

Jasmine 撰寫規範

  • 編寫 可讀性高 的測試用例描述

  • 將測試用例 分組, 提高可維護性

  • 使用 beforeEachafterEach, 避免執行重複的操作

  • 避免在測試用例之間共享狀態, 每個單元測試應該要獨立運行

Suit & Spec

單元測試應該要這樣寫:

describe('登入頁功能', () => {
  it('應該檢核帳密輸入', () => {
    expect something...
  });

  it('若登入成功,應該跳轉到系統頁', () => {
    expect something...
  });
});

describe 巢狀

可以將 describe 分組:

describe('登入頁功能', () => {
  it('應該檢核帳密輸入', () => {
    expect something...
  });

  describe('登入成功後', () => {
    it('應該跳轉到系統頁', () => {
      expect something...
    });
  });
});

focus & skip

要執行特定的 describeit 可以使用 f(focus):

describe('登入頁功能', () => { // 不執行
  it('應該檢核帳密輸入', () => { // 不執行
    expect something...
  });
});

fdescribe('登入成功後', () => { // 執行
  it('應儲存使用者資料', () => { // 不執行
    expect something...
  });

  fit('應該跳轉到系統頁', () => { // 執行
    expect something...
  });
});

要跳過特定的 describeit 可以使用 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 用於檢查變數是否為 undefinednull 以外的任何值:

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 物件的 gettersetter 方法:

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 方法是否被調用過, 會回傳 truefalse:

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 等待驗證的時間