单元测试


思考测试

  1. 什么是单元测试?什么事集成测试
    如果一个测试,需要用到真实的系统时间,真实的文件系统,或者真实的数据库,那么这个测试就进入了集成测试的范围。我们开发常说的测试属于单元测试的范畴
  2. 测试的作用是什么
    • 保证项目的稳定
    • 提升代码的质量(一个好的代码,一定是可测试的)
  3. 应该测试什么
    • 只测试输入和输出的正确性,不测试代码实现
    • 只测试自己的代码,不测试三方库,或者引入的其他代码(三方库、其他代码的稳定应该由各自自己保证)
    • 测试交互
    • 测试结果分支
    • 测试渲染结果
  4. 一个UI测试框架应该具备哪些能力
    • 渲染页面 render(显示正确)
    • 事件触发 event(交互正确)
    • mock操作,callback,timer、fetch、fs 等,因为这些东西属于三方提供的东西,不应该由自己的代码保证正确
    • 其他
      • 断言
      • 易编写
      • 良好的错误提示等

demo

import React, { useState, useEffect } from 'react'
import { Radio, Button } from 'antd'
import { RadioChangeEvent } from 'antd/lib/radio';

export default function Calc(props: { id?: number }) {
  const [value, setValue] = useState<'increase' | 'decrease'>('increase');
  const [count, setCount] = useState(0);
  const [userName, setUserName] = useState('')

  function handleChange(e: RadioChangeEvent) {
    setValue(e.target.value)
  }

  function handleClick () {
    setCount(value === 'increase' ? count + 1 : count - 1)
  }

  async function fetchUserData(userId: number) {
    const response = await fetch("/" + userId);
    setUserName(await response.json());
  }

  useEffect(() => {
    fetchUserData(props.id!);
  }, [props.id]);

  return (
    <div>
      <div id="user-name">copy right {userName}</div>
      {value === 'increase' ? <div>每次加一</div> : <div>每次减一</div>}
      <Radio.Group id="test" onChange={handleChange} value={value}>
        <Radio value={1}>+</Radio>
        <Radio value={2}>-</Radio>
      </Radio.Group>
      <div>
        <span>{count}</span>
        <Button onClick={handleClick}>modify</Button>
      </div>
    </div>
  )
}

需要测试的东西

  1. 能正常渲染
  2. value === ‘increase’ 分支
  3. radio onChange 触发逻辑
  4. Button onclick 触发逻辑
  5. username 正常渲染(根据ajax返回结果渲染)
import React from 'react'
import { render, act } from '@testing-library/react'
import Calc from './index'

beforeAll(() => {
  const fakerUser = 'enochjs'
  jest.spyOn(global, 'fetch').mockImplementation(() => {
      return Promise.resolve({
          json: () => Promise.resolve(fakerUser)
      }) as any
  })
})

describe.only('test calc', () => {

  test('render right', async () => {
    let container: any
    await act(async () => {
      container = render(<Calc />)
    })
    expect(container.queryByText('每次加一')).toBeInTheDocument()
    expect(container.queryByText('0')).toBeInTheDocument()
  })

  test('radio change', async () => {
    let container: any
    await act(async () => {
      container = render(<Calc />)
    })
    const radios = document.querySelectorAll('input')
    act(() => {
      radios[1].dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });
    expect(container.queryByText('每次减一')).toBeInTheDocument()
  })

  test('count change', async () => {
    let container: any
    await act(async () => {
      container = render(<Calc />)
    })
    const button = container.queryByText('modify')
    act(() => {
      button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });
    expect(container.queryByText('1')).toBeInTheDocument()

    const radios = document.querySelectorAll('input')
    act(() => {
      radios[1].dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });

    act(() => {
      button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });
    expect(container.queryByText('0')).toBeInTheDocument()
  })

  test('render with fetch data', async () => {
    let container: any
    await act(async () => {
      container = render(<Calc id={123} />)
    })
    expect(container.queryByText("copy right enochjs")).toBeInTheDocument();
  })

})

__mock__ 文件夹

__mock__ 文件需要放在 request的同级目录下

  // request.js
  const http = require('http');

  export default function request(url) {
    return new Promise(resolve => {
      // This is an example of an http request, for example to fetch
      // user data from an API.
      // This module is being mocked in __mocks__/request.js
      http.get({path: url}, response => {
        let data = '';
        response.on('data', _data => (data += _data));
        response.on('end', () => resolve(data));
      });
    });
  }

  // __mocks__/request.js
  const users = {
      4: {name: 'Mark'},
      5: {name: 'Paul'},
    };

  export default function request(url) {
    return new Promise((resolve, reject) => {
      const userID = parseInt(url.substr('/users/'.length), 10);
      process.nextTick(() =>
        users[userID]
          ? resolve(users[userID])
          : reject({
              error: 'User with ' + userID + ' not found.',
            }),
      );
    });
  }
  // user.js
  import request from './request';

  export function getUserName(userID) {
    return request('/users/' + userID).then(user => user.name);
  }

  // test
  jest.mock('./request');

  import * as user from './user';

  // The assertion for a promise must be returned.
  it('works with promises', () => {
    expect.assertions(1);
    return user.getUserName(4).then(data => expect(data).toEqual('Mark'));
  });

  // Testing for async errors using Promise.catch.
  it('tests error with promises', () => {
    expect.assertions(1);
    return user.getUserName(2).catch(e =>
      expect(e).toEqual({
        error: 'User with 2 not found.',
      }),
    );
  });

  // Or using async/await.
  it('tests error with async/await', async () => {
    expect.assertions(1);
    try {
      await user.getUserName(1);
    } catch (e) {
      expect(e).toEqual({
        error: 'User with 1 not found.',
      });
    }
  });

  // Testing for async errors using `.rejects`.
  it('tests error with rejects', () => {
    expect.assertions(1);
    return expect(user.getUserName(3)).rejects.toEqual({
      error: 'User with 3 not found.',
    });
  });

  // Or using async/await with `.rejects`.
  it('tests error with async/await and rejects', async () => {
    expect.assertions(1);
    await expect(user.getUserName(3)).rejects.toEqual({
      error: 'User with 3 not found.',
    });
  });

mockTimer

  // timeGame
  export default function infiniteTimerGame(callback) {
    console.log('Ready....go!');
    setTimeout(() => {
      console.log("Time's up! 10 seconds before the next game starts...");
      callback && callback();
      // Schedule the next game in 10 seconds
      setTimeout(() => {
        infiniteTimerGame(callback);
      }, 10000);
    }, 1000);
  }
  // test
  import infiniteTimerGame from './timerGame'

  jest.useFakeTimers();

  describe('infiniteTimerGame', () => {
    test('schedules a 10-second timer after 1 second', () => {
    //   const infiniteTimerGame = require('../infiniteTimerGame');
      const callback = jest.fn();

      infiniteTimerGame(callback);

      // At this point in time, there should have been a single call to
      // setTimeout to schedule the end of the game in 1 second.
      expect(setTimeout).toHaveBeenCalledTimes(1);
      expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);

      // Fast forward and exhaust only currently pending timers
      // (but not any new timers that get created during that process)
      jest.runOnlyPendingTimers();

      // At this point, our 1-second timer should have fired it's callback
      expect(callback).toBeCalled();

      // And it should have created a new timer to start the game over in
      // 10 seconds
      expect(setTimeout).toHaveBeenCalledTimes(2);
      expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000);
    });
  });

更多知识请看API

参考文档


文章作者: enochjs
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 enochjs !
 上一篇
jest-api jest-api
global beforeAll(fn, timeout) afterAll(fn, timeout) beforeEach(fn, timeout) afterEach(fn, timeout) describe describ
2020-08-25
本篇 
单元测试 单元测试
思考测试 什么是单元测试?什么事集成测试 如果一个测试,需要用到真实的系统时间,真实的文件系统,或者真实的数据库,那么这个测试就进入了集成测试的范围。我们开发常说的测试属于单元测试的范畴 测试的作用是什么 保证项目的稳定 提升代码的质量(一
2020-08-21
  目录