单元测试实例

action 构造函数测试

  • 测试actions.js
    import {ADD_TODO, TOGGLE_TODO, REMOVE_TODO} from './actionTypes.js';
    
    let nextTodoId = 10;
    
    export const addTodo = (text) => ({
      type: ADD_TODO,
      completed: false,
      id: nextTodoId ++,
      text: text
    });
    
    export const toggleTodo = (id) => ({
      type: TOGGLE_TODO,
      id: id
    });
    
    export const removeTodo = (id) => ({
      type: REMOVE_TODO,
      id: id
    });
    
  • actions.test.js
    import * as actions from '../../src/todos/actions.js';
    import * as actionTypes from '../../src/todos/actionTypes.js';
    
    /**
     *  测试目标是todos功能模块
     *  let nextTodoId = 10;
        export const addTodo = (text) => ({
          type: ADD_TODO,
          completed: false,
          id: nextTodoId ++,
          text: text
        });
        单元测试的基本套路:
          预设参数
          调用纯函数
          用expect验证纯函数的返回结果
     */
    describe('todos/actions', () => {
      describe('addTodo', () => {
        const addTodo = actions.addTodo
    
        it('should create an action to add todo', () => {
          const text = 'first todo';
          const action = addTodo(text);
    
          expect(action.text).toBe(text);
          expect(action.completed).toBe(false);
          expect(action.type).toBe(actionTypes.ADD_TODO);
        });
    
        it('should have different id for different actions', () => {
          const text = 'first todo';
          const action1 = addTodo(text);
          const action2 = addTodo(text);
    
          expect(action1.id !== action2.id).toBe(true);
        });
      });
    });
    

异步action构造函数测试

  • action.js
    import {FETCH_STARTED, FETCH_SUCCESS, FETCH_FAILURE} from './actionTypes.js';
    
    // 一个访问服务器的action,至少要涉及三个action类型
    
    // 表示异步操作已经开始的action类型
    export const fetchWeatherStarted = () => ({
      type: FETCH_STARTED
    });
    
    // 表示异步操作成功的action类型
    export const fetchWeatherSuccess = (result) => ({
      type: FETCH_SUCCESS,
      result
    })
    
    // 表示异步操作失败的action类型
    export const fetchWeatherFailure = (error) => ({
      type: FETCH_FAILURE,
      error
    })
    
    // 异步action构造函数fetchWeather
    export const fetchWeather = (cityCode) => {
      return (dispatch) => {
        const apiUrl = `/data/cityinfo/${cityCode}.html`;
    
        // 同步派发这个action对象的目的是将视图置于“有异步action还未结束”的状态
        dispatch(fetchWeatherStarted())
    
        return fetch(apiUrl).then((response) => {
          // 访问服务器的异步action,最后无论成败,都要通过派发action对象改变Redux Store上的状态完结
          if (response.status !== 200) {
            throw new Error('Fail to get response with status ' + response.status);
          }
    
          response.json().then((responseJson) => {
            dispatch(fetchWeatherSuccess(responseJson.weatherinfo));
          }).catch((error) => {
            dispatch(fetchWeatherFailure(error));
          });
        }).catch((error) => {
          dispatch(fetchWeatherFailure(error));
        })
      };
    }
    
  • actions.test.js
    import thunk from 'redux-thunk';
    import {stub} from 'sinon';
    import configureStore from 'redux-mock-store';
    
    import * as actions from '../../src/weather/actions.js';
    import * as actionTypes from '../../src/weather/actionTypes.js';
    
    const middlewares = [thunk];
    // 在测试用例中我们会用这个函数而不是redux提供的createStore函数
    // 因为在单元测试环境下,dispatch函数最好不要做实际的派发动作,只要能够把被派发的对象记录下来,留在验证阶段读取就可以了
    // 注意createMockStore可以使用Redux中间件,添加了redux-thunk之后可以处理异步action对象
    const createMockStore = configureStore(middlewares);
    
    
    describe('weather/actions', () => {
      describe('fetchWeather', () => {
        // 改变fetch函数的行为
        let stubbedFetch;
        // 创建MockStore
        const store = createMockStore();
    
        // 在beforeEach中创造stub对象
        beforeEach(() => {
          // stub第一个参数是一个对象,第二个参数是这个函数的字符串名,返回一个stub对象,通过这个stub上的对象可以指定被“篡改”函数的行为
          stubbedFetch = stub(global, 'fetch');
        });
    
        // 在afterEach函数中用stub对象的restore方法恢复被“篡改”函数原本的行为
        afterEach(() => {
          stubbedFetch.restore();
        });
    
        // 虽然mockResponse是通过Promise.resolve函数产生的“创造即已经完结的”Promise对象,但是其then指定的函数依然要等到Node.js的下一个时钟周期才执行,所以也不能在fetchWeather函数执行完之后就认为异步操作就已经完结
        it('should dispatch fetchWeatherSuccess action type on fetch success', () => {
          // 就是当如果要执行fetch函数返回结果的时候,返回下面的,而不是实际fetch返回的值
          const mockResponse= Promise.resolve({
            status: 200,
            json: () => Promise.resolve({
              weatherinfo: {}
            })
          });
    
          // stubbedFetch规定fetch函数被调用时返回一个指定的mockReposonse
          // fetchWeather函数中的fetch函数行为就完全被操纵,只需要让fetch函数返回我们想要的结果就行
          stubbedFetch.returns(mockResponse);
          
          // fetchWeather的测试用例返回的并不是store.dispatch函数返回的那个Promise对象,而是经过then函数产生的一个新的Promise对象,所以当Jest获取的Promise对象被认为是完结时,在then函数中的所有断言语句绝对已经执行完毕了
          return store.dispatch(actions.fetchWeather(1)).then(() => {
            // 断言部分我们使用了 redux-mock-store 所创造的 Store 的 getActions函数
            // 能够帮助我们读取到所有派发到Store上的actions
            const dispatchedActions = store.getActions();
            expect(dispatchedActions.length).toBe(2);
            expect(dispatchedActions[0].type).toBe(actionTypes.FETCH_STARTED);
            expect(dispatchedActions[1].type).toBe(actionTypes.FETCH_SUCCESS);
          });
        });
    
      });
    });
    
    /**
     * 在Jest中测试异步函数有两种方法,一种是代表测试用例的函数增加一个参数,习惯上这个参数叫做done, Jest发现这个参数存在就会认为这个参数是一个回调函数,只有这个回调函数被执行才算是测试用例结束
     * if('should timeout', (done) => {
     * });
     * 
     * 让测试用例函数返回一个Promise对象,这样也等于告诉Jest这个测试用例是一个异步过程,只有当返回的Promise对象完结的时候,这个测试用例才算结束。
     * fetchWeather的测试用例返回的并不是store.dispatch函数返回的那个Promise对象,而是经过then函数产生的一个新的Promise对象,所以当Jest获取的Promise对象被认为是完结时,在then函数中的所有断言语句绝对已经执行完毕了
     */
    

reducer测试

  • reducer是纯函数,所以测试很简单
import * as actions from '../../src/weather/actions.js';
import * as actionTypes from '../../src/weather/actionTypes.js';
import * as Status from '../../src/weather/status.js';
import reducer from '../../src/weather/reducer.js';

/**
 *  actions.js:
 *  export const fetchWeatherStarted = () => ({
      type: FETCH_STARTED
    });

    reducer.js:
    export default (state = {status: Status.LOADING}, action) => {
      switch(action.type) {
        case FETCH_STARTED: {
          return {status: Status.LOADING};
        }
        case FETCH_SUCCESS: {
          return {...state, status: Status.SUCCESS, ...action.result};
        }
        case FETCH_FAILURE: {
          return {status: Status.FAILURE};
        }
        default: {
          return state;
        }
      }
    }
 */
describe('weather/reducer', () => {
  it('should return loading status', () => {
    const action = actions.fetchWeatherStarted();

    const newState = reducer({}, action);

    expect(newState.status).toBe(Status.LOADING);
  });
});

无状态React组件测试

  • 对于一个无状态的React组件,可以使用Enzyme的shallow方法来渲染,因为shallow方法只渲染一层,所以不会牵涉子组件的React组件渲染
  • filter.js
    import React from 'react';
    import Link from './link.js';
    import {FilterTypes} from '../../constants.js'
    
    import './style.css';
    
    const Filters = () => {
      return (
        <p className="filters">
          <Link filter={FilterTypes.ALL}> {FilterTypes.ALL} </Link>
          <Link filter={FilterTypes.COMPLETED}> {FilterTypes.COMPLETED} </Link>
          <Link filter={FilterTypes.UNCOMPLETED}> {FilterTypes.UNCOMPLETED} </Link>
        </p>
      );
    };
    
    export default Filters;
    
  • filters.test.js
    import React from 'react';
    import {shallow} from 'enzyme';
    import Filters from '../../../src/filter/views/filters.js';
    import Link from '../../../src/filter/views/link.js';
    import {FilterTypes} from '../../../src/constants.js';
    
    describe('filters', () => {
      it('should render three link', () => {
        // 把Enzyme函数渲染的结果命名为wrapper,对wrapper可以使用contains函数判断是否包含某个子组件
        // shallow并没有渲染产生子组件Link的DOM元素,所以完全可以用contains来判断是否包含Link组件
        // 这种单元测试不深入渲染React子组件,主要的意义是可以简化测试过程,因为React子组件的完全渲染可能引入其他的依赖关系
        // 使用shallow浅层渲染,只要在渲染过程中知道创造了Child组件,传递给Child的prop都对,这就足够了,至于Child功能是否正确,那就交给Child的单元测试去验证,不是Parent单元测试的责任
        const wrapper = shallow(<Filters />);
    
        expect(wrapper.contains(<Link filter={FilterTypes.ALL}> {FilterTypes.ALL} </Link>)).toBe(true);
        expect(wrapper.contains(<Link filter={FilterTypes.COMPLETED}> {FilterTypes.COMPLETED} </Link>)).toBe(true);
        expect(wrapper.contains(<Link filter={FilterTypes.UNCOMPLETED}> {FilterTypes.UNCOMPLETED} </Link>)).toBe(true);
      });
    });
    

被链接的React组件测试

  • 所有有状态的React组件都是通过connect函数产生的组件,被称为“被连接的组件”
    import React from 'react';
    import {mount} from 'enzyme';
    import {createStore, combineReducers} from 'redux';
    import {Provider} from 'react-redux';
    
    import {reducer as todosReducer, actions} from '../../../src/todos/index.js';
    import {reducer as filterReducer} from '../../../src/filter/index.js';
    import {FilterTypes} from '../../../src/constants.js';
    import TodoList from '../../../src/todos/views/todoList.js';
    import TodoItem from '../../../src/todos/views/todoItem.js';
    
    describe('todos', () => {
      it('should add new todo-item on addTodo action', () => {
        // 因为TodoList组件依赖于Redux Store,所以创建一个store
        const store = createStore(
          combineReducers({
            todos: todosReducer,
            filter: filterReducer
          }), {
            todos: [],
            filter: FilterTypes.ALL
          });
        
        // 为了将这个Store放在React Context,还需要创造Provider
        /**
         * <TodoItem
              key={item.id}
              id={item.id}
              text={item.text}
              completed={item.completed}
            />
            ))
         */
        const subject = (
          <Provider store={store}>
            <TodoList />
          </Provider>
        );
        // 使用Enzyme的mount方法渲染的是Provider包裹起来的TodoList组件
        const wrapper = mount(subject);
    
        store.dispatch(actions.addTodo('write more test'));
    
        expect(wrapper.find('.text').text()).toEqual('write more test');
      });
    });
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值