最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React Testing Library使用总结

    正文概述 掘金(yosang)   2020-12-17   521
    • React单元测试
      • React Testing Library
        • 问题
        • 解决
        • 不具备的部分
        • React Testing Library 常见测试场景
          • Rendering a component
          • Selecting elements
          • Search variants
            • getBy和queryBy的不同
            • Search multiple elements
            • Assertive Functions
          • Fire event
          • Callback handlers
          • Asynchronous
          • React Router
          • React Redux
        • React Testing Library 常见的错误使用方式
        • 应该使用哪个查询
          • 注意
          • 有用的浏览器扩展
        • Reference

    React Testing Library

    React Testing Library是基于DOM Testing Library构建的,它提供了一些用于处理React components的api。(如果使用Create React App创建的项目,那么它已经支持使用React Testing Library编写测试代码)

    问题

    如果你希望为你的WEB UI 编写可维护的测试。为了实现这个目标,你希望测试可以避开组件的具体实现细节,而是更关注于其是否能保证实现你期望的功能。另一方面,测试库应该是长期可维护的状态,在改变应用的实现方式不改变功能的情况下(也就是代码重构),不需要重新编写测试,拖慢项目进度。

    解决

    React Testing Library是测试React components的非常轻量级的解决方案。它提供的主要功能是类似于用户在页面上查找元素的方式查找DOM节点。通过这种测试方式,可以让你确保Web UI是否能正常工作。React Testing Library的主要指导原则是:

    你可以通过Label查找表单元素,通过Text查找链接和按钮,以及其它类似的查找方式。同时它还提供data-testid用于查找内容或标签没有意义或不实际的元素(我的理解是类似按钮是一个图标的情况,无法直接描述)

    这个库是Enzyme的替代品。你可以使用Enzyme遵循上面的规则进行测试,但是因为Enzyme提供了很多额外的对于应用实现细节测试的功能,所以强行使用它会增加测试编写的难度。

    不具备的部分

    1. 测试运行程序或框架
    2. 特定于某个测试框架

    React Testing Library 常见测试场景

    Rendering a component

    下面是待测试组件:

    import React from 'react';
     
    const title = 'Hello React';
     
    function App() {
      return <div>{title}</div>;
    }
     
    export default App;
    

    在测试中可以通过render渲染一个组件,然后在后面的测试中便可以访问该组件

    import React from 'react';
    import { render } from '@testing-library/react';
     
    import App from './App';
     
    describe('App', () => {
      test('renders App component', () => {
        render(<App />);
      });
    });
    

    可以通过screen.debug()查看渲染出来的HTML DOM树是什么样的,在写测试代码前,先通过debug查看当前页面中可见的元素,再开始查询元素,这会有助于编写测试代码.

    import React from 'react';
    import { render, screen } from '@testing-library/react';
     
    import App from './App';
     
    describe('App', () => {
      test('renders App component', () => {
        render(<App />);
     
        screen.debug();
      });
    });
    
    <body>
      <div>
        <div>
          Hello React
        </div>
      </div>
    </body>
    

    下面是使用了React的一些特性(useState,event handler,props,component)以后,生成的HTML DOM树.可以看出React Testing Library 不关心真实的组件的编写方式,最后渲染出来的还是普通的HTML DOM树.所以我们在测试的时候,也只需要针对渲染出来的HTML DOM树进行测试即可.

    import React from 'react';
     
    function App() {
      const [search, setSearch] = React.useState('');
     
      function handleChange(event) {
        setSearch(event.target.value);
      }
     
      return (
        <div>
          <Search value={search} onChange={handleChange}>
            Search:
          </Search>
     
          <p>Searches for {search ? search : '...'}</p>
        </div>
      );
    }
     
    function Search({ value, onChange, children }) {
      return (
        <div>
          <label htmlFor="search">{children}</label>
          <input
            id="search"
            type="text"
            value={value}
            onChange={onChange}
          />
        </div>
      );
    }
     
    export default App;
    
    <body>
      <div>
        <div>
          <div>
            <label
              for="search"
            >
              Search:
            </label>
            <input
              id="search"
              type="text"
              value=""
            />
          </div>
          <p>
            Searches for
            ...
          </p>
        </div>
      </div>
    </body>
    

    React Testing库用于像用户一样与React组件进行交互。用户看到的只是从React组件渲染的HTML,因此这就是为什么将此HTML结构视为输出而不是两个单独的React组件的原因。

    Selecting elements

    在渲染完React组件以后,React Testing Library为你提供了多种不同的搜索方法用来获取元素.获取到的元素便可以用来进行后面的断言或者用户交互操作.下面来看看如何使用它们:

    import React from 'react';
    import { render, screen } from '@testing-library/react';
     
    import App from './App';
     
    describe('App', () => {
      test('renders App component', () => {
        render(<App />);
     
        expect(screen.getByText('Search:')).toBeInTheDocument();
      });
    });
    

    如果你不是很清楚组件渲染后的HTML DOM树,建议你先使用debug查看树结构.然后再通过screen对象的搜索方法查找你需要的元素.

    通常,如果没有找到元素,getByText会抛出错误,这样的错误提示会有助于让你知道,在你执行下一步的操作前,你没有正确的获取到你想要的元素.有的人也会使用此抛出错误的特性做隐式类型判断,但并不推荐这么用

    import React from 'react';
    import { render, screen } from '@testing-library/react';
     
    import App from './App';
     
    describe('App', () => {
      test('renders App component', () => {
        render(<App />);
     
        // 隐式类型判断
        // because getByText would throw error
        // if element wouldn't be there
        screen.getByText('Search:');
     
        // 显式类型判断
        // recommended
        expect(screen.getByText('Search:')).toBeInTheDocument();
      });
    });
    

    getByText不仅可以接受字符串作为查询条件,也可以接受正则表达式.字符串参数用于完全匹配,而正则表达式用于部分匹配,在某些情况下这样会更加方便和灵活.

    import React from 'react';
    import { render, screen } from '@testing-library/react';
     
    import App from './App';
     
    describe('App', () => {
      test('renders App component', () => {
        render(<App />);
     
        // fails
        expect(screen.getByText('Search')).toBeInTheDocument();
     
        // succeeds
        expect(screen.getByText('Search:')).toBeInTheDocument();
     
        // succeeds
        expect(screen.getByText(/Search/)).toBeInTheDocument();
      });
    });
    

    当然getByText只是众多搜索方法中的一种,其他的搜索方法,以及方法的优先及请参考后文应该使用哪个查询

    Search variants

    除了查询函数以外,还存在查询变体queryBy findBy.具体的方法如下:

    • queryByText
    • queryByRole
    • queryByLabelText
    • queryByPlaceholderText
    • queryByAltText
    • queryByDisplayValue
    • findByText
    • findByRole
    • findByLabelText
    • findByPlaceholderText
    • findByAltText
    • findByDisplayValue
    getBy和queryBy的不同

    在使用时最大的疑问通常是: 什么使用应该使用getBy,什么时候该使用其他的两个变体queryBy findBy

    如果要判断一个元素不存在,并进行断言.这时候如果使用getBy就会导致测试报错.使用queryBy便能正常的进行.

    import React from 'react';
    import { render, screen } from '@testing-library/react';
     
    import App from './App';
     
    describe('App', () => {
      test('renders App component', () => {
        render(<App />);
     
        screen.debug();
     
        // fails
        expect(screen.getByText(/Searches for JavaScript/)).toBeNull();
      });
    });
    
    import React from 'react';
    import { render, screen } from '@testing-library/react';
     
    import App from './App';
     
    describe('App', () => {
      test('renders App component', () => {
        render(<App />);
     
        expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();
      });
    });
    

    findBy通常用于异步元素.在下面的例子中,在初始化的渲染后,组件会远程获取user的数据信息,获取到数据以后重新渲染组件,条件渲染部分就会渲染出Signed in as.

    function getUser() {
      return Promise.resolve({ id: '1', name: 'Robin' });
    }
     
    function App() {
      const [search, setSearch] = React.useState('');
      const [user, setUser] = React.useState(null);
     
      React.useEffect(() => {
        const loadUser = async () => {
          const user = await getUser();
          setUser(user);
        };
     
        loadUser();
      }, []);
     
      function handleChange(event) {
        setSearch(event.target.value);
      }
     
      return (
        <div>
          {user ? <p>Signed in as {user.name}</p> : null}
     
          <Search value={search} onChange={handleChange}>
            Search:
          </Search>
     
          <p>Searches for {search ? search : '...'}</p>
        </div>
      );
    }
    

    如果我们想测试异步获取数据前后页面的变化,就可以使用findBy等待我们要更新的元素,不需要使用WaitFor.

    import React from 'react';
    import { render, screen } from '@testing-library/react';
     
    import App from './App';
     
    describe('App', () => {
      test('renders App component', async () => {
        render(<App />);
     
        expect(screen.queryByText(/Signed in as/)).toBeNull();
     
        expect(await screen.findByText(/Signed in as/)).toBeInTheDocument();
      });
    });
    
    Search multiple elements

    如果要断言多个元素,可以使用多元素查询方法

    • getAllBy
    • queryAllBy
    • findAllBy
    Assertive Functions

    除了常见的Jest的断言函数,React Testing Library还提供了一些常用的断言函数,类似于上文中我们用到的toBeInTheDocument.

    • toBeDisabled
    • toBeEnabled
    • toBeEmpty
    • toBeEmptyDOMElement
    • toBeInTheDocument
    • toBeInvalid
    • toBeRequired
    • toBeValid
    • toBeVisible
    • toContainElement
    • toContainHTML
    • toHaveAttribute
    • toHaveClass
    • toHaveFocus
    • toHaveFormValues
    • toHaveStyle
    • toHaveTextContent
    • toHaveValue
    • toHaveDisplayValue
    • toBeChecked
    • toBePartiallyChecked
    • toHaveDescription

    Fire event

    到目前为止,我们只接触了测试当前组件是否渲染了某个元素.接下来说一下用户交互:

    下面测试的场景是用户在input当中输入新的值,页面重新渲染,新的值会显示在页面上.

    import React from 'react';
    import { render, screen, fireEvent } from '@testing-library/react';
     
    import App from './App';
     
    describe('App', () => {
      test('renders App component', () => {
        render(<App />);
     
        screen.debug();
     
        fireEvent.change(screen.getByRole('textbox'), {
          target: { value: 'JavaScript' },
        });
     
        screen.debug();
      });
    });
    

    fireEvent函数的两个参数分别是,input元素和事件对象.screen.debug()输出键入新值以后渲染的HTML DOM树的变化,可以发现第二次的输出中包含了新的值.

    此外,如果你的组件包含异步任务,比如在页面加载的一开始先请求用户信息,那么上面的测试代码就会提示下面的错误信息: "Warning: An update to App inside a test was not wrapped in act(...).".这代表这里有异步任务需要我们等待,需要先等异步人物执行完毕以后再进行其它的操作

    describe('App', () => {
      test('renders App component', async () => {
        render(<App />);
     
        // wait for the user to resolve
        // needs only be used in our special case
        await screen.findByText(/Signed in as/);
     
        screen.debug();
     
        fireEvent.change(screen.getByRole('textbox'), {
          target: { value: 'JavaScript' },
        });
     
        screen.debug();
      });
    });
    

    然后我们再针对input键入事件前后页面变化进行断言

    describe('App', () => {
      test('renders App component', async () => {
        render(<App />);
     
        // wait for the user to resolve
        // needs only be used in our special case
        await screen.findByText(/Signed in as/);
     
        expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();
     
        fireEvent.change(screen.getByRole('textbox'), {
          target: { value: 'JavaScript' },
        });
     
        expect(screen.getByText(/Searches for JavaScript/)).toBeInTheDocument();
      });
    });
    

    针对event的测试,官方更推荐使用使用,具体原因,看下文常见的错误使用方式

    Callback handlers

    回调函数的测试方式: mock回调函数,传给render的组件即可

    function Search({ value, onChange, children }) {
      return (
        <div>
          <label htmlFor="search">{children}</label>
          <input
            id="search"
            type="text"
            value={value}
            onChange={onChange}
          />
        </div>
      );
    }
    
    describe('Search', () => {
      test('calls the onChange callback handler', () => {
        const onChange = jest.fn();
     
        render(
          <Search value="" onChange={onChange}>
            Search:
          </Search>
        );
     
        fireEvent.change(screen.getByRole('textbox'), {
          target: { value: 'JavaScript' },
        });
     
        expect(onChange).toHaveBeenCalledTimes(1);
      });
    });
    

    Asynchronous

    下面的例子是一个远程获取数据以后展示在页面上的例子:

    import React from 'react';
    import axios from 'axios';
     
    const URL = 'http://hn.algolia.com/api/v1/search';
     
    function App() {
      const [stories, setStories] = React.useState([]);
      const [error, setError] = React.useState(null);
     
      async function handleFetch(event) {
        let result;
     
        try {
          result = await axios.get(`${URL}?query=React`);
     
          setStories(result.data.hits);
        } catch (error) {
          setError(error);
        }
      }
     
      return (
        <div>
          <button type="button" onClick={handleFetch}>
            Fetch Stories
          </button>
     
          {error && <span>Something went wrong ...</span>}
     
          <ul>
            {stories.map((story) => (
              <li key={story.objectID}>
                <a href={story.url}>{story.title}</a>
              </li>
            ))}
          </ul>
        </div>
      );
    }
     
    export default App;
    

    单击按钮以后,请求开始.下面是对应的测试代码:

    import React from 'react';
    import axios from 'axios';
    import { render, screen } from '@testing-library/react';
    import userEvent from '@testing-library/user-event';
     
    import App from './App';
     
    jest.mock('axios');
     
    describe('App', () => {
      test('fetches stories from an API and displays them', async () => {
        const stories = [
          { objectID: '1', title: 'Hello' },
          { objectID: '2', title: 'React' },
        ];
     
        axios.get.mockImplementationOnce(() =>
          Promise.resolve({ data: { hits: stories } })
        );
     
        render(<App />);
     
        await userEvent.click(screen.getByRole('button'));
     
        const items = await screen.findAllByRole('listitem');
     
        expect(items).toHaveLength(2);
      });
    });
    

    在render组件以前,要先确保对http请求进行了mock处理,在进行请求时返回的便是我们的mock数据.

    测试请求出错的代码:

    import React from 'react';
    import axios from 'axios';
    import { render, screen } from '@testing-library/react';
    import userEvent from '@testing-library/user-event';
     
    import App from './App';
     
    jest.mock('axios');
     
    describe('App', () => {
      test('fetches stories from an API and displays them', async () => {
        ...
      });
     
      test('fetches stories from an API and fails', async () => {
        axios.get.mockImplementationOnce(() =>
          Promise.reject(new Error())
        );
     
        render(<App />);
     
        await userEvent.click(screen.getByRole('button'));
     
        const message = await screen.findByText(/Something went wrong/);
     
        expect(message).toBeInTheDocument();
      });
    });
    

    React Router

    待测试组件:

    // app.js
    import React from 'react'
    import { Link, Route, Switch, useLocation } from 'react-router-dom'
    
    const About = () => <div>You are on the about page</div>
    const Home = () => <div>You are home</div>
    const NoMatch = () => <div>No match</div>
    
    export const LocationDisplay = () => {
      const location = useLocation()
    
      return <div data-testid="location-display">{location.pathname}</div>
    }
    
    export const App = () => (
      <div>
        <Link to="/">Home</Link>
    
        <Link to="/about">About</Link>
    
        <Switch>
          <Route exact path="/">
            <Home />
          </Route>
    
          <Route path="/about">
            <About />
          </Route>
    
          <Route>
            <NoMatch />
          </Route>
        </Switch>
    
        <LocationDisplay />
      </div>
    )
    

    测试代码:

    // app.test.js
    import { render, screen } from '@testing-library/react'
    import userEvent from '@testing-library/user-event'
    import { createMemoryHistory } from 'history'
    import React from 'react'
    import { Router } from 'react-router-dom'
    
    import '@testing-library/jest-dom/extend-expect'
    
    import { App, LocationDisplay } from './app'
    
    test('full app rendering/navigating', () => {
      const history = createMemoryHistory()
      render(
        <Router history={history}>
          <App />
        </Router>
      )
      // verify page content for expected route
      // often you'd use a data-testid or role query, but this is also possible
      expect(screen.getByText(/you are home/i)).toBeInTheDocument()
    
      const leftClick = { button: 0 }
      userEvent.click(screen.getByText(/about/i), leftClick)
    
      // check that the content changed to the new page
      expect(screen.getByText(/you are on the about page/i)).toBeInTheDocument()
    })
    
    test('landing on a bad page', () => {
      const history = createMemoryHistory()
      history.push('/some/bad/route')
      render(
        <Router history={history}>
          <App />
        </Router>
      )
    
      expect(screen.getByText(/no match/i)).toBeInTheDocument()
    })
    
    test('rendering a component that uses useLocation', () => {
      const history = createMemoryHistory()
      const route = '/some-route'
      history.push(route)
      render(
        <Router history={history}>
          <LocationDisplay />
        </Router>
      )
    
      expect(screen.getByTestId('location-display')).toHaveTextContent(route)
    })
    

    React Redux

    待测试组件:

    import { connect } from 'react-redux'
    
    const App = props => {
      return <div>{props.user}</div>
    }
    
    const mapStateToProps = state => {
      return state
    }
    
    export default connect(mapStateToProps)(App)
    

    测试代码:

    // test-utils.js
    import React from 'react'
    import { render as rtlRender } from '@testing-library/react'
    import { createStore } from 'redux'
    import { Provider } from 'react-redux'
    // Import your own reducer
    import reducer from '../reducer'
    
    function render(
      ui,
      {
        initialState,
        store = createStore(reducer, initialState),
        ...renderOptions
      } = {}
    ) {
      function Wrapper({ children }) {
        return <Provider store={store}>{children}</Provider>
      }
      return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
    }
    
    // re-export everything
    export * from '@testing-library/react'
    // override render method
    export { render }
    

    React Testing Library 常见的错误使用方式

    1. Using cleanup

    建议:don't use cleanup

    大多数主流的测试框架现在都会进行自动清理工作,所以不再需要手动的清理行为

    // bad
    import { render, screen, cleanup } from "@testing-library/react";
    afterEach(cleanup)
    
    // good
    import { render,screen } from "@testing-library/react";
    
    1. Not using screen

    建议:use screen for querying and debugging

    DOM Testing Library v6.11.0加入了screen,使用screen可以避免手动的添加和删除查询函数,你只需要使用screen,然后由编辑器帮你自动补全剩下的查询函数

    // bad
    const { getByRole } = render(<Example />);
    const errorMesssageNode = getByRole("alert");
    
    // good
    render(<Example />)
    const errorMessageNode = screen.getByRole("alert");
    
    1. Using the Wrong assertion

    建议:install and use @testing-library/jest-dom

    toBedisabled断言来自jest-dom。强烈建议使用jest-dom,因为这样收到的错误消息要好得多

    const button = screen.getByRole("button, {name: /disabled button/i});
    
    // bad
    expect(button.disabled).toBe(true);
    // error message:
    // expect(received).toBe(expected) // Obejct.is equality
    //
    // Expected: true
    // Received: false
    
    // good
    expect(button).toBeDisabled()
    // error massage
    // received element id not disabled
    // <button />
    
    1. Wrapping things in act unnecessarily

    建议:Learn when act is necessary and don't wrap things in act unnecessarily.

    render fireEvent已经包含了act的功能,所以不需要再使用act

    // bad
    act(() => {
      render(<Example />)
    });
    const input = screen.getByRole('textbox', {name: /choose a fruit/i});
    act(() => {
      fireEvent.keyDown(input, {key: 'ArrowDown'});
    });
    // good
    render(<Example />);
    const input = screen.getByRole('textbox', {name: /choose a fruit/i});
    fireEvent.keyDown(input, {key: 'ArrowDown'});
    
    1. Using the wrong query

    这是一个查询推荐顺序:应该使用哪个查询,使用最接近用户的方式进行查询

    // bad
    // assuming you've got this DOM to work with:
    // <label>Username</label><input data-testid="username" />
    screen.getByTestId('username');;
    
    // good
    // change the DOM to be accessible by associating the label and setting the type
    // <label for="username">Username</label><input id="username" type="text" />
    screen.getByRole('textbox', {name: /username/i});
    
    // bad
    const {container} = render(<Example />);
    const button = container.querySelector('.btn-primary');
    expect(button).toHaveTextContent(/click me/i);
    
    // good
    render(<Example />)
    screen.getByRole('button', {name: /click me/i});
    
    // bad
    screen.getByTestId('submit-button');
    
    // good
    screen.getByRole('button', {name: /submit/i});
    
    1. Not using @testing-library/user-event

    建议:Use @testing-library/user-event over fireEvent where possible.

    @testing-library/user-event是一个基于fireEvent构建的,它提供了几种与用户交互更相似的方法。 在下面的示例中,fireEvent.change将只触发input上面的change事件。 但是userEvent.type还会触发keyDown,keyPress和keyUp事件。它更接近于用户的实际交互。

    // bad
    fireEvent.change(input, {target: {value: 'hello world'}});
    
    // good
    userEvent.type(input, 'hello world');
    
    1. Using query variants for anything except checking for non-existence*

    建议:Only use the query* variants for asserting that an element cannot be found

    类似于queryByRole这样的查询方法,只有在判断一个元素不存在于当前页面中时使用

    // bad
    expect(screen.queryByRole('alert')).toBeInTheDocument();
    
    // good
    expect(screen.getByRole('alert')).toBeInTheDocument();
    expect(screen.queryByRole('alert')).not.toBeInTheDocument();
    
    1. Using waitFor to wait for elements that can be queried with find

    下面这两段代码是等效的,但是第二个更简单,而且错误提示信息也会更好

    // bad
    const submitButton = await waitFor(() =>
      screen.getByRole('button', {name: /submit/i}),
    )
    // good
    const submitButton = await screen.findByRole('button', {name: /submit/i})
    

    应该使用哪个查询

    根据指导原则,你的测试应该尽可能的类似于用户使用你的页面或组件。下面是推荐的有限顺序:

    1. Queries Accessible to Everyone(每个人都可以访问的查询)
    • getByRole
    • getByLabelText
    • getByPlaceholderText
    • getByText
    • getByDisplayValue

    2.Semantic Queries(语义查询)

    • getByAltText
    • getByTitle

    3.Test IDs

    • getByTestId

    注意

    虽然也可以使用querySelector DOM API进行查询,但是这是极其不推荐的做法,因为用户是看不到这些属性的。如果你不得不这样做的话,可以给它添加testid,像下面这样

    // @testing-library/react
    const { container } = render(<MyComponent />)
    const foo = container.querySelector('[data-foo="bar"]')
    

    有用的浏览器扩展

    扩展工具Testing Playground可以帮助你找到最合适查询方式

    Reference

    react-testing-library

    Which query should I use

    Appearance and Disappearance

    Considerations for fireEvent

    Test for React Router

    Test for React redux

    Common mistakes with React Testing Library

    How to use React Testing Library Tutorial


    起源地下载网 » React Testing Library使用总结

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元