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等項目配合使用;
起步
- 安裝jest
npm i -D jest
- 通過script開啓單元測試
{
"scripts": { "test": "jest" }
}
- 使用TypeScript和Babel
npm i -D @babel/preset-env @babel/core @babel/preset-typescript npm i -D babel-jest
- 配置文件
配置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');
});
假如在測試代碼前需要做一些通用操作
那麼可以使用beforeEach
、afterEach
、beforeAll
、afterAll
、describe
通過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);
});
});
- 通過jest.spyOn對三個屬性的get進行定義,scrollHeight和clientHeight是一個靜態值我們只需要返回一個固定的值即可,scrollTop則是一個需要在scrollTo函數下進行改變的值,這個時候我們get劫持始終訪問一個對象裏的值
- 對scrollTo進行mock,使每次調用scrollTo函數時都是去改變fakeWindow裏的屬性值,這樣讀取和設置我們就都是一個值了
- 然後通過對mockScrollTo的mock屬性讀取,我們就能獲取一些記錄值,對於這個函數就是最後一次調用一定是傳入了目標位置,也就是0。
這樣我們就通過jest.spyOn()、jest.fn()方法完成了我們對scrollTo方法的滑動到頂部的單元測試,再添加更多的滑動到底部、中間等測試,我們就算完成了這個方法的基本情況測試,在生產環境使用也就更加自信😼拉~
其他注意事項
待補充
附錄
待補充