Redux-saga-test-plan expectSaga 似乎在独立测试之间保留状态
Redux-saga-test-plan expectSaga seems to retain state between independent tests
我有以下两个测试
import {put, select, takeEvery} from 'redux-saga/effects';
import {combineReducers} from 'redux';
export default class SessionReducer {
public static readonly _initialState: any = {
disconnectCounts: {},
};
public static reducer(state: any = SessionReducer._initialState, action: any): any {
// console.log('reducer', action);
let newState: any;
switch (action.type) {
case 'DEVICE_DISCONNECTED':
newState = {
...state,
};
if (!newState.disconnectCounts[action.value]) newState.disconnectCounts[action.value] = 0;
newState.disconnectCounts[action.value]++;
newState.error = {
type: 'DEVICE_DISCONNECTED',
utc: 1,
};
return newState;
default:
return state;
}
}
}
export function* errorHandler() {
yield takeEvery(['DEVICE_DISCONNECTED'], function* (action: any) {
let state = yield select();
console.log('*********', state);
if (state.session.disconnectCounts[action.value] > 1) {
yield put({
type: 'WATCH_REBOOT_REQUEST',
});
// state.session.disconnectCounts[action.value] = 0
}
});
}
let action = {type: 'DEVICE_DISCONNECTED', value: '111'};
describe('Handles Error States and Transitions', () => {
test('Sends watch reboot request when disconnection count threshold met', () => {
return expectSaga(errorHandler)
.withReducer(
combineReducers({
session: SessionReducer.reducer,
}),
{session: SessionReducer._initialState},
)
.dispatch(action)
.dispatch(action)
.put({type: 'WATCH_REBOOT_REQUEST'})
.run()
.then((result: {storeState: any}) => {
debugger;
let session = result.storeState.session;
expect(session.disconnectCounts[action.value]).toBe(2); // values for error are tested in reducer test
expect(session.error).toBeTruthy(); // values for error are tested in reducer test
});
});
test('Does not send WATCH_REBOOT_REQUEST when threshold not met', () => {
return expectSaga(errorHandler)
.withReducer(
combineReducers({
session: SessionReducer.reducer,
}),
{session: SessionReducer._initialState},
)
.dispatch(action)
.run()
.then((result: {storeState: any}) => {
let session = result.storeState.session;
expect(session.disconnectCounts[action.value]).toBe(1); // values for error are tested in reducer test
// expect(session.currentScreen).toEqual('actionRequiredIdleScreen');
});
});
});
如果你 运行 每个独立测试,我使用 .only,它们通过但 运行 它们没有 .only 并且第二个测试总是失败 w/ disconnectCounts
Handles Error States and Transitions
✓ Sends watch reboot request when disconnection count threshold met (263 ms)
✕ Does not send WATCH_REBOOT_REQUEST when threshold not met (258 ms)
● Handles Error States and Transitions › Does not send WATCH_REBOOT_REQUEST when threshold not met
expect(received).toBe(expected) // Object.is equality
Expected: 1
Received: 3
76 | .then((result: {storeState: any}) => {
77 | let session = result.storeState.session;
> 78 | expect(session.disconnectCounts[action.value]).toBe(1); // values for error are tested in reducer test
| ^
79 | // expect(session.currentScreen).toEqual('actionRequiredIdleScreen');
80 | });
81 | });
at __tests__/sagas/sagaStateIssue.ts:78:64
at tryCallOne (node_modules/promise/lib/core.js:37:12)
at node_modules/promise/lib/core.js:123:15
at flush (node_modules/asap/raw.js:50:29)
我错过了什么?
我认为问题在于两个测试都使用了 SessionReducer._initialState
的相同引用。当您将它传递给 withReducer
中的状态时,它不会以任何方式被克隆,因此您最终会在内存中使用相同的对象。
有很多方法可以修复它,例如你可以用一个方法而不是 属性 来创建初始对象:
_initialState = () => ({disconnectCounts: {}})
// ...
.withReducer(
combineReducers({
session: SessionReducer.reducer,
}),
{session: SessionReducer._initialState()},
)
或者你可以在测试中自己深度克隆对象
const deepClone = obj => JSON.parse(JSON.stringify(obj))
// ...
.withReducer(
combineReducers({
session: SessionReducer.reducer,
}),
{session: deepClone(SessionReducer._initialState)},
)
将 reducer 和 state 放在一起 class 是 redux 的反模式。
const initialState = () => ({ disconnectCounts: {} });
const reducer = (state: any = initialState(), action: any): any => {
您坚持对 initialState 的单一引用
最好有一个函数 returns 一个新实例
https://codesandbox.io/s/proud-morning-0w4wu?file=/src/testy.test.ts:175-182
这是一个包含测试的沙箱 运行
我有以下两个测试
import {put, select, takeEvery} from 'redux-saga/effects';
import {combineReducers} from 'redux';
export default class SessionReducer {
public static readonly _initialState: any = {
disconnectCounts: {},
};
public static reducer(state: any = SessionReducer._initialState, action: any): any {
// console.log('reducer', action);
let newState: any;
switch (action.type) {
case 'DEVICE_DISCONNECTED':
newState = {
...state,
};
if (!newState.disconnectCounts[action.value]) newState.disconnectCounts[action.value] = 0;
newState.disconnectCounts[action.value]++;
newState.error = {
type: 'DEVICE_DISCONNECTED',
utc: 1,
};
return newState;
default:
return state;
}
}
}
export function* errorHandler() {
yield takeEvery(['DEVICE_DISCONNECTED'], function* (action: any) {
let state = yield select();
console.log('*********', state);
if (state.session.disconnectCounts[action.value] > 1) {
yield put({
type: 'WATCH_REBOOT_REQUEST',
});
// state.session.disconnectCounts[action.value] = 0
}
});
}
let action = {type: 'DEVICE_DISCONNECTED', value: '111'};
describe('Handles Error States and Transitions', () => {
test('Sends watch reboot request when disconnection count threshold met', () => {
return expectSaga(errorHandler)
.withReducer(
combineReducers({
session: SessionReducer.reducer,
}),
{session: SessionReducer._initialState},
)
.dispatch(action)
.dispatch(action)
.put({type: 'WATCH_REBOOT_REQUEST'})
.run()
.then((result: {storeState: any}) => {
debugger;
let session = result.storeState.session;
expect(session.disconnectCounts[action.value]).toBe(2); // values for error are tested in reducer test
expect(session.error).toBeTruthy(); // values for error are tested in reducer test
});
});
test('Does not send WATCH_REBOOT_REQUEST when threshold not met', () => {
return expectSaga(errorHandler)
.withReducer(
combineReducers({
session: SessionReducer.reducer,
}),
{session: SessionReducer._initialState},
)
.dispatch(action)
.run()
.then((result: {storeState: any}) => {
let session = result.storeState.session;
expect(session.disconnectCounts[action.value]).toBe(1); // values for error are tested in reducer test
// expect(session.currentScreen).toEqual('actionRequiredIdleScreen');
});
});
});
如果你 运行 每个独立测试,我使用 .only,它们通过但 运行 它们没有 .only 并且第二个测试总是失败 w/ disconnectCounts
Handles Error States and Transitions
✓ Sends watch reboot request when disconnection count threshold met (263 ms)
✕ Does not send WATCH_REBOOT_REQUEST when threshold not met (258 ms)
● Handles Error States and Transitions › Does not send WATCH_REBOOT_REQUEST when threshold not met
expect(received).toBe(expected) // Object.is equality
Expected: 1
Received: 3
76 | .then((result: {storeState: any}) => {
77 | let session = result.storeState.session;
> 78 | expect(session.disconnectCounts[action.value]).toBe(1); // values for error are tested in reducer test
| ^
79 | // expect(session.currentScreen).toEqual('actionRequiredIdleScreen');
80 | });
81 | });
at __tests__/sagas/sagaStateIssue.ts:78:64
at tryCallOne (node_modules/promise/lib/core.js:37:12)
at node_modules/promise/lib/core.js:123:15
at flush (node_modules/asap/raw.js:50:29)
我错过了什么?
我认为问题在于两个测试都使用了 SessionReducer._initialState
的相同引用。当您将它传递给 withReducer
中的状态时,它不会以任何方式被克隆,因此您最终会在内存中使用相同的对象。
有很多方法可以修复它,例如你可以用一个方法而不是 属性 来创建初始对象:
_initialState = () => ({disconnectCounts: {}})
// ...
.withReducer(
combineReducers({
session: SessionReducer.reducer,
}),
{session: SessionReducer._initialState()},
)
或者你可以在测试中自己深度克隆对象
const deepClone = obj => JSON.parse(JSON.stringify(obj))
// ...
.withReducer(
combineReducers({
session: SessionReducer.reducer,
}),
{session: deepClone(SessionReducer._initialState)},
)
将 reducer 和 state 放在一起 class 是 redux 的反模式。
const initialState = () => ({ disconnectCounts: {} });
const reducer = (state: any = initialState(), action: any): any => {
您坚持对 initialState 的单一引用 最好有一个函数 returns 一个新实例
https://codesandbox.io/s/proud-morning-0w4wu?file=/src/testy.test.ts:175-182
这是一个包含测试的沙箱 运行