使用 saga 对组件进行 React 测试

Posted

技术标签:

【中文标题】使用 saga 对组件进行 React 测试【英文标题】:React test a component with saga 【发布时间】:2021-09-17 13:25:10 【问题描述】:

大家好,我在测试我的组件时遇到了一些问题

问题是我想测试我的 React Native 组件,它使用 saga 从服务器获取数据。

问题是我确实知道我应该做什么,我想我应该在我的测试文件中模拟我的 API 调用,但我不知道如何:/

组件文件非常简单,安装后调度操作以获取车辆列表,然后在 UI 中显示它们。在获取之前,它会显示正在加载文本

下面是我当前的组件设置和测试文件。

这是一个在屏幕加载时获取初始数据的屏幕组件

屏幕组件

import React,  useContext, useEffect, useState  from 'react';
import  Platform, FlatList, View, ActivityIndicator, Text  from 'react-native';
import PropTypes from 'prop-types';

import  useDispatch, useSelector  from 'react-redux';
import  vehiclesActions  from '_store/vehicles';


export const MainScreen = ( navigation ) => 


  /**
   * Redux selectors and dispatch
   */
  const 
    loading = true,
    vehicles = [],
    loadMore = false
   = useSelector((state) => state.vehicles);


  /**
 * Initial effect, fetches all vehicles
 */
  useEffect(() => 
    dispatch(
      vehiclesActions.vehicleGet(
        page: 1,
      )
    );
  , []);

  const renderCard = () => 
    return (<View><Text>Test</Text></View>)
  


 if (loading) 
     return (<View><Text>App Loading </Text></View>
  

  return (
    <View style=styles.wrapper>
      <View
        style=
          Platform.OS === 'ios' ?  marginTop: 30  :  marginTop: 0, flex: 1 
        
      >
        !loading && (
          <View style=Platform.OS === 'ios' ?  :  flex: 1 >
            <FlatList
              testID='flat-list'
              data=vehicles
              renderItem=renderCard
            />
          </View>
        )
      </View>
    </View>
  );
;

MainScreen.propTypes = 
  navigation: PropTypes.object
;

export default MainScreen;

我的车辆传奇:

const api = 
  vehicles: 
    getVehicles: (page) => 
      return api.get(`/vehicles/list?page=$page`, );
    ,

function* getVehicles(action) 
  try 
    const  page  = action.payload;
    const  data  = yield call(api.vehicles.getVehicles, page);
    yield put( type: vehiclesConstants.VEHICLE_GET_SUCCESS, payload: data );

   catch (err) 
    yield call(errorHandler, err);
    yield put( type: vehiclesConstants.VEHICLE_GET_FAIL );
  


export function* vehiclesSaga() 
  yield takeLatest(vehiclesConstants.VEHICLE_GET_REQUEST, getVehicles);

操作:

export const vehiclesActions = 
  vehicleGet: payload => ( type: vehiclesConstants.VEHICLE_GET_REQUEST, payload ),
  vehicleGetSuccess: payload => ( type: vehiclesConstants.VEHICLE_GET_SUCCESS, payload ),
  vehicleGetFail: error => ( type: vehiclesConstants.VEHICLE_GET_FAIL, error ),

减速器

import  vehiclesConstants  from "./constants";

const initialState = 
  vehicles: [],
  loading: true,
;

export const vehiclesReducer = (state = initialState, action) => 
  switch (action.type) 
    case vehiclesConstants.VEHICLE_GET_REQUEST:
       return 
        ...state,
        loading: true,
      ;
    case vehiclesConstants.VEHICLE_GET_SUCCESS:
      return 
        ...state,
        loading: false,
        vehicles: action.payload,
      ;
  

我的测试文件

import 'react-native';
import React from 'react';

import cleanup, render, fireEvent from '@testing-library/react-native';

import AppScreen from '../../../../src/screens/App/index';

import Provider from 'react-redux';
import store from '../../../../src/store/configureStore';

describe('App List Component', () => 
  beforeEach(() => jest.useFakeTimers());
  afterEach(cleanup);

  it('should render vehicle list page title', async () => 

    const navigation = 
      setParams: () => ,
      navigate: jest.fn(),
    ;

    const route = 

    
    const component = (
      <Provider store=store>
        <AppScreen route=route navigation=navigation />
      </Provider>);

    const getByText, getByTestId = render(component);
    const pageTitle = await getByText('App Loading'); // this works fine
    expect(pageTitle).toBeDefined();
  );

  it('should navigate to add vehicle', async () => 

    const navigation = 
      setParams: () => ,
      navigate: jest.fn(),
    ;

    const route = 

    
    const component = (
      <Provider store=store>
        <AppScreen route=route navigation=navigation />
      </Provider>);

    const getByText, getByTestId = render(component);
    const flatList = await getByTestId('flat-list');// this throws error since flat list is still not shown, and loading is showing instead
  );

就像我在上面看到的那样,我找不到带有 testId flat-list 的元素,因为组件 AppScreen 它总是显示加载文本,有什么方法可以模拟该 API 调用并让它工作?

【问题讨论】:

你用什么测试?我有使用 cypress 的经验,它拥有用于此类端到端测试的所有工具。 如果您可以在 web 模式下使用您的应用程序,您可以使用 cypress,否则请查看此***.com/q/57768294/11218031 @AvinashThakur 感谢您的回答,我正在使用 jest 和 react-native-testing 工具我只需要一种模拟 axios 调用的方法 感谢您的信息。也可以开玩笑地模拟模块。 @AvinashThakur 我只想从模块中模拟一个函数,这可能吗? 【参考方案1】:

Jest 允许您使用 jest.mock 模拟任何模块。

你必须像这样写axios.get的替代品


const vehiclesData = [
// ... put default data here
]

const delay = (ms, value) =>
  new Promise(res => setTimeout(() => res(value), ms))

const mockAxiosGet = async (path) => 
  let result = null
  if (path.includes('vehicles/list') 
    const query = new URLSearchParams(path.replace(/^[^?]+\?/, ''))
    const page = + query.get('page')
    const pageSize = 10
    const offset = (page - 1)*pageSize
    result = vehiclesData.slice(offset, offset + pageSize)
  
  return delay(
    // simulate 100-500ms latency
    Math.floor(100 + Math.random()*400),
     data: result 
  )

然后将测试文件修改为

import 'react-native';
import React from 'react';

import cleanup, render, fireEvent from '@testing-library/react-native';
import axios from 'axios'

// enable jest mock on 'axios' module
jest.mock('axios')

import AppScreen from '../../../../src/screens/App/index';

import Provider from 'react-redux';
import store from '../../../../src/store/configureStore';

describe('App List Component', () => 
  before(() => 
    // mock axios implementation
    axios.get.mockImplementation(mockAxiosGet)
  )
  beforeEach(() => jest.useFakeTimers());
  afterEach(cleanup);

  it('should render vehicle list page title', async () => 

    const navigation = 
      setParams: () => ,
      navigate: jest.fn(),
    ;

    const route = 

    
    const component = (
      <Provider store=store>
        <AppScreen route=route navigation=navigation />
      </Provider>);

    const getByText, getByTestId = render(component);
    const pageTitle = await getByText('App Loading'); // this works fine
    expect(pageTitle).toBeDefined();
  );

  it('should navigate to add vehicle', async () => 

    const navigation = 
      setParams: () => ,
      navigate: jest.fn(),
    ;

    const route = 

    
    const component = (
      <Provider store=store>
        <AppScreen route=route navigation=navigation />
      </Provider>);

    const getByText, getByTestId = render(component);
    const flatList = await getByTestId('flat-list');// this throws error since flat list is still not shown, and loading is showing instead
  );

对于您的用例,请在Mocking Implementations阅读更多内容

【讨论】:

以上是关于使用 saga 对组件进行 React 测试的主要内容,如果未能解决你的问题,请参考以下文章

如何用 jest 测试 redux saga?

使用带有表单输入元素的 ReactBootstrap 模式对 React 组件进行单元测试

使用上下文消费者对 React 组件进行单元测试

如何对 React-Redux 连接组件进行单元测试?

React 测试库 + Jest:如何测试接收数字的组件然后对其进行格式化

如何对 fetch 完成后呈现的 React 组件进行单元测试?