React 搭配Testing Library 实作测试

摘要:虽然Jest 本身有提供很多测试方法,但在测试上都比较偏向逻辑测试,像是a + b 是否等于c。而实际上我们所需要的测试有一大部分也包含UI 的测试,像是画面上有没显示正确文字

本篇使用的JavaScript 框架是Jest,所以在开始前要先了解一些Jest 基本语法,才会比较快理解喔!

 

测试库

虽然Jest 本身有提供很多测试方法,但在测试上都比较偏向逻辑测试,像是a + b 是否等于c。而实际上我们所需要的测试有一大部分也包含UI 的测试,像是画面上有没显示正确文字,或是使用者点击有没有跳出视窗等等,这时候就是Testing Library出马的时候了!

它的官网介绍就很明确地告诉你

The @testing-library family of packages helps you test UI components in a user-centric way.

也就是@testing-library 系列的套件可以帮助你以用户为中心的方式测试UI 元件。

Testing Library 不只提供可以共通使用的测试套件,还有专为各个框架所出的套件,基本的三大框架React、Vue、Angular 都有属于他们的测试UI 套件。而比较新的框架像是Preact、Svelte 也都有在持续更新。

 

Testing Libaray For React

以React 来说,搭配Jest 会需要的Testing Libary 有以下几个:

1. @testing-library/react - 模拟React 渲染,Ex:render、screen

2. @testing-library/jest-dom - 扩充jest 的断言库,Ex:toBeInTheDocument、toHaveClass

3. @testing-library/user-event - 模拟使用者操作,Ex:useEvent.click、userEvent.type

接下来来各别介绍这三个的使用时机:

◆ @testing-library/react

主要是提供模拟渲染UI 画面的函式,比较常用的有

1.render()

就如同React 中的render,就是模拟渲染React 元件,假设有一个<Home/>元件:

export const Home = () => {
  return <h1>Home Page</h1>
};

要测试元件的DOM 元素时,就可以使用render()函式。

import { render } from '@testing-library/react'

describe('testing home component', () => {
  it('show Home Page in the home component', () => {
    const {getByText} = render(<Home/>);
    expect(getByText('Home Page')).toBeTruthy(); //使用 getByText 判斷抓到文字的元素是否存在
  });
});

render() 函式会回传一个物件包含一些属性对象:

  • Query:getBy、queryBy、findBy、getAllBy、queryAllBy、findAllBy,取得元件内的元素。

  • container:渲染的DOM 节点。

  • debug:侦错函式,可以显示当前的DOM 结构。

  • rerender:重新渲染元件。

  • unmount:取消渲染。

Query 有很多不同的取元素方法,有get、find、query,各自都有取得多元素的all 方法

Query 种类\结果

没有符合

一项符合大于一个符合是否为非同步函式
getBy...Throw error

回传元素

Throw error
queryBy...回传null

回传元素

Throw error
findBy...Throw error

回传元素

Throw error
getAllBy...Throw error

回传阵列元素

回传阵列元素
queryAllBy...回传[]

回传阵列元素

回传阵列元素
findAllBy...Throw error

回传阵列元素

回传阵列元素

 

看起来很容易搞混,不过他们都有各自使用的时机

  • getBy...:大部分都可以使用,判断元素是否存在。

  • queryBy...:因为找不到会回传null的特性,所以常用来判断元素是否一开始不存在。

  • findBy...:非同步函式,可以判断需等待的元素是否存在,例如API 回传才会显示在画面上。

2.screen()

虽然render()解决了模拟UI 的情况,不过只能根据render()的内容进行测试,如果有多个render()函式测起来就会比较麻烦,这时候就可以使用screen()。

其实screen()算是@testing-library/dom 所提供,而所有框架的@testing-library 底层都有@testing-library/dom,所以这边才可以直接做使用。

而screen()所抓取的元素是<body></body> 内的所有DOM 元素。

import { render, screen } from '@testing-library/react'

describe('testing home component', () => {
  it('show Home Page in the home component', () => {
    render(<Home/>);
    render(<Home/>);
    expect(getAllByText('Home Page')).toBeTruthy(); //使用 getAllByText 一次判斷兩個 Home 元件元素是否存在
  });
});

个人在抓元素时是比较常用  screen()胜过于使用render()回传的方法,使用起来也比较方便。

3.renderHook()

顾名思义就是去模拟客制化的React Hook,假设今天有一个计数器的customhook

import { useState } from 'react';

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  return { count, increment, decrement };
}

export default useCounter;

要测试这个hook 就可以使用renderHook(),他可以传入两个参数,第一个就是要测试的函式,第二是传入函式的参数(非必要)。

import useCounter from './useCounter';
import { renderHook, act } from '@testing-library/react';

it("should increment and decrement the counter", () => {
  const { result } = renderHook(useCounter);

  expect(result.current.count).toBe(0);

  act(() => {
    result.current.increment(); // 呼叫 + 1 函式
  });

  expect(result.current.count).toBe(1);

  act(() => {
    result.current.decrement(); // 呼叫 -1 函式
  });

  expect(result.current.count).toBe(0);
});

renderHook()会回传一个物件,可以使用result属性利用 result.current去操控函式的回传物件。

如果有更改state的操作,就需要使用act包起来,这样才可以即时更新state的状态。

 

◆ @testing-library/jest-dom

@testing-library/jest-dom 提供给Jest 很多DOM 元素的扩充判断,让我们在抓元素时,有更多的方法去测试,比较常用的像是toBeInTheDocument、toHaveClass等等。

以刚刚的<Home/>渲染测试为例

import { render } from '@testing-library/react'

describe('testing home component', () => {
  it('show Home Page in the home component', () => {
    const {getByText} = render(<Home/>);
    expect(getByText('Home Page')).toBeInTheDocument(); //判斷元素是否存在於 Document
  });
});

或是

import { render } from '@testing-library/react'

describe('testing div', () => {
  it('test div classname is hide', () => {
    render(<div className='hide'>test</div>);
    expect(screen.getByText('test')).toHaveClass('hide'); //判斷元素是否含有指定的 class name
  });
});

 

◆ @testing-library/user-event

最后一个就是模拟使用着操作,user-event 常常跟另一个@testing-library/react 的fireEvent 拿来比较,fireEvent 就是在程式码中会用的事件处理,像是click 或是change,而user-event的底层就是fireEvent,不过userEvent 能更贴合使用者的模拟情况,像是同样是在input 输入文字,如果使用fireEvent 就会使用change 的事件。

fireEvent:

import { fireEvent } from '@testing-library/react';

fireEvent.change(inputElement, { target: { value: 'Hello, world!' } });

不过如果是用userEvent 就会使用type 的事件。

userEvent:

import userEvent from '@testing-library/user-event';

const user = userEvent.setup()
user.type(inputElement, 'Hello, world!');

乍看之下没有什么不一样,不过userEvent 的type 还包含使用者点击input,输入文字的keydown、keyup 事件,比起fireEvent 的change,更贴近实际的使用者操作情况。

除此之外,userEvent 还有提供很多实用的方法像是:

  • dblClick:点击两次

  • tripleClick:点击三次

  • type:input 输入

  • upload:上传

  • hover/unhover:指标移进移出

  • copy/paste:复制/贴上

详细的可以参考User Interactions

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://shenqiku.cn/article/FLY_12674