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