Closed spiicy-sauce closed 6 years ago
Thank you for making this post, it's very helpful, as I'm also trying to test asynchronous epic's.
Hey, @tcclevela! Thank you for sharing your thoughts, I've learned a lot from this and other issues. Here is a very naive "ping-pong" sketch of what I've came up with for the current project I'm working on: https://gist.github.com/vyorkin/2b344e16e17be480e7065c5067c9e30e, another example: https://gist.github.com/vyorkin/9ee747744802f31ae2881d2fd2db61e0 (a bit outdated now, I've changed a lot yesterday night)
I got something that seems a little bit simpler for high-level testing epics with async operators (using Jasmine). I'd love thoughts on this approach - it feels a little sneaky to reach into the core Observable.prototype, but maybe okay?
epic.js
const suggestionsEpic = (action$) => {
// assume some input is emitting these update text actions
let changes$ = action$.ofType('update_text')
// do some fetches and map to a success action
let suggestionFetches$ = changes$
.debounceTime(200)
.flatMap(action => Observable.from($.get('/suggest', { q: action.data.q })
.map(response => ({type: 'suggestion_fetch_success', data: response.data }))
)
return suggestionFetches$
}
test.js
import { Observable } from 'rxjs'
import mockStore from 'redux-mock-store'
describe('suggestionsEpic', () => {
let middleware, store, response
beforeEach(() => {
spyOn(Observable.prototype, 'debounceTime').and.callFake(function () {
return this
})
let mockStore = createMockStore([createEpicMiddleware(suggestionsEpic)])
store = mockStore({})
response = { data: ['a', 'b', 'c'] }
spyOn($, 'get').and.returnValue(Promise.resolve(response))
})
it('on text update, calls the api and emits the right action on success', (done) => {
let action = {type: 'update_text', data: { q: 'test search' }}
store.dispatch(action).then(() => {
expect(store.getActions()).toEqual([action,
{type: 'suggestion_fetch_success', data: response.data }])
done()
})
expect($.get).toHaveBeenCalledWith('/suggest', { q: 'test search' })
})
})
The key moment being:
spyOn(Observable.prototype, 'debounceTime').and.callFake(function () {
return this
})
I spied on debounce instead of delay, but it should work for any of the async methods
@rrcobb stubbing stuff on the prototype for testing isn't that bad, your solution seems pretty fair. The issue is mostly that you aren't asserting that the time really did pass as expected. Your approach could be extended to do that as well, though.
RxJS TestScheduler would ideally solve these, but none of us have had cycles to fix it. 😢
@jayphelps Is there any update on the RxJS TestScheduler mentioned above? In lieu of the tooling has there been any further work around 'best practice' for mocking out the test scheduler?
If you are using rxjs 6 and redux-observable 1.0.0+ use can use almost the same pattern as @rrcobb :
import { of } from 'rxjs';
import * as operators from 'rxjs/operators';
spyOn(operators, 'delay').and.returnValue(() => of([]));
Good news: we made the new testScheduler.run(callback)
in rxjs v6 to make things much easier: https://github.com/ReactiveX/rxjs/blob/master/doc/marble-testing.md tl;dr it provides automatic usage of the TestScheduler instance for async operators (no need to inject it), provides a new time progression syntax e.g. a 100ms b
, and changes the value of a single frame from 10ms to 1ms.
The redux-observable docs also mention it: https://redux-observable.js.org/docs/recipes/WritingTests.html though the meat of the documentation is still the rxjs doc listed above.
I found a workaround that allow me to test a code wich use the delay()
operator with jest faketimers and without having to do marbles: https://stackoverflow.com/a/56478886/5251198
So I just spent a questionable amount of time trying to test an epic that has a
delay
in it, and after many breakpoints in source code and lots of head scratching, I figured out a solution that I think would be a helpful start for people trying to do the same thing. (I couldn't find any reference to this kind of test on the interwebs). The epic I was trying to test is pretty straightforward:It just looks for alert creation actions and emits tear-down actions after
duration
time iftimeout
is true. So I tried to use the example here to do so. I started by making a function to get a test scheduler (for reference, I'm using jasmine for testing):And then made a (bare bones) version of the
expectEpic
method from the example I linked to above:When I finally ran a pretty simple marble test:
I got the error:
Expected [ ] to equal [ Object({ frame: 40, ...
No actions were actually emitted by the time
flush
was called because the epic was getting hung up on the delay (this happens if you add adelay
to the JSBin example linked to from theredux-observable
website as well: here). This was honestly the thing that I couldn't figure out for a really long time, until I remembered a document about testing with Rxjs4 that I had skimmed over, which mentions injecting schedulers into operators. The issue was that delay was being scheduled completely separately from the test. So the epic, by the time the test flushed, had not actually emitted any actions.Ultimately the solution to my problem was to inject my
testScheduler
into the delay operator at test-time.Which injects the test scheduler into time-based operators (although right now I only have delay). This method can be called right after the
testScheduler
is created inexpectEpic
and voila, thedelay
operator is now scheduled bytestScheduler
.I hope my day of struggling helps at least one person with a similar problem. I'm really interested in seeing
redux-observable
andrxjs
testing utilities develop even further. Being able to easily test complex async flows is absurdly powerful as a developer.