查看react-redux网站上的高级示例,使用fetchPosts
,mocha
和chai
对sinon
函数进行单元测试的最佳方法是什么? ?它使用了一个承诺链,所以鉴于此,我查看了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 dispatch
和requestPosts
receivePosts
被调用两次
答案 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