Jamie Chien

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

透過 .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

透過 .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

透過 .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

透過 .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

透過 .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

透過 .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,分別是: false0""nullundefinedNaN

Example
it('should be falsy', () => {
  const falsyValue = false;

  expect(falsyValue).toBeFalsy();
})

…toBeTruthy() -> Link

透過 .toBeTruthy 來確認是否為truthy value,除了下面提到的六個falsy value外,其餘的值都會被視為truthy value

在JavaScript中,總共有六個falsy value,分別是: false0""nullundefinedNaN

Example
it('should be truthy', () => {
  const truthyValue = true;

  expect(truthyValue).toBeTruthy();
})

…toBeGreaterThan(number | bigint) -> Link

透過 .toBeGreaterThan 來確認 收到的值 > 期望的值

Example
it('should be greater than 10', () => {
  const receivedValue = 11;

  expect(receivedValue).toBeGreaterThan(10);
})

…toBeGreaterThanOrEqual(number | bigint) -> Link

透過 .toBeGreaterThanOrEqual 來確認 收到的值 >= 期望的值

Example
it('should be greater than or equal 10', () => {
  const receivedValue = 10;

  expect(receivedValue).toBeGreaterThanOrEqual(10);
})

…toBeLessThan(number | bigint) -> Link

透過 .toBeLessThan 來確認 收到的值 < 期望的值

Example
it('should be less than 10', () => {
  const receivedValue = 9;

  expect(receivedValue).toBeLessThan(10);
})

…toBeLessThanOrEqual(number | bigint) -> Link

透過 .toLessThanOrEqual 來確認 收到的值 <= 期望的值

Example
it('should be less than or equal 10', () => {
  const receivedValue = 10;

  expect(receivedValue).toBeLessThanOrEqual(10);
})

…toBeInstanceOf(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

透過 .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

透過 .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

透過 .toMatch 來確認string符合正則的規則

Example
it('should be match', () => {
  const phone = '0912345678'
  const validator = /^(09)[0-9]{8}$/
  
  expect(phone).toMatch(validator);
})

…toThrow(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的: buttoninputselecttextareaoptgroupoptionfieldset 和客製化的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: requiredaria-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 沒有設定成 hiddencollapse
  • 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

透過 .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

透過 .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

透過 .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

透過 .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

透過 .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

透過 .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 的型態為 checkboxradio
  • rolecheckboxradioswitch
  • aria-checked 的屬性(attribute)為 truefalse
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,避免因為重複寫同樣的程式碼而變成碼農

DataForm.js
  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 整理