如何在react / redux应用程序中对promise进行单元测试

时间:2017-10-17 13:14:34

标签: javascript unit-testing promise react-redux chai

查看react-redux网站上的高级示例,使用fetchPostsmochachaisinon函数进行单元测试的最佳方法是什么? ?它使用了一个承诺链,所以鉴于此,我查看了chai-as-promised

promises链与最后一个解析为空值并调度操作以更新redux状态。

应用代码:

export function fetchPosts(subreddit) {   
    return dispatch => {
      dispatch(requestPosts(subreddit))
      return fetch(`https://www.reddit.com/r/${subreddit}.json`)
        .then(response => response.json())
        .then(json => dispatch(receivePosts(subreddit, json)))   
    } 
}

//other functions

export function requestPosts(subreddit){
    return {
        type:REQUEST_POSTS,
        subreddit
    }
}

export function receivePosts(subreddit,json){
    return {
        type:RECEIVE_POSTS,
        subreddit,
        posts:json.data.children.map(child => child.data),
        receivedAt:Date.now()
    } 
}

我尝试进行单元测试:

describe('fetchPosts action creator', () => {
    it('should have one parameter', () => {
        expect(fetchPosts.length).to.equal(1);
    });

    it('should return a function holding one parameter', () => {
        let subreddit = 'frontend';
        expect(fetchPosts(subreddit).length).to.equal(1);
    });

   it('should execute the dispatch funcion with requestPosts' ,() =>{
       let spy = spy(dispatch);
       let subreddit = 'frontend';
       expect(spy.calledWith(fetchPosts(subreddit))).to.be.calledOnce;
       expect(spy.calledWith(receivePosts(subreddit,json))).to.be.calledOnce;
   })
})

我的想法是

  • 测试fetchPosts()可以点击reddit api
  • 检查回复是否有效json
  • 使用dispatchrequestPosts
  • 检查receivePosts被调用两次

1 个答案:

答案 0 :(得分:0)

您可以关注 Asynchronous actions 个包中的 redux-mock-store 个。

<块引用>

用于测试 Redux 异步操作创建者和中间件的模拟商店。模拟存储将创建一个调度操作数组,用作测试的操作日志。

此外,您应该存根 STOCK_DIM = 422 # INITIAL_BALANCE, Amount_of_shares, 60 days * (Norm_close, 1m/2m/3m/1y ret, macd,rsi) INITIAL_ACCOUNT_BALANCE = 1000000 TRANSACTION_FEE_PERCENT = 0.01 REWARD_SCALING = 1e-5 HMAX_NORMALIZE = 100 N = config.PREV_DATA_POINTS class StockEnv_Train(gym.Env): def __init__(self,df,day = 374): # day set to 260 since we need to account for the annual returns that only start around 252 days, # macd only starts from 254 # we add 63 days to this to account for the historical data self.day = day self.df = df self.action_space = spaces.Box(low = -1,high = 1,shape =(1,) ) self.observation_space = spaces.Box(low = 0, high = np.inf, shape = (STOCK_DIM,)) self.terminal = False self.state = [INITIAL_ACCOUNT_BALANCE] + [0] + self.df.iloc[self.day-N:self.day,1].tolist() + \ self.df.iloc[self.day-N:self.day,2].tolist() + self.df.iloc[self.day-N:self.day,3].tolist() + \ self.df.iloc[self.day-N:self.day,4].tolist() + self.df.iloc[self.day-N:self.day,5].tolist() + \ self.df.iloc[self.day-N:self.day,6].tolist() + self.df.iloc[self.day-N:self.day,7].tolist() self.reward = 0 self.asset_memory = [INITIAL_ACCOUNT_BALANCE] self.cost = 0 self.rewards_memory = [] self.trades = 0 self._seed() def _sell_stock(self,action): if self.state[1] > 0: self.state[0] += (self.df.iloc[self.day,-1]*min(abs(action),self.state[1]) * \ (1- TRANSACTION_FEE_PERCENT)).item() self.cost +=self.df.iloc[self.day,-1]*min(abs(action),self.state[1]) * \ TRANSACTION_FEE_PERCENT self.state[1] -= (min(abs(action), self.state[1])) self.trades+=1 else: pass def _buy_stock(self,action): # perform buy action based on the sign of the action available_amount = self.state[0] // self.df.iloc[self.day,-1] # print('available_amount:{}'.format(available_amount)) #update balance self.state[0] -= (self.df.iloc[self.day,-1]*min(available_amount, action)* \ (1+ TRANSACTION_FEE_PERCENT)).item() self.state[1] += (min(available_amount, action)) self.cost+=self.df.iloc[self.day,-1]*min(available_amount, action)* \ TRANSACTION_FEE_PERCENT self.trades+=1 def step(self,action): self.terminal = self.day >= len(self.df.Date.unique()) - 1 if self.terminal: plt.plot(self.asset_memory,'r') plt.savefig('results/account_value_train.png') plt.close() df_total_value = pd.DataFrame(self.asset_memory) df_total_value.to_csv('results/account_value_train.csv') df_total_value.columns = ['account_value'] df_total_value['daily_return'] = df_total_value.pct_change(1) df_rewards = pd.DataFrame(self.rewards_memory) return self.state, self.reward*REWARD_SCALING,self.terminal, {} else: # print(np.array(self.state[1:29])) action = action * HMAX_NORMALIZE #actions = (actions.astype(int)) begin_total_asset = self.state[0]+ self.state[1]*self.df.iloc[self.day,-1] if action<0: self._sell_stock( action) else: self._buy_stock(action) self.day += 1 #load next state # print("stock_shares:{}".format(self.state[29:])) self.state = [self.state[0]] + [self.state[1]] + self.df.iloc[self.day-N:self.day,1].tolist() + \ self.df.iloc[self.day-N:self.day,2].tolist() + self.df.iloc[self.day-N:self.day,3].tolist() + \ self.df.iloc[self.day-N:self.day,4].tolist() + self.df.iloc[self.day-N:self.day,5].tolist() + \ self.df.iloc[self.day-N:self.day,6].tolist() + self.df.iloc[self.day-N:self.day,7].tolist() end_total_asset = self.state[0]+ self.state[1]*self.df.iloc[self.day,-1] try: self.asset_memory.append(end_total_asset.item()) except: self.asset_memory.append(end_total_asset) #print("end_total_asset:{}".format(end_total_asset)) self.reward = end_total_asset - begin_total_asset self.rewards_memory.append(self.reward) self.reward = self.reward*REWARD_SCALING return np.array([self.state]),np.array([self.reward]),self.terminal,{} def reset(self): self.asset_memory = [INITIAL_ACCOUNT_BALANCE] self.day = 374 self.cost = 0 self.trades = 0 self.terminal = False self.rewards_memory = [] self.state = [INITIAL_ACCOUNT_BALANCE] + [0] + self.df.iloc[self.day-N:self.day,1].tolist() + \ self.df.iloc[self.day-N:self.day,2].tolist() + self.df.iloc[self.day-N:self.day,3].tolist() + \ self.df.iloc[self.day-N:self.day,4].tolist() + self.df.iloc[self.day-N:self.day,5].tolist() + \ self.df.iloc[self.day-N:self.day,6].tolist() + self.df.iloc[self.day-N:self.day,7].tolist() return self.state def render(self,mode='human'): return self.state def _seed(self,seed=None): self.np_random, seed = seeding.np_random(seed) return [seed] fetch 函数。

这样它就不会通过真实网络发送 HTTP 请求,也不依赖于真实系统时间(本地机器、CI/CD 服务器),从而允许您的单元测试在隔离的、无副作用的环境中运行。

例如

Date.now()

actionCreators.ts

const REQUEST_POSTS = 'REQUEST_POSTS'; const RECEIVE_POSTS = 'RECEIVE_POSTS'; export function fetchPosts(subreddit) { return (dispatch) => { dispatch(requestPosts(subreddit)); return fetch(`https://www.reddit.com/r/${subreddit}.json`) .then((response) => response.json()) .then((json) => dispatch(receivePosts(subreddit, json))); }; } export function requestPosts(subreddit) { return { type: REQUEST_POSTS, subreddit, }; } export function receivePosts(subreddit, json) { return { type: RECEIVE_POSTS, subreddit, posts: json.data.children.map((child) => child.data), receivedAt: Date.now(), }; }

actionCreators.test.ts

单元测试结果:

import createMockStore from 'redux-mock-store';
import thunk, { ThunkDispatch } from 'redux-thunk';
import { AnyAction } from 'redux';
import { fetchPosts } from './actionCreators';
import sinon from 'sinon';
import { expect } from 'chai';

const middlewares = [thunk];
const mockStore = createMockStore<{}, ThunkDispatch<{}, any, AnyAction>>(middlewares);

describe('46791110', () => {
  afterEach(() => {
    sinon.restore();
  });
  it('should pass', () => {
    const store = mockStore({});
    const mRes = { json: sinon.stub().resolves({ data: { children: [{ data: 'jav' }, { data: 'big xx' }] } }) };
    const fetchStub = sinon.stub().resolves(mRes);
    const dateStub = sinon.stub(Date, 'now').returns(111);
    global.fetch = fetchStub;
    return store.dispatch(fetchPosts('nsfw')).then(() => {
      const actions = store.getActions();
      expect(actions).to.be.deep.equal([
        { type: 'REQUEST_POSTS', subreddit: 'nsfw' },
        {
          type: 'RECEIVE_POSTS',
          subreddit: 'nsfw',
          posts: ['jav', 'big xx'],
          receivedAt: 111,
        },
      ]);
      sinon.assert.calledOnceWithExactly(fetchStub, 'https://www.reddit.com/r/nsfw.json');
      sinon.assert.calledOnce(mRes.json);
      sinon.assert.calledOnce(dateStub);
    });
  });
});

源代码:https://github.com/mrdulin/jest-v26-codelab/tree/main/examples/46791110