How to test redux-saga take with pattern function by Jest? - unit-testing

I have some saga using take effect to get action by pattern. Now, I'm trying to test it:
Saga:
export function* mySaga(spaceId) {
while (true) {
const action = yield take(
action =>
action.type === MEDIA_SESSION_RESPONSE &&
action.payload.data.category === ScreenShareData &&
action.payload.data.topicId === spaceId
);
const remoteScreenSessionId = action.payload.data.content.sessionId;
yield put(addRemoteScreenSessionId(remoteScreenSessionId));
}
}
Test:
it('test', () => {
const gen = sagas.mySaga('space_1');
expect(gen.next().value).toEqual(
take(
action =>
action.type === MEDIA_SESSION_RESPONSE &&
action.payload.data.category === ScreenShareData &&
action.payload.data.topicId === spaceId
)
);
});
In take pattern I have an anonymous function, so how I can to test that pattern in take is equals to pattern, I provide in test?
Test result now is:
Expected value to equal:
{"##redux-saga/IO": true, "combinator": false, "payload": {"pattern": [Function anonymous]}, "type": "TAKE"}
Received:
{"##redux-saga/IO": true, "combinator": false, "payload": {"pattern": [Function anonymous]}, "type": "TAKE"}
If I will use JSON.stringify, I can't be sure that patterns are equal.

Jest can help you assert on the order of effects layer. In unit testing, you want to test a snippet of code (i.e saga, function, etc) not a flow of execution chain. Hence, the pattern you are trying to test is given [Function anonymous].
There is a little library called k-redux-saga-tester that can provide you with the flexibility of asserting on patterns. Fill in the VALUEs and create the necessary mocks for MEDIA_SESSION_RESPONSE, ScreenShareData, spaceId. It should look something like this:
describe('mySaga', () => {
describe('search', () => {
const test = tester(mySaga) <--- your saga
const spaceId = 'VALUE';
const MEDIA_SESSION_RESPONSE = 'VALUE';
const ScreenShareData = 'VALUE';
const payload = { data: { category: VALUE, topicId: VALUE }};
const action = { type: 'YOUR_ACTION_TYPE', payload: { payload }};
it('should search and set something', () => {
const mocks = {
take: [action =>
action.type === MEDIA_SESSION_RESPONSE &&
action.payload.data.category === ScreenShareData &&
action.payload.data.topicId === spaceId]
}
expect(test(mocks)).toMatchSnapshot()
})
})
})
In general, if you want to test the pattern, use a separate test for testing only that specific function. And assert it output given different arguments.

Related

Writing tests for RxJS that uses retryWhen operator (understanding difference from retry operator)

I'm trying to write tests for the following function that uses retryWhen operator:
// some API I'm using and mocking out in test
import { geoApi } from "api/observable";
export default function retryEpic(actions$) {
return actions$.pipe(
filter(action => action === 'A'),
switchMap(action => {
return of(action).pipe(
mergeMap(() => geoApi.ipLocation$()),
map(data => ({ data })),
retryWhen(errors => {
return errors.pipe(take(2));
}),
);
}),
);
}
The code is supposed to perform a request to some remote API geoApi.ipLocation$(). If it gets an error, it retries 2 times before giving up.
I have written the following test code that uses Jest and RxJS TestScheduler:
function basicTestScheduler() {
return new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
}
const mockApi = jest.fn();
jest.mock('api/observable', () => {
return {
geoApi: {
ipLocation$: (...args) => mockApi(...args),
},
};
});
describe('retryEpic()', () => {
it('retries fetching 2 times before succeeding', () => {
basicTestScheduler().run(({ hot, cold, expectObservable, expectSubscriptions }) => {
const actions$ = hot('-A');
// The first two requests fail, third one succeeds
const stream1 = cold('-#', {}, new Error('Network fail'));
const stream2 = cold('-#', {}, new Error('Network fail'));
const stream3 = cold('-r', { r: 123 });
mockApi.mockImplementationOnce(() => stream1);
mockApi.mockImplementationOnce(() => stream2);
mockApi.mockImplementationOnce(() => stream3);
expectObservable(retryEpic(actions$)).toBe('----S', {
S: { data: 123 },
});
expectSubscriptions(stream1.subscriptions).toBe('-^!');
expectSubscriptions(stream2.subscriptions).toBe('--^!');
expectSubscriptions(stream3.subscriptions).toBe('---^');
});
});
});
This test fails.
However, when I replace retryWhen(...) with simply retry(2), then the test succeeds.
Looks like I don't quite understand how to implement retry with retryWhen. I suspect this take(2) is closing the stream and kind of preventing everything from continuing. But I don't quite understand it.
I actually want to write some additional logic inside retryWhen(), but first I need to understand how to properly implement retry() with retryWhen(). Or perhaps that's actually not possible?
Additional resources
My implementation of retryWhen + take was based on this SO answer:
How to create an RXjs RetryWhen with delay and limit on tries
Official docs:
retryWhen
You can use retryWhen for those two purposes, one to have your logic in it and the second is the retry numbers you'd like to give it (no need to use retry operator):
// some API I'm using and mocking out in test
import { geoApi } from "api/observable";
export default function retryEpic(actions$) {
return actions$.pipe(
filter(action => action === 'A'),
switchMap(action => {
return of(action).pipe(
mergeMap(() => geoApi.ipLocation$()),
map(data => ({ data })),
retryWhen(errors =>
errors.pipe(
mergeMap((error, i) => {
if (i === 2) {
throw Error();
}
// return your condition code
})
)
)
)
}),
);
}
Here is a simple DEMO of that.
As for understanding this logic:
retryWhen and retry operators, according to the Official docs you've referenced:
resubscribing to the source Observable (if no error or complete executes)
This is why you can't pipe retry and retryWhen together. You can say that these operators are a chain breakers...

How to test timeout() in a rxjs pipe with jasmine-marbles

I have written a pipe that filters an input observable. In the pipe I specify a timeout with the timeout() operator to abort waiting if the expected value is not emitted by the source in time.
I want to test the timeout case with jasmine-marbles, but I can't get it to work.
I believe that expect(source).toBeObservable() evaluates before the source emits.
see Stackblitz
The pipe to be tested:
source = cold('a', { a: { id: 'a' } }).pipe(
timeout(500),
filter((a) => false),
catchError((err) => {
return of({ timeout: true })
}),
take(1)
);
Testing with toPromise() works as expected:
expect(await source.toPromise()).toEqual({ timeout: true });
Testing with jasmine-marbles
const expected = cold('500ms (a|)', { a: { timeout: true } });
expect(source).toBeObservable(expected);
fails with the error
Expected $.length = 0 to equal 2.
Expected $[0] = undefined to equal Object({ frame: 500, notification: Notification({ kind: 'N', value: Object({ timeout: true }), error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 500, notification: Notification({ kind: 'C', value: undefined, error: undefined, hasValue: false }) }).
Support for time progression was recently added (see jasmine-marbles PR #38) to jasmine-marbles 0.5.0. Additional test specs were added to the package that demonstrate one of a couple of possible ways to accomplish what you want. Here are some options I was able to throw together using your Stackblitz sample.
Option 1
When you initialize the source observable outside the test method (e.g. in beforeEach), you must explicitly initialize and pass the test scheduler to timeout to get expect().toBeObservable() working. However, take note that this change will break the "should work with toPromise" test. (I don't know why, but toPromise() doesn't appear to work with this approach.)
describe('Marble testing with timeout', () => {
let source;
beforeEach(() => {
// You must explicitly init the test scheduler in `beforeEach`.
initTestScheduler()
source = cold('a', { a: { id: 'a' } }).pipe(
// You must explicitly pass the test scheduler.
timeout(500, getTestScheduler()),
filter((a) => false),
catchError(err => {
return of({ timeout: true })
}),
take(1)
);
});
it('should work with toBeObservable', () => {
const expected = cold('500ms (a|)', { a: { timeout: true } });
expect(source).toBeObservable(expected);
});
});
Option 2
You can refactor things slightly and initialize the source observable inside the test method (not in beforeEach). You don't need to explicitly initializes the test scheduler (jasmine-marbles will do it for you before the test method runs), but you still have to pass it to timeout. Note how the createSource function can be used with the test scheduler or the default scheduler (if the scheduler argument is left undefined). This options works with both the "should work with toPromise" test and the "should work with toBeObservable" test.
describe('Marble testing with timeout', () => {
const createSource = (scheduler = undefined) => {
return cold('a', { a: { id: 'a' } }).pipe(
// You must explicitly pass the test scheduler (or undefined to use the default scheduler).
timeout(500, scheduler),
filter((a) => false),
catchError(err => {
return of({ timeout: true })
}),
take(1)
);
};
it('should work with toPromise', async () => {
const source = createSource();
expect(await source.toPromise()).toEqual({ timeout: true });
});
it('should work with toBeObservable', () => {
const source = createSource(getTestScheduler());
const expected = cold('500ms (a|)', { a: { timeout: true } });
expect(source).toBeObservable(expected);
});
});
Option 3
Finally, you can skip passing the test scheduler to timeout if you explicitly use the test scheduler's run method, but you must use expectObservable (as opposed to expect().toBeObservable(). It works just fine, but Jasmine will report the warning "SPEC HAS NO EXPECTATIONS".
describe('Marble testing with timeout', () => {
let source;
beforeEach(() => {
source = cold('a', { a: { id: 'a' } }).pipe(
timeout(500),
filter((a) => false),
catchError(err => {
return of({ timeout: true })
}),
take(1)
);
});
it('should work with scheduler and expectObservable', () => {
const scheduler = getTestScheduler();
scheduler.run(({ expectObservable }) => {
expectObservable(source).toBe('500ms (0|)', [{ timeout: true }]);
});
});
});

How can I pass a boolean to an expect statement for testing using mocha/chai?

Using Vue CLI I have a unit test that I am trying to check for a true/false that looks like this:
describe('The thing', () => {
it('must be available.', () => {
const available = true
const wrapper = shallowMount(MyVueComponent, {
propsData: { available },
})
expect(wrapper).to.be.true
})
})
When I run npm run test:unit
I get the following:
AssertionError: expected { Object (isFunctionalComponent, _emitted, ...) } to be true
If I just check the value of available, then it's all good. But that seems like I'm doing it wrong.
Other tests I have written are working fine as I am checking for a text value:
describe('The thing', () => {
it('should have a name.', () => {
const name = 'Hal'
const wrapper = shallowMount(MyVueComponent, {
propsData: { name },
})
expect(wrapper.text()).to.include(name)
})
})
I am not sure how to check that the available is a boolean and it must be true. Any suggestions would be appreciated!
EDIT
This is what my Vue component looks like:
export default {
name: 'MyVueComponent',
props: {
name: String
},
data: function() {
return {
available: true,
}
},
}
EDIT 2
This seems to work in my unit test:
describe('The thing', () => {
it('must be available.', () => {
const available = true
const wrapper = shallowMount(MyVueComponent, {
propsData: { available },
})
expect(wrapper.vm.available).to.be.true
})
})
However, it is looking at my actual component in my /src directory. If I change the data values from true to false my tests come out correctly. I'm not sure how to have the data stay at the test level. So if I were to change const available = false, my test should fail--but it does not.
EDIT 3
It seems like this works (to access the data object):
describe("The thing", () => {
it("must be available.", () => {
const defaultData = MyVueComponent.data();
// const wrapper = shallowMount(MyVueComponent, {});
expect(defaultData.available).to.be.true;
});
});
But it still seems not right that I'm referencing my actual code, and not sticking within the unit tests.
You want to check the received prop, which you can do with wrapper.props()
describe('The thing', () => {
it('must be available.', () => {
const available = true
const wrapper = shallowMount(MyVueComponent, {
propsData: { available },
})
expect(wrapper.props().available).to.be.true
// or:
// expect(wrapper.props().available).to.equal(available)
})
})
Chai's .to.be.true and .to.equal use === so there is no need to separately check that it is indeed a Boolean, but if you prefer the "expressiveness" of it, you can check it too:
expect(wrapper.props().available).to.be.a('boolean')

how to test a function dependent on document global?

I want to start writing unit tests for my code. I thought I'd start with a nice and simple function.
string => string
in action here: http://jsbin.com/yufuzawalo/edit?js,console,output
const animEndString = (type = 'transition') => {
let types =
type === 'transition'
? {
OTransition: 'oTransitionEnd',
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
transition: 'transitionend'
}
: {
OAnimation: 'oAnimationEnd',
WebkitAnimation: 'webkitAnimationEnd',
MozAnimation: 'animationend',
animation: 'animationend'
}
const elem = document.createElement('div')
return Object.keys(types).reduce(function(prev, trans) {
return undefined !== elem.style[trans] ? types[trans] : prev
}, '')
}
And the test:
describe('example', () => {
it('should return animationend when passed the string animation', () => {
const value = animationEnd('animation')
expect(value).toBe('animationend')
})
it('should return transitionEnd when passed the string animation', () => {
const value = animationEnd('transition')
expect(value).toBe('transitionend')
})
})
output:
example › should return transitionEnd when passed the string animation
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"transitionend"
Received:
The test is failing as an empty string is being returned. I'm presuming that Jest doesn't know what to do with document.createElement('fake')
How would I get around this issue?
Solved...
I've added the following to my setup-jest file
global.document.createElement = jest.fn(() => ({
style: {
transition: 'opacity 300ms ease',
animation: 'test 300ms'
}
}))

How to unit test PanResponder in React Native?

I am working on a React Native component handling gestures via PanResponder.
I would like to test that when certain dx, dy values are passed to the onPanResponderRelease method, expected action will be executed. Please find below an example of what I would like to test:
export default class MyComponent extends Component<Props, State> {
constructor(props: Props) {
super(props);
this._panResponder = this._initPanResponder();
}
_initPanResponder(): PanResponder {
const panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([
null,
{ dx: this._animation.x, dy: this._animation.y }
]),
onPanResponderRelease: (e, gestureState) => {
const { dx, dy } = gestureState;
if (dy > 100) {
doSomething(); // <---- what I would like to unit test
}
}
});
return panResponder;
}
}
Is there any straightforward way to unit test the following with Jest?
I tend to leave testing the wiring code out, since testing an actual PanResponder involves a UI test that is hard to write, is usually flaky, and gives little value.
What you can do, is extract the event handling function out, and test it independently.
In that case, the test would very simple, as all you need to do is invoke the handler in the test and see you're getting what you expected.
If you want to test user's interection with the component you should mock PanResponder in a way you forward the functions given as params to PanResponder.create to the panHandlers it returns.
// setup-tests.ts
jest.doMock('react-native', () => {
return Object.setPrototypeOf(
PanResponder: {
...ReactNative.PanResponder,
create: (config: any) => ({ panHandlers: config }),
},
},
ReactNative
);
});
Tha way, when yoou spread panHandlers in the View you are handling Pan, you can access the functions the way you set up in the file
// pan-handling-component.tsx
const Component: React.FC = () => {
...
const panResponder = React.useRef(
PanResponder.create({
...
onPanResponderMove: (_, gestureState) => {
Animated.event([slideAnim], { useNativeDriver: false })(
gestureState.moveY
);
},
onPanResponderTerminationRequest: () => true,
onPanResponderRelease: () => {
// #ts-ignore
const currentValue = slideAnim._value;
if (currentValue < 0) {
slideIn();
} else if (currentValue > height - 300) {
handleClose();
}
},
})
).current;
...
return (
<View
testID={DRAWER_WRAPPER_TESTID}
{...panResponder.panHandlers}
>
...
</View>
);
And, finally, the unit test would be something like this
// pan-handling-component.test.tsx
...
const panHandler = instance.root.findByProps({
testID: DRAWER_WRAPPER_TESTID,
});
act(() => {
panHandler.props.onPanResponderMove(eventMock, gestureStateMOck);
panHandler.props.onPanResponderRelease();
});
...