寫在前面
在編寫測試時,我們通常需要檢查值是否滿足某些條件,Jest中提供的expect允許你訪問很多“Matchers”,這些“匹配器”允許您驗證不同的東西。
Expect 可以驗證什么
Jest中提供了如下的驗證方法:
expect(value) expect.extend(matchers) expect.anything() expect.any(constructor) expect.arrayContaining(array) expect.assertions(number) expect.hasAssertions() expect.not.arrayContaining(array) expect.not.objectContaining(object) expect.not.stringContaining(string) expect.not.stringMatching(string | regexp) expect.objectContaining(object) expect.stringContaining(string) expect.stringMatching(string | regexp) expect.addSnapshotSerializer(serializer) .not .resolves .rejects .toBe(value) .toHaveBeenCalled() .toHaveBeenCalledTimes(number) .toHaveBeenCalledWith(arg1, arg2, ...) .toHaveBeenLastCalledWith(arg1, arg2, ...) .toHaveBeenNthCalledWith(nthCall, arg1, arg2, ....) .toHaveReturned() .toHaveReturnedTimes(number) .toHaveReturnedWith(value) .toHaveLastReturnedWith(value) .toHaveNthReturnedWith(nthCall, value) .toBeCloseTo(number, numDigits) .toBeDefined() .toBeFalsy() .toBeGreaterThan(number) .toBeGreaterThanOrEqual(number) .toBeLessThan(number) .toBeLessThanOrEqual(number) .toBeInstanceOf(Class) .toBeNull() .toBeTruthy() .toBeUndefined() .toContain(item) .toContainEqual(item) .toEqual(value) .toHaveLength(number) .toMatch(regexpOrString) .toMatchObject(object) .toHaveProperty(keyPath, value) .toMatchSnapshot(propertyMatchers, snapshotName) .toStrictEqual(value) .toThrow(error) .toThrowErrorMatchingSnapshot()
下面我們將介紹部分驗證的使用場景:
expect(value)
每當您希望測試一個值時,都會使用expect函數。你很少會調用expect本身。相反你將使用expect和“matcher”函數來斷言關於值的某些內容。更容易理解這個例子。假設您有一個方法bestLaCroixFlavor(),它應該返回字符串“柚子”。下面是測試方法:
test('the best flavor is grapefruit', () => { expect(bestLaCroixFlavor()).toBe('grapefruit'); });
在上面的case中,toBe是matcher函數。為了幫助你測試不同的東西,Jest中有很多不同的matcher函數。
expect的參數應該是代碼生成的值,而匹配程序的任何參數都應該是正確的值。如果你將它們混合在一起,那么你的測試仍然可以工作,但是失敗測試的錯誤消息看起來會很奇怪。
expect.extend(matchers)
你可以使用expect.extend將自己的matcher添加到Jest中。例如假設你正在測試一個 theory library,並且你經常斷言數字可以被其他數整除,你可以把它抽象成toBeDivisibleBy matcher:
expect.extend({ toBeDivisibleBy(received, argument) { const pass = received % argument == 0; if (pass) { return { message: () => `expected ${received} not to be divisible by ${argument}`, pass: true, }; } else { return { message: () => `expected ${received} to be divisible by ${argument}`, pass: false, }; } }, }); test('even and odd numbers', () => { expect(100).toBeDivisibleBy(2); expect(101).not.toBeDivisibleBy(2); expect({apples: 6, bananas: 3}).toEqual({ apples: expect.toBeDivisibleBy(2), bananas: expect.not.toBeDivisibleBy(2), }); });
expect.extends還支持異步匹配器。異步匹配器返回一個promise,因此你需要等待返回的值。讓我們使用一個示例matcher來說明它們的用法。我們要實現一個非常相似的matcher,而不是toBeDivisibleBy,唯一的區別是可分割的數字將從外部源中提取。
expect.extend({ async toBeDivisibleByExternalValue(received) { const externalValue = await getExternalValueFromRemoteSource(); const pass = received % externalValue == 0; if (pass) { return { message: () => `expected ${received} not to be divisible by ${externalValue}`, pass: true, }; } else { return { message: () => `expected ${received} to be divisible by ${externalValue}`, pass: false, }; } }, }); test('is divisible by external value', async () => { await expect(100).toBeDivisibleByExternalValue(); await expect(101).not.toBeDivisibleByExternalValue(); });
匹配器應該返回帶有兩個鍵的對象(或對象的promise)。pass指示是否存在匹配,message提供了一個沒有參數的函數,在失敗時返回錯誤消息。因此當pass為false時,當expect(x). yourmatcher()失敗時,消息應該返回錯誤消息。當pass為true時,消息應該返回expect(x).no . yourmatcher()失敗時的錯誤消息。
這些輔助函數可以在自定義匹配器中找到: this.isNot,返回一個布爾值,讓你知道這個匹配器是用否定的.not修飾符調用的,允許你翻轉斷言。
this.equals(a, b)
如果兩個對象具有相同的值(遞歸地),則返回true。
this.utils有很多有用的工具。utils主要由來自jest-matcher-utils的導出組成。最有用的是matcherHint、printExpected和printReceived,它們可以很好地格式化錯誤消息。例如看看toBe matcher的實現:
const diff = require('jest-diff'); expect.extend({ toBe(received, expected) { const pass = Object.is(received, expected); const message = pass ? () => this.utils.matcherHint('.not.toBe') + '\n\n' + `Expected value to not be (using Object.is):\n` + ` ${this.utils.printExpected(expected)}\n` + `Received:\n` + ` ${this.utils.printReceived(received)}` : () => { const diffString = diff(expected, received, { expand: this.expand, }); return ( this.utils.matcherHint('.toBe') + '\n\n' + `Expected value to be (using Object.is):\n` + ` ${this.utils.printExpected(expected)}\n` + `Received:\n` + ` ${this.utils.printReceived(received)}` + (diffString ? `\n\nDifference:\n\n${diffString}` : '') ); }; return {actual: received, message, pass}; }, });
打印結果如下:
expect(received).toBe(expected) Expected value to be (using Object.is): "banana" Received: "apple"
當斷言失敗時,錯誤消息應該向用戶提供必要的盡可能多的信號,以便用戶能夠快速地解決問題。你應該編寫一個精確的失敗消息,以確保自定義斷言的用戶具有良好的開發經驗。
expect.anything()
它匹配除null或undefined之外的任何內容。你可以在內部使用toEqual或toBeCalledWith而不是文字值。例如如果你想檢查一個模擬函數是否被調用,它的參數是非空的:
test('map calls its argument with a non-null argument', () => { const mock = jest.fn(); [1].map(x => mock(x)); expect(mock).toBeCalledWith(expect.anything()); });
expect.any(constructor)
匹配給定構造函數所創建的任何內容。你可以在內部使用toEqual或toBeCalledWith而不是文字值。如果你想檢查一個模擬函數是否被調用時帶有一個數字:
function randocall(fn) { return fn(Math.floor(Math.random() * 6 + 1)); } test('randocall calls its callback with a number', () => { const mock = jest.fn(); randocall(mock); expect(mock).toBeCalledWith(expect.any(Number)); });
expect.arrayContaining(array)
匹配一個接收到的數組,該數組包含預期數組中的所有元素。也就是說預期數組是接收數組的子集。因此它匹配一個接收到的數組,該數組包含不屬於預期數組的元素。
你可以用它代替文字的值: toEqual或toBeCalledWith
describe('arrayContaining', () => { const expected = ['Alice', 'Bob']; it('matches even if received contains additional elements', () => { expect(['Alice', 'Bob', 'Eve']).toEqual(expect.arrayContaining(expected)); }); it('does not match if received does not contain expected elements', () => { expect(['Bob', 'Eve']).not.toEqual(expect.arrayContaining(expected)); }); });
describe('Beware of a misunderstanding! A sequence of dice rolls', () => { const expected = [1, 2, 3, 4, 5, 6]; it('matches even with an unexpected number 7', () => { expect([4, 1, 6, 7, 3, 5, 2, 5, 4, 6]).toEqual( expect.arrayContaining(expected) ); }); it('does not match without an expected number 2', () => { expect([4, 1, 6, 7, 3, 5, 7, 5, 4, 6]).not.toEqual( expect.arrayContaining(expected), ); }); });
expect.assertions(number)
驗證在測試期間調用了一定數量的斷言。在測試異步代碼時這通常很有用,以便確保回調中的斷言確實被調用。
假設我們有一個函數doAsync,它接收兩個回調callback1和callback2,它將異步地以一個未知的順序調用它們。我們可以用:
test('doAsync calls both callbacks', () => { expect.assertions(2); function callback1(data) { expect(data).toBeTruthy(); } function callback2(data) { expect(data).toBeTruthy(); } doAsync(callback1, callback2); });
expect.hasAssertions()
驗證在測試期間至少調用了一個斷言。在測試異步代碼時,這通常很有用以便確保回調中的斷言確實被調用。
假設我們有一些處理狀態的函數。prepareState調用一個狀態對象的回調,validateState運行在那個狀態對象上,waitOnState返回一個承諾,直到所有prepareState回調完成。我們可以用:
test('prepareState prepares a valid state', () => { expect.hasAssertions(); prepareState(state => { expect(validateState(state)).toBeTruthy(); }); return waitOnState(); });
expect.not.arrayContaining(array)
匹配所接收的數組,該數組不包含預期數組中的元素。也就是說,預期的數組不是接收數組的子集。它與 expect.arrayContaining 相反
describe('not.arrayContaining', () => { const expected = ['Samantha']; it('matches if the actual array does not contain the expected elements', () => { expect(['Alice', 'Bob', 'Eve']).toEqual( expect.not.arrayContaining(expected), ); }); });
expect.not.objectContaining(object)
匹配任何未遞歸地匹配預期屬性的接收對象。也就是說預期對象不是接收對象的子集。因此,它匹配所接收的對象,該對象包含不屬於預期對象的屬性。它與expect. objectcontains相反。
describe('not.objectContaining', () => { const expected = {foo: 'bar'}; it('matches if the actual object does not contain expected key: value pairs', () => { expect({bar: 'baz'}).toEqual(expect.not.objectContaining(expected)); }); });
expect.not.stringContaining(string)
匹配不包含確切期望字符串的接收字符串。它與expect.stringContaining.相反
describe('not.stringContaining', () => { const expected = 'Hello world!'; it('matches if the actual string does not contain the expected substring', () => { expect('How are you?').toEqual(expect.not.stringContaining(expected)); }); });
expect.not.stringMatching(string | regexp)
匹配不匹配預期regexp的接收字符串。它與expect.stringMatching.相反
describe('not.stringMatching', () => { const expected = /Hello world!/; it('matches if the actual string does not match the expected regex', () => { expect('How are you?').toEqual(expect.not.stringMatching(expected)); }); });
expect.objectContaining(object)
匹配遞歸地匹配預期屬性的任何接收對象。也就是說,預期對象是接收對象的子集。因此,它匹配所接收的對象,該對象包含不屬於預期對象的屬性。
與期望對象中的文字屬性值不同,您可以使用matchers、expect.anything()等等。
假設我們希望使用事件對象調用onPress函數,我們需要驗證的是事件是否有event.x屬性和y屬性。我們可以這樣做:
test('onPress gets called with the right thing', () => { const onPress = jest.fn(); simulatePresses(onPress); expect(onPress).toBeCalledWith( expect.objectContaining({ x: expect.any(Number), y: expect.any(Number), }), ); });
expect.stringMatching(string | regexp)
匹配與預期regexp匹配的接收字符串。你可以用它代替文字的值:
1. 在toEqual或toBeCalledWith
2. 匹配arraycontains中的元素
3. 匹配objectContaining
或者toMatchObject的
屬性
這個示例還展示了如何使用expect嵌套多個不對稱的匹配器。在expect.arrayContaining stringMatching。
describe('stringMatching in arrayContaining', () => { const expected = [ expect.stringMatching(/^Alic/), expect.stringMatching(/^[BR]ob/), ]; it('matches even if received contains additional elements', () => { expect(['Alicia', 'Roberto', 'Evelina']).toEqual( expect.arrayContaining(expected), ); }); it('does not match if received does not contain expected elements', () => { expect(['Roberto', 'Evelina']).not.toEqual( expect.arrayContaining(expected), ); }); });
.toBe(value)
toBe只是檢查一個值是否符合您的期望。它使用對象。是要檢查完全相等。例如此代碼將驗證can對象的一些屬性:
const can = { name: 'pamplemousse', ounces: 12, }; describe('the can', () => { test('has 12 ounces', () => { expect(can.ounces).toBe(12); }); test('has a sophisticated name', () => { expect(can.name).toBe('pamplemousse'); }); });
.toEqual(value)
如果要檢查兩個對象是否具有相同的值,請使用. toequal。此matcher遞歸地檢查所有字段的相等性,而不是檢查對象標識——這也稱為“深度相等”。例如,toEqual和toBe在這個測試套件中表現不同,所以所有的測試都通過:
const can1 = { flavor: 'grapefruit', ounces: 12, }; const can2 = { flavor: 'grapefruit', ounces: 12, }; describe('the La Croix cans on my desk', () => { test('have all the same properties', () => { expect(can1).toEqual(can2); }); test('are not the exact same can', () => { expect(can1).not.toBe(can2); }); });
.toMatchObject(object)
使用. tomatchobject檢查一個JavaScript對象是否匹配一個對象的屬性子集。它將把接收到的對象與預期對象中沒有的屬性匹配起來。
您還可以傳遞一個對象數組,在這種情況下,只有當接收到的數組中的每個對象(在上面描述的番茄對象意義中)與預期數組中的相應對象相匹配時,該方法才會返回true。如果想要檢查兩個數組在它們的元素數量上是否匹配,而不是arrayinclude,這是非常有用的,因為它允許在接收的數組中添加額外的元素。
可以將屬性匹配到值或匹配項。
const houseForSale = { bath: true, bedrooms: 4, kitchen: { amenities: ['oven', 'stove', 'washer'], area: 20, wallColor: 'white', }, }; const desiredHouse = { bath: true, kitchen: { amenities: ['oven', 'stove', 'washer'], wallColor: expect.stringMatching(/white|yellow/), }, }; test('the house has my desired features', () => { expect(houseForSale).toMatchObject(desiredHouse); });
describe('toMatchObject applied to arrays arrays', () => { test('the number of elements must match exactly', () => { expect([{foo: 'bar'}, {baz: 1}]).toMatchObject([{foo: 'bar'}, {baz: 1}]); }); // .arrayContaining "matches a received array which contains elements that // are *not* in the expected array" test('.toMatchObject does not allow extra elements', () => { expect([{foo: 'bar'}, {baz: 1}]).toMatchObject([{foo: 'bar'}]); }); test('.toMatchObject is called for each elements, so extra object properties are okay', () => { expect([{foo: 'bar'}, {baz: 1, extra: 'quux'}]).toMatchObject([ {foo: 'bar'}, {baz: 1}, ]); }); });
.toHaveProperty(keyPath ,value)
使用. tohaveproperty檢查在提供的引用keyPath中是否存在對象的屬性。要檢查對象中深度嵌套的屬性,可以使用點表示法或包含深度引用的keyPath的數組。
可選地,你可以提供一個值來檢查它是否等於目標對象的keyPath中的值。此matcher使用“深度相等”(如toEqual()))並遞歸地檢查所有字段的相等性。
下面的示例包含一個帶有嵌套屬性的houseForSale對象。我們使用tohave屬性來檢查對象中各種屬性的存在性和值。
// Object containing house features to be tested const houseForSale = { bath: true, bedrooms: 4, kitchen: { amenities: ['oven', 'stove', 'washer'], area: 20, wallColor: 'white', 'nice.oven': true, }, }; test('this house has my desired features', () => { // Simple Referencing expect(houseForSale).toHaveProperty('bath'); expect(houseForSale).toHaveProperty('bedrooms', 4); expect(houseForSale).not.toHaveProperty('pool'); // Deep referencing using dot notation expect(houseForSale).toHaveProperty('kitchen.area', 20); expect(houseForSale).toHaveProperty('kitchen.amenities', [ 'oven', 'stove', 'washer', ]); expect(houseForSale).not.toHaveProperty('kitchen.open'); // Deep referencing using an array containing the keyPath expect(houseForSale).toHaveProperty(['kitchen', 'area'], 20); expect(houseForSale).toHaveProperty( ['kitchen', 'amenities'], ['oven', 'stove', 'washer'], ); expect(houseForSale).toHaveProperty(['kitchen', 'amenities', 0], 'oven'); expect(houseForSale).toHaveProperty(['kitchen', 'nice.oven']); expect(houseForSale).not.toHaveProperty(['kitchen', 'open']); });
寫在最后
文中列出的只是其中一部分,更多詳細信息請參考官網: https://facebook.github.io/jest/docs/en/expect.html#expectanything
系列教程:
1. 前端測試框架Jest系列教程 -- Matchers(匹配器)
2.前端測試框架Jest系列教程 -- Asynchronous(測試異步代碼)
3.前端測試框架Jest系列教程 -- Mock Functions(模擬器)