TypeScript + Jest + React-Testing-Library

纯Typescript测试相关配置

1: 安装jest相关的依赖
yarn add --dev jest @types/jest ts-jest

jest -- 我们的test runner, 测试断言库, mock库
@types/jest -- Jest的Typescript版
ts-jest -- 把Typescript编译为JavaScript

2:创建jest.config.js

在项目根目录下创建一个jest.config.js文件作为jest的配置文件,且添加以下内容

// jest.config.js
module.exports = {
    roots: ['<rootDir>/src'],
    transform: {
        '^.+\\.tsx?$': 'ts-jest',
    },
    testRegex: '^.+\\.test\\.(ts|tsx)$',
    moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};
3: 添加跑测试的脚本
// package.json
"scripts": {
    "test": "jest",
      //....
  },

完成以上相关配置,我们就可以对一个Typescript纯函数进行测试了,例如以下例子:

//stringProcessor.ts
function reverseString(str: string) {
    return str.split('').reverse().join('');
}
export {reverseString}
//stringProcessor.test.ts
import {reverseString} from './stringProcessor';
test('', ()=> {
    expect(reverseString('hello')).toBe('olleh');
});

但是,如果我们需要测一个React component测试,我们需要借助另一个库Enzyme以及相关其他依赖。接下来,让我们来完成这部分的配置:

React测试相关配置

安装React测试相关的依赖
yarn add  --dev @testing-library/react @testing-library/react-hooks @testing-library/jest-dom

@testing-library/react 测试React Component的库
@testing-library/react-hooks 测试自己写的的React Hooks的库
@testing-library/jest-dom 提供更多利于dom测试的断言

往jest.config.js文件里面添加支持React component测试的相关配置
//jest.config.js
module.exports = {
    roots: ['<rootDir>/src'],
    setupFiles: ['<rootDir>/test.setup.js', '<rootDir>/test.shim.js'],
    transform: {
        '^.+\\.tsx?$': 'ts-jest',
    },
 
    moduleNameMapper: {
        '\\.(css|scss)': 'identity-obj-proxy', // mock 在react组件里import的CSS
        '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
            '<rootDir>/testMocks/assetsMocks.js', //mock 在react组件里import的图片
    },
    testRegex: '^.+\\.test\\.(ts|tsx)$',
    moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};
在项目根目录添加testMocks/assetsMocks.js
//testMocks/assetsMocks.js
module.exports = '';

测试一个最简单的React组件

组件文件counter.tsx

//counter.tsx
import * as React from 'react';
import { useState } from 'react';

const Counter = () => {
    const [count, setCount] = useState(0);

    return (
        <div>
            <div data-testid='count-announcement'> you have been clicked {count} times</div>
            <button
                data-testid='increase-button'
                onClick={() => {
                    setCount(count + 1);
                }}
            >
                {' '}
                increase count
            </button>
        </div>
    );
};
export default Counter;

测试文件counter.test.tsx

//counter.test.tsx
import * as React from 'react';
import Counter from './counter';
import { render, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';

test('the count should be 1 when you click the increase button once', () => {
    // render
    const { getByTestId } = render(<Counter />);
    const increaseButton = getByTestId('increase-button');
    // act
    fireEvent.click(increaseButton);
    // assert
    expect(getByTestId('count-announcement')).toHaveTextContent('1');
});

测试一个有<Link>的React组件

组件文件HomePage.tsx

//HomePage.tsx
import React from 'react';
import logo from './images/logo.svg';
import pink from './images/pink.png';
import './HomePage.scss';
import { Link } from 'react-router-dom';

function App() {
    const name = `nana`;
    return (
        <div className='App'>
            <header className='App-header'>
                <img src={logo} className='App-logo' alt='logo' />
                <img src={pink} className='' alt='pink' />
                <Link to='/blogList'>Blog List Page</Link>
            </header>
            <p data-testid='name'>name: {name}</p>
        </div>
    );
}

export default App;

测试文件HomePage.test.tsx

//HomePage.test.tsx
import * as React from 'react';
import HomePage from './HomePage';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { BrowserRouter } from 'react-router-dom';

//This is testing demo to testing a react component with CSS and Images import
test('should render name correctly', () => {
    // render
    const { getByTestId } = render(
        //对于有Link的组件,在测试的时候,必须把它包到BrowserRouter里面
        <BrowserRouter>
            <HomePage />
        </BrowserRouter>,
    );
    const name = getByTestId('name');
    // act
    // assert
    expect(name).toHaveTextContent('nana');
});

这里需要特别注意的是: 对于有Link的组件,在测试的时候,必须把它包到BrowserRouter里面,不然JEST会不过。

测试一个有axios请求的组件

组件文件profile.tsx

//profile.tsx
import * as React from 'react';
import axios from 'axios';
import { useEffect, useState } from 'react';

const Profile = () => {
    const [isLoading, setIsLoading] = useState(true);
    const [username, setUsername] = useState('');
    const [errorMessage, setErrorMessage] = useState('');

    useEffect(() => {
        async function fetchData() {
            try {
                const response = await axios.get('https://my-json-server.typicode.com/pengmq/mock-server/profile', {});
                setIsLoading(false);
                setUsername(response.data.data.username);
            } catch (error) {
                setIsLoading(false);
                setErrorMessage(error);
            }
        }

        fetchData();
    }, []);

    return (
        <div>
            <div data-testid='error-message'>{errorMessage}</div>
            <div data-testid='loading-text'>
                <span>{isLoading ? 'loading....' : ''}</span>
            </div>
            <div data-testid='username'>{username}</div>
        </div>
    );
};
export default Profile;

测试文件profile.test.tsx

//profile.test.tsx
import * as React from 'react';
import { render, waitForElement } from '@testing-library/react';
import Profile from './profle';
import axios from 'axios';

//jest.mock(...)自动mock axios的API调用,这行代码必须放在文件最外层,不能放到test里面
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>; //This line is needed when work with typescript

test('loading text should hide and user name should show after get profile data successfully from server ', async () => {

    //mock axios的API返回,这行代码必须在render之前
    mockedAxios.get.mockResolvedValueOnce({ data: { data: { username: 'nana' } } });
    const { getByTestId, container } = render(<Profile />);
    // awaiting for sync function to be done with  await waitForElement()
    const [loadingText, username] = await waitForElement(() => [getByTestId('loading-text'), getByTestId('username')], {
        container,
    });
    //assert dom changes
    expect(loadingText).toHaveTextContent('');
    expect(username).toHaveTextContent('nana');
});

推荐阅读更多精彩内容