使用Jest做單元測試

Jest是什麼?

Jest is a delightful JavaScript Testing Framework with a focus on simplicity.
It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!
Jest是一個關注於簡單使用且令人愉悅的JavaScript測試框架,它能夠和Babel、TypeScript、Node、React、Angular、Vue等項目配合使用;

起步

  1. 安裝jest
npm i -D jest
  1. 通過script開啓單元測試
{
	"scripts": { "test": "jest" }
}
  1. 使用TypeScript和Babel
npm i -D @babel/preset-env @babel/core @babel/preset-typescript npm i -D babel-jest
  1. 配置文件

配置babel.config.js:react和typescript,配置env當process.env.NODE_ENV是test時對.less、.sass、.styl進行忽略避免在jest測試時對樣式文件作出響應。

// babel.config.js
module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-typescript',
    '@babel/preset-react'
  ],
  env: {
	test: {
		plugins: ['babel-plugin-transform-require-ignore', {
			extensions: ['.less', '.sass', '.styl']
		}]
	}	
  }
};

配置jest.config.js

module.exports = {
    rootDir: './test/', // 測試目錄
    // 對jsx、tsx、js、ts文件採用babel-jest進行轉換
    transform: {
        '^.+\\.[t|j]sx?$': 'babel-jest',
    },
    testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]s?$',
    moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
    collectCoverage: true, // 統計覆蓋率
    testEnvironment: 'jsdom', // 測試環境,默認爲”jsdom“
    collectCoverageFrom: ['**/*.ts'],
    coverageDirectory: './coverage', // 測試覆蓋率的文檔
    globals: { 
        window: {}, // 設置全局對象window
    },
    setupFiles: ['<rootDir>/setupFiles/shim.js'], 
    // 測試前執行的文件,主要可以補齊模擬一些在node環境下的方法但又window下需要使用
};

// shim.js
// 在測試環境下模擬requestAnimationFrame函數
global.window.requestAnimationFrame = function (cb) {
    return setTimeout(() => {
        cb();
    }, 0);
};

編寫測試用例

使用Matchers編寫測試用例,例如:

test('測試用例名稱', () => {
	expect(2 + 2).toBe(4);
	expect(2 + 2).not.toBe(3);
	expect({ one: 1, two: 2 }).toEqual({ one: 1, two: 2 });
	expect(null).toBeNull();
	expect(null).toBeUndefined();
	expect(null).toBeTruthy();
	expect(null).toBeFalsy();
})

toBe使用的是Object.is所以可以用來測試非引用變量,當要測試Object時應該使用toEqual,這個方法會遞歸的進行比較。
toBeNull、toBeUndefined、toBeTruthy、ToBeFalsy是測試一些邊界情況使用的API
下面是關於數字類型的API,語義非常明顯不再需要解釋

test('測試類型的API', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);
});

更多類型的matcher將寫在附錄中,盡情參考

測試異步代碼

默認的jest代碼是一下執行到最後的,所以通常下面的代碼是無法被正確測試的

// 不會生效
test('the data is peanut butter', () => {
  function callback(data) {
    expect(data).toBe('peanut butter');
  }
  fetchData(callback);
});

而下面的才能生效,通過done告訴jest是否測試完畢

test('the data is peanut butter', done => {
  function callback(data) {
    try {
      expect(data).toBe('peanut butter');
      done();
    } catch (error) {
      done(error);
    }
  }
  fetchData(callback);
});

同樣的也是支持promise的測試

test('the data is peanut butter', () => {
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});
// resolve和reject兩種情況進行測試
test('the data is peanut butter', () => {
  return expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', () => {
  return expect(fetchData()).rejects.toMatch('error');
});
// 或者結合async和await
test('the data is peanut butter', async () => {
  await expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
  await expect(fetchData()).rejects.toThrow('error');
});

假如在測試代碼前需要做一些通用操作

那麼可以使用beforeEachafterEachbeforeAllafterAlldescribe通過Each在每個test前後進行執行,通過All在當前文件的所有test前後進行執行
,由describe包裹起來的部分,將形成一個scope使得All和Each只當前scope內會生效。示例如下:

beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));
test('', () => console.log('1 - test'));
describe('Scoped / Nested block', () => {
  beforeAll(() => console.log('2 - beforeAll'));
  afterAll(() => console.log('2 - afterAll'));
  beforeEach(() => console.log('2 - beforeEach'));
  afterEach(() => console.log('2 - afterEach'));
  test('', () => console.log('2 - test'));
});
// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll

mock函數

jest.fn()能夠創建一個mock函數,它的.mock屬性保存了函數被調用的信息,還追蹤了每次調用的this值。

const mockCallback = jest.fn();
forEach([0, 1], mockCallback);
test('該模擬函數被調用了兩次', () => {
    // 此模擬函數被調用了兩次
    expect(mockCallback.mock.calls.length).toBe(2);
})
test('第一次調用函數時的第一個參數是0', () => {
    // 第一次調用函數時的第一個參數是 0
    expect(mockCallback.mock.calls[0][0]).toBe(0);
})
test('第二次調用函數時的第一次參數是1', () => {
    // 第二次調用函數時的第一個參數是 1
    expect(mockCallback.mock.calls[1][0]).toBe(1);
})

// 表示被調用的次數
// The function was called exactly once
expect(someMockFunction.mock.calls.length).toBe(1);
// [n]表示第幾次調用[m]表示第幾個參數
// The first arg of the first call to the function was 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
// The second arg of the first call to the function was 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');

// The return value of the first call to the function was 'return value'
expect(someMockFunction.mock.results[0].value).toBe('return value');

// 被實例化兩次
// This function was instantiated exactly twice
expect(someMockFunction.mock.instances.length).toBe(2);
// 第一次實例化返回的對象有一個name屬性並且值是test
// The object returned by the first instantiation of this function
// had a `name` property whose value was set to 'test'
expect(someMockFunction.mock.instances[0].name).toEqual('test');

實際應用

mock的實際應用

上述對於mock的簡單說明其實看過一遍是很懵的無法直到它的實際用途,那麼下面我將列舉一個我在開發中使用jest.fn()模擬函數來對方法進行單元測試的例子:
首先我們有一個功能,目的是能使頁面滑動到某一個位置,最頂部或者是中間或者是底部,這個函數通常都被用於展示在頁面上的回到頂部按鈕。
函數類似下面這樣

export const scrollTo: (y?: number, option?: { immediately: boolean }) => void = (
    y = 0,
    option = { immediately: false }
) => {
    if (option.immediately) {
        window.scrollTo(0, y);
        return;
    }
    const top = document.body.scrollTop || document.documentElement.scrollTop;
    const clientHeight = document.body.clientHeight || document.documentElement.clientHeight;
    const scrollHeight = document.body.scrollHeight || document.documentElement.scrollHeight;
    if (top === y) {
        return;
    }
    if (y > scrollHeight - clientHeight && y <= scrollHeight) {
        y = scrollHeight - clientHeight;
    }
    if (Math.abs(top - y) > 1) {
        let movDistance = Math.ceil((y - top) / 8);
        let nextDestination = top + movDistance;
        if (Math.abs(top - y) < 8) {
            nextDestination = y;
        }
        window.requestAnimationFrame(scrollTo.bind(this, y));
        window.scrollTo(0, nextDestination);
    }
};

我們可以明確的知道函數裏讀取了scrollTop、clientHeight、scrollHegith和使用了window.requestAnimationFrame、window.scrollTo方法。這三個屬性和這兩個方法在node環境中其實都是沒有的。

針對window.requestAnimationFrame其實我們在上面的shim.js已經做了模擬,方法的具體多久執行我們不需要在意,只需要知道過一段時間他就應該執行其就可以了,所以使用了setTimeout來進行模擬,這個api在node端也是存在的。那麼對於其他的屬性和方法,我們可以有下面的測試代碼。

const scrollHeight = 8000;
const clientHeight = 1000;
const fakeWindow = {
    scrollTop: 0,
};
beforeAll(() => {
    // 定義網頁整體高度
    jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => scrollHeight);
    // 定義窗口高度
    jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => clientHeight);
    // 劫持scrollTop的獲取,存放在fakeWindow裏
    jest.spyOn(document.documentElement, 'scrollTop', 'get').mockImplementation(() => fakeWindow.scrollTop);
    // 或者像下面這樣去操作
});

describe('測試立即達到對應位置', () => {
    beforeEach(() => {
        fakeWindow.scrollTop = 1000;
    });
    test('立即回到頂部', () => {
        const mockScrollTo = jest.fn().mockImplementation((x = 0, y = 0) => {
            fakeWindow.scrollTop = y;
        });
        global.window.scrollTo = mockScrollTo;
        scrollTo(0, { immediately: true });
        const length = mockScrollTo.mock.calls.length;
        expect(mockScrollTo.mock.calls[length - 1][0]).toEqual(0);
    });
});
  1. 通過jest.spyOn對三個屬性的get進行定義,scrollHeight和clientHeight是一個靜態值我們只需要返回一個固定的值即可,scrollTop則是一個需要在scrollTo函數下進行改變的值,這個時候我們get劫持始終訪問一個對象裏的值
  2. 對scrollTo進行mock,使每次調用scrollTo函數時都是去改變fakeWindow裏的屬性值,這樣讀取和設置我們就都是一個值了
  3. 然後通過對mockScrollTo的mock屬性讀取,我們就能獲取一些記錄值,對於這個函數就是最後一次調用一定是傳入了目標位置,也就是0。

這樣我們就通過jest.spyOn()、jest.fn()方法完成了我們對scrollTo方法的滑動到頂部的單元測試,再添加更多的滑動到底部、中間等測試,我們就算完成了這個方法的基本情況測試,在生產環境使用也就更加自信😼拉~

其他注意事項

待補充

附錄

待補充

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章