Jamie Chien • 2023-06-04
Unit Test (3):常見的 Matchers (Assertions) 整理
前言
Matchers的機制可以讓我們利用各種方法來進行測試來判斷結果是否符合預期。這篇文章會列出一些常用的 matchers,並簡單地用一個範例來介紹,文章的最後也會簡單介紹如何客製化自己的 matchers~
目錄
Jest 中的 Matchers (完整清單)
…toBe() -> Link
透過 .toBe 來比較原始型別(Primitive)的值,使用他甚至比使用 === 還來的適合
只有Primitive type才使用 .toBe ,其他type的請使用其他斷言 ( .toEqual ……)(Primitive type 包含 Number, String, Boolean, Undefined, Null, Symbol)
浮點數的比較不要使用 .toBe ,請使用 .toBeCloseTo
ex: 在Javascript中,0.2 + 0.1 不嚴格(not-strictly)等於 0.3
Example
const box = {
football: 10,
basketball: 12
}
it('should have 12', () => {
expect(box.basketball).toBe(12);
})…toHaveBeenCalled() -> Link
透過 .toHaveBeenCalled 來確認mock function是否有被成功呼叫,他同時也有個別名: .toBeCalled
Example
function callFn(callback) {
callback();
}
it('should call mock function', () => {
const mockFn = jest.fn();
callFn(mockFn);
expect(mockFn).toHaveBeenCalled();
})…toHaveBeenCalledTimes(number) -> Link
number) -> Link透過 .toHaveBeenCalledTimes 來確保mock function有被成功呼叫與傳入的 number 參數一樣的次數,他同時也有個別名: .toBeCalledTimes(number)
Example
function callFn(callback) {
callback();
}
it('should call mock function one time', () => {
const mockFn = jest.fn();
callFn(mockFn);
expect(mockFn).toHaveBeenCalledTimes(1);
})…toHaveBeenCalledWith(arg1, arg2, …) -> Link
arg1, arg2, …) -> Link透過 .toHaveBeenCalledWith 來確保mock function呼叫時的參數與傳入的參數(arg1, arg2) 一致,他同時也有個別名: .toBeCalledWith
Example
function callFn(callback, args) {
callback(args);
}
it('should call mock function with specific params', () => {
const mockFn = jest.fn((num) => num);
callFn(mockFn, 123);
expect(mockFn).toHaveBeenCalledWith(123);
})…toHaveReturned() -> Link
透過 .toHaveReturned 來確保mock function成功回傳至少一次(i.e. 沒有throw error),他同時也有個別名: .toReturn
Example
it('should return mock function', () => {
const mockFn = jest.fn(() => true);
mockFn();
expect(mockFn).toHaveReturned();
})…toHaveReturnedTimes(number) -> Link
number) -> Link透過 .toHaveReturnedTimes 來確保mock function成功回傳數次(i.e. 沒有throw error),而次數與傳入的參數 number 一致,他同時也有個別名: .toReturnTimes(number)
Example
it('should return mock function twice', () => {
const mockFn = jest.fn(() => true);
mockFn();
mockFn();
expect(mockFn).toHaveReturnedTimes(2);
})…toHaveReturnedWith(value) -> Link
value) -> Link透過 .toHaveReturnedWith 來確保mock function回傳的值與傳入的參數 value 一致,他同時也有個別名: .toReturnWith(value)
Example
it('should return mock function twice', () => {
const mockFn = jest.fn(() => 'hello world');
mockFn();
expect(mockFn).toHaveReturnedWith('hello world');
})…toHaveLength(number) ->
Link
number) ->
Link透過 .toHaveLength 來確保該object擁有 .length 屬性,並且與傳入的參數
number 一致
Example
expect('').not.toHaveLength(5); ```
</div>
</details>
</div>
</details>
{/* toHaveProperty */}
<details>
<summary>
<h3 style="margin: 0">
...toHaveProperty(<code style="font-size: 16px">keyPath, value?</code>) -> <a href="https://jestjs.io/docs/expect#tohavepropertykeypath-value">Link</a>
</h3>
</summary>
<div style="padding-left: 14px">
透過 <code>.toHaveProperty</code> 來確保提供的參數 <code>keyPath</code> 是否有存在object中,而第二個參數 <code>value</code> 則是選填,可以用來比對這個keyPath的值是否一致。
<Callout type="tip">
第二個參數 <code>value</code> 比對的方式相當於 <code>toEqual</code>
</Callout>
<details>
<summary style="font-style: italic; font-weight: 600">Example</summary>
<div style="padding-left: 14px">
```javascript
const box = {
football: 10,
basketball: 12,
toy: [
'LEGO',
'doll'
]
}
it('should have property', () => {
expect(box).toHaveProperty('football');
expect(box).toHaveProperty('basketball', 12);
expect(box).not.toHaveProperty('baseball');
expect(box).toHaveProperty('toy', ['LEGO', 'doll']);
})…toBeCloseTo(number, numberDigits?) -> Link
number, numberDigits?) -> Link透過 .toBeCloseTo 來比較大致相等的浮點數數字 (floating point numbers)
第二個參數 numberDigits 是用來限制要檢查的位數 (小數點後的位數),預設為 2
Example
it('should have 12', () => {
expect(0.2 + 0.1).toBeCloseTo(0.3, 5);
})…toBeDefined() -> Link
透過 .toBeDefined 來檢查變數,確保他不是 undefined
Example
let definedValue = 'Hi';
let undefinedValue;
it('should check variable', () => {
expect(definedValue).toBeDefined();
expect(undefinedValue).not.toBeDefined();
})…toBeFalsy() -> Link
透過 .toBeFalsy 來確認是否為falsy value
在JavaScript中,總共有六個falsy value,分別是: false、0、""、null、undefined、NaN
Example
it('should be falsy', () => {
const falsyValue = false;
expect(falsyValue).toBeFalsy();
})…toBeTruthy() -> Link
透過 .toBeTruthy 來確認是否為truthy value,除了下面提到的六個falsy value外,其餘的值都會被視為truthy value
在JavaScript中,總共有六個falsy value,分別是: false、0、""、null、undefined、NaN
Example
it('should be truthy', () => {
const truthyValue = true;
expect(truthyValue).toBeTruthy();
})…toBeGreaterThan(number | bigint) -> Link
number | bigint) -> Link透過 .toBeGreaterThan 來確認 收到的值 > 期望的值
Example
it('should be greater than 10', () => {
const receivedValue = 11;
expect(receivedValue).toBeGreaterThan(10);
})…toBeGreaterThanOrEqual(number | bigint) -> Link
number | bigint) -> Link透過 .toBeGreaterThanOrEqual 來確認 收到的值 >= 期望的值
Example
it('should be greater than or equal 10', () => {
const receivedValue = 10;
expect(receivedValue).toBeGreaterThanOrEqual(10);
})…toBeLessThan(number | bigint) -> Link
number | bigint) -> Link透過 .toBeLessThan 來確認 收到的值 < 期望的值
Example
it('should be less than 10', () => {
const receivedValue = 9;
expect(receivedValue).toBeLessThan(10);
})…toBeLessThanOrEqual(number | bigint) -> Link
number | bigint) -> Link透過 .toLessThanOrEqual 來確認 收到的值 <= 期望的值
Example
it('should be less than or equal 10', () => {
const receivedValue = 10;
expect(receivedValue).toBeLessThanOrEqual(10);
})…toBeInstanceOf(Class) -> Link
Class) -> Link透過 .toBeInstanceOf 來確認object是某個class的實例(instance)
Example
class A {}
it('should be instance of class A', () => {
expect(new A()).toBeInstanceOf(A);
expect(() => {}).toBeInstanceOf(Function);
})…toBeNull() -> Link
透過 .toBeNull 來確認是否為null,他與 toBe(null)相同,但使用 .toBeNull 的error messages更好
Example
it('should be null', () => {
const nullValue = null;
expect(nullValue).toBeNull();
})…toBeUndefined() -> Link
透過 .toBeUndefined 來確認是否為 undefined
Example
it('should be undefined', () => {
let undefinedValue;
expect(undefinedValue).toBeUndefined();
})…toBeNaN() -> Link
透過 .toBeNaN 來確認是否為 NaN
Example
it('should check NaN', () => {
expect(NaN).toBeNaN();
expect(1).not.toBeNaN();
})…toContain(item) -> Link
item) -> Link透過 .toContain 來確認某個item是否在array裡面,或是確認某個string item是否為string的substring,此外,也可以用在sets, node lists, HTML collections中
Example
it('should contain item', () => {
const item = 'hi'
expect(['hi', 'hello']).toContain(item);
expect('hi ~ everyone').toContain(item);
expect(new Set(['hi'])).toContain(item);
})…toEqual(value) -> Link
value) -> Link透過 .toEqual 來確認object是否與期望的一致 (deep equality)
當要比較Object type時,必須使用 .toEqual
Example
it('should be equal', () => {
const obj1 = { name: 'Jamie', info: { age: 24 } }
const obj2 = { name: 'Jamie', info: { age: 24 } }
expect(obj1).toEqual(obj2);
})…toMatch(regexp | string) -> Link
regexp | string) -> Link透過 .toMatch 來確認string符合正則的規則
Example
it('should be match', () => {
const phone = '0912345678'
const validator = /^(09)[0-9]{8}$/
expect(phone).toMatch(validator);
})…toThrow(error?) -> Link
error?) -> Link透過 .toThrow 來確認function有沒有throw
要把code用function包起來,不然error不會被抓到,間接導致測試失敗
Example
it('should throw', () => {
function throwFunc() {
throw new Error();
}
expect(() => throwFunc()).toThrow();
})React Testing Library 中的 Matchers (完整清單)
…toBeDisabled() -> Link
透過 .toBeDisabled 來確認element有沒有被disabled
以下的elements是可以被disabled的: button、input、select、textarea、optgroup、option、fieldset 和客製化的elements
Example
<button data-testid="button" type="submit" disabled>submit</button>it('should be disabled', () => {
expect(getByTestId('button').toBeDisabled();
})…toBeEmptyDOMElement() -> Link
透過 .toBeEmptyDOMElement 來確認element有沒有不可視(not visible)的內容
這個函式會忽略註解,但如果包含空格(white-space)的話,會導致測試失敗
Example
<span data-testid="not-empty"><span data-testid="empty"></span></span>it('should be empty | not empty', () => {
expect(getByTestId('empty')).toBeEmptyDOMElement()
expect(getByTestId('not-empty')).not.toBeEmptyDOMElement()
})…toBeInTheDocument() -> Link
透過 .toBeInTheDocument 來確認element是否有呈現在document中
Example
<span data-testid="html-element"><span>Html Element</span></span>it('should be (not) in the document', () => {
expect(getByTestId(document.documentElement, 'html-element')).toBeInTheDocument()
expect(getByTestId(document.documentElement, 'not-exist')).not.toBeInTheDocument()
})…toBeRequired() -> Link
透過 .toBeRequired 來確認form element目前是否為 required
element required當他有以下attribute: required、aria-required=“true”
Example
<input data-testid="input" required />
<select data-testid="select"></select>it('should be (not) required', () => {
expect(getByTestId('input')).toBeRequired()
expect(getByTestId('select')).not.toBeRequired()
})…toBeVisible() -> Link
透過 .toBeVisible 來確認element目前是否可以被user看到
element 要被看到必須要符合所有以下條件:
- 呈現在document中
- css 的屬性
display沒有設定成none - css 的屬性
visibility沒有設定成hidden或collapse - css 的屬性
opacity沒有設定成0 - 此element的parent也是visible的狀態 (包含所有parent直到DOM tree的頂點)
- 此element沒有
hidden的attribute - 如果
<details />,那他要有open的attribute
Example
<div data-testid="display-none" style="display: none">Display None Example</div>
<div data-testid="visibility-el">Hi</div>it('should be (not) visible', () => {
expect(getByTestId('display-none')).not.toBeVisible()
expect(getByTestId('visibility-el')).toBeVisible()
})…toContainElement(
element: HTMLElement | SVGElement | null) ->
Link
element: HTMLElement | SVGElement | null) ->
Link透過 .toContainElement 來確認element是否有包含其他element作為子孫
Example
<span data-testid="ancestor">
<span data-testid="descendant"></span>
</span>
``` ```javascript it('should contain element', () =>{' '}
{expect(getByTestId('ancestor')).toContainElement(getByTestId('descendant'))}) ```
</div>
</details>
</div>
</details>
{/* toContainHTML */}
<details>
<summary>
<h3 style="margin: 0">
...toContainHTML(<code style="font-size: 16px">htmlText: string</code>) ->{' '}
<a href="https://github.com/testing-library/jest-dom#tocontainhtml">Link</a>
</h3>
</summary>
<div style="padding-left: 14px">
透過 <code>.toContainHTML</code> 來確認element是否有包含其他html element作為子孫,且此html
element使用string來做表示
<details>
<summary style="font-style: italic; font-weight: 600">Example</summary>
<div style="padding-left: 14px">
```javascript
<span data-testid="parent">
<span data-testid="child"></span>
</span>
``` ```javascript it('should contain element', () =>{' '}
{expect(getByTestId('parent')).toContainHTML('<span data-testid="child"></span>')}) ```
</div>
</details>
</div>
</details>
{/* toHaveAttribute */}
<details>
<summary>
<h3 style="margin: 0">
...toHaveAttribute(<code style="font-size: 16px">attr: string, value?: any</code>) -> <a href="https://github.com/testing-library/jest-dom#tohaveattribute">Link</a>
</h3>
</summary>
<div style="padding-left: 14px">
透過 <code>.toHaveAttribute</code> 來確認element是否有該attribute,也可以加上value來確認這個attribute是否有該value
<details>
<summary style="font-style: italic; font-weight: 600">Example</summary>
<div style="padding-left: 14px">
```javascript
<button data-testid="ok-button" type="submit" disabled>ok</button>it('should have attribute', () => {
const button = getByTestId('ok-button')
expect(button).toHaveAttribute('disabled')
expect(button).toHaveAttribute('type', 'submit')
})…toHaveClass(…classNames: string[], options?: {exact: boolean}) -> Link
…classNames: string[], options?: {exact: boolean}) -> Link透過 .toHaveClass 來確認element是否有該class,必須至少給一個class,否則此element會被視為沒有任何classes
Example
<button data-testid="delete-button" class="btn extra btn-danger">
Delete item
</button>
<button data-testid="no-classes">No Classes</button>it('should (not) have class', () => {
const deleteButton = getByTestId('delete-button')
const noClasses = getByTestId('no-classes')
expect(deleteButton).toHaveClass('extra')
expect(deleteButton).toHaveClass('btn-danger btn')
expect(noClasses).not.toHaveClass()
})…toHaveFocus() -> Link
透過 .toHaveFocus 來確認element是否有被focus
Example
<div><input type="text" data-testid="element-to-focus" /></div>it('should (not) have focus', () => {
const input = getByTestId('element-to-focus')
input.focus()
expect(input).toHaveFocus()
input.blur()
expect(input).not.toHaveFocus()
})…toHaveStyle(css: string | object) -> Link
css: string | object) -> Link透過 .toHaveStyle 來確認element是否有specific css屬性和value
Example
<button
data-testid="delete-button"
style="display: none"
>
Delete item
</button>it('should have style', () => {
const button = getByTestId('delete-button')
expect(button).toHaveStyle('display: none')
expect(button).toHaveStyle({display: 'none'})
})…toHaveTextContent(text: string | RegExp, options?: { normalizeWhitespace: boolean}) -> Link
text: string | RegExp, options?: { normalizeWhitespace: boolean}) -> Link透過 .toHaveTextContent 來確認element是否有具體的文字內容
若要忽略大小寫問題,可以使用 RegExp 並使用 /i
Example
<span data-testid="text-content">Text Content</span>it('should have style', () => {
const el = getByTestId('text-content')
expect(el).toHaveTextContent('Content')
expect(el).toHaveTextContent(/^Text Content$/) // to match whole content
})…toHaveValue(value: string | string[] | number) -> Link
value: string | string[] | number) -> Link透過 .toHaveTextValue 來確認form element是否有具體的值
可以用來確認以下form element的value: <input>、<select>、<textarea>
(但不包括 <input type=“checkbox”> 和 <input type=“radio”> )
若要確認其他form element的值,可以使用 toHaveFormValues 這個斷言
Example
<input type="text" value="text" data-testid="input-text" />
<input type="number" value="5" data-testid="input-number" />
<input type="text" data-testid="input-empty" />
<select multiple data-testid="select-number">
<option value="first">First Value</option>
<option value="second" selected>Second Value</option>
<option value="third" selected>Third Value</option>
</select>it('should have value', () => {
const textInput = getByTestId('input-text')
const numberInput = getByTestId('input-number')
const emptyInput = getByTestId('input-empty')
const selectInput = getByTestId('select-number')
expect(textInput).toHaveValue('text')
expect(numberInput).toHaveValue(5)
expect(emptyInput).not.toHaveValue()
expect(selectInput).toHaveValue(['second', 'third'])
})…toHaveDisplayValue(value: string | RegExp | (string | RegExp)[]) -> Link
value: string | RegExp | (string | RegExp)[]) -> Link透過 .toHaveTextValue 來確認form element是否有具體的值
可以用來確認以下form element的value: <input>、<select>、<textarea>
(但不包括 <input type=“checkbox”> 和 <input type=“radio”> )
Example
<label for="input-example">First name</label>
<input type="text" id="input-example" value="Luca" />
<label for="textarea-example">Description</label>
<textarea id="textarea-example">An example description here.</textarea>it('should have display value', () => {
const input = getByLabelText('First name')
const textarea = getByLabelText('Description')
expect(input).toHaveDisplayValue('Luca')
expect(input).toHaveDisplayValue(/Luc/)
expect(textarea).toHaveDisplayValue('An example description here.')
expect(textarea).toHaveDisplayValue(/example/)
})…toBeChecked() -> Link
透過 .toHaveChecked 來確認element是否為 checked
可以用來確認以下element:
input的型態為checkbox或radiorole為checkbox、radio或switcharia-checked的屬性(attribute)為true或false
Example
<input type="checkbox" checked data-testid="input-checkbox-checked" />
<input type="checkbox" data-testid="input-checkbox-unchecked" />
<div role="checkbox" aria-checked="true" data-testid="aria-checkbox-checked" />
<div
role="checkbox"
aria-checked="false"
data-testid="aria-checkbox-unchecked"
/>
<input type="radio" checked value="foo" data-testid="input-radio-checked" />
<input type="radio" value="foo" data-testid="input-radio-unchecked" />
<div role="radio" aria-checked="true" data-testid="aria-radio-checked" />
<div role="radio" aria-checked="false" data-testid="aria-radio-unchecked" />
<div role="switch" aria-checked="true" data-testid="aria-switch-checked" />
<div role="switch" aria-checked="false" data-testid="aria-switch-unchecked" />it('should have checked or unchecked', () => {
const inputCheckboxChecked = getByTestId('input-checkbox-checked')
const inputCheckboxUnchecked = getByTestId('input-checkbox-unchecked')
const ariaCheckboxChecked = getByTestId('aria-checkbox-checked')
const ariaCheckboxUnchecked = getByTestId('aria-checkbox-unchecked')
expect(inputCheckboxChecked).toBeChecked()
expect(inputCheckboxUnchecked).not.toBeChecked()
expect(ariaCheckboxChecked).toBeChecked()
expect(ariaCheckboxUnchecked).not.toBeChecked()
const inputRadioChecked = getByTestId('input-radio-checked')
const inputRadioUnchecked = getByTestId('input-radio-unchecked')
const ariaRadioChecked = getByTestId('aria-radio-checked')
const ariaRadioUnchecked = getByTestId('aria-radio-unchecked')
expect(inputRadioChecked).toBeChecked()
expect(inputRadioUnchecked).not.toBeChecked()
expect(ariaRadioChecked).toBeChecked()
expect(ariaRadioUnchecked).not.toBeChecked()
const ariaSwitchChecked = getByTestId('aria-switch-checked')
const ariaSwitchUnchecked = getByTestId('aria-switch-unchecked')
expect(ariaSwitchChecked).toBeChecked()
expect(ariaSwitchUnchecked).not.toBeChecked()
})客製化 Matchers
透過 expect.extend ,我們可以自己實作 matchers 來用在 unit test 上,詳細使用方式可以參考官方 docs,以下用一個例子來簡述:
Example
這個範例中,我們透過創建一個 toContainRole 來自己創建一個客製化的 matcher,避免因為重複寫同樣的程式碼而變成碼農
function ButtonComponent() {
return (
<div>
<button>Submit</button>
<button>Cancel</button>
</div>
)
}
DataForm.test.js
function toContainRole(container, role, quantity = 1) {
const elements = within(container).queryAllByRole(role);
if (elements.length === quantity) {
return {
pass: true
}
}
return {
pass: false,
message: () => `Expected to find ${quantity} ${role} elements.`
}
}
// 透過 expect.extend 來使用 custom matchers
expect.extend({ toContainRole })
it('should display two buttons', () => {
render(<DataForm />);
const form = screen.getByRole('form');
expect(form).toContainRole('button', 2);
})
Unit Test 系列其他文章
Unit Test (1):介紹單元測試Unit Test (2):Query Functions 整理