RxJS / Redux-observables: How would I test EventEmitter? - rxjs

I am trying to test that redux actions are fired when the finish event on my upload object is fired.
Here's my epic:
const uploadFileEpic = (action$, state$, dependencies$) =>
action$.pipe(
ofType(uploadActions.UPLOAD_FILE),
mergeMap(({ payload }) => {
const { file, masterHandle } = payload;
return new Observable(o => {
const upload = masterHandle.uploadFile("/", file);
const handle = upload.handle;
upload.on("finish", () => {
o.next(
uploadActions.uploadSuccess({
masterHandle
})
);
o.complete();
});
});
})
);
Here is what I have written so far. It doesn't work:
import { of } from "rxjs";
import "rxjs/add/operator/toArray";
import { EventEmitter } from "events";
import uploadActions from "../actions/upload-actions";
import uploadEpic from "./upload-epic";
test("uploadFilesEpic filesActions.UPLOAD_FILE on success", done => {
const file = { name: "f1" };
const upload = new EventEmitter();
upload.handle = "h1";
const masterHandle = {
uploadFile: jest.fn(() => upload)
};
const action$ = of(uploadActions.uploadFile({ file, masterHandle }));
upload.emit("finish");
uploadEpic(action$).subscribe(actions => {
expect(actions).toEqual(uploadActions.uploadSuccess({ masterHandle }));
done();
});
});
It says the async callback was not fired:
FAIL src/redux/epics/upload-epic.test.js (8.531s)
✓ uploadFilesEpic filesActions.UPLOAD_FILES (9ms)
✕ uploadFilesEpic filesActions.UPLOAD_FILE on success (5021ms)
● uploadFilesEpic filesActions.UPLOAD_FILE on success
Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error:
23 | });
24 |
> 25 | test("uploadFilesEpic filesActions.UPLOAD_FILE on success", done => {
| ^
26 | const file = { name: "f1" };
27 | const upload = new EventEmitter();
28 | upload.handle = "h1";
at new Spec (node_modules/jest-config/node_modules/jest-jasmine2/build/jasmine/Spec.js:116:22)
at Object.test (src/redux/epics/upload-epic.test.js:25:1)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 9.297s
It makes sense to me that the test is failing but I'm not sure how to subscribe to the epic, fire the finish event, and then inspect the actions returned by the epic.

The event for finishing the upload is emitted too early in the test. The EventEmitter does not keep a buffer of events and if no one is subscribed, then the even is lost.
Move the emit "finish" to the bottom of the test.
upload.emit("finish"); // must emit after subscribing

Related

Redux connected React component not updating until a GET api request is recalled

My react app uses a redux connected component to render data from backend for a project page, so I called a GET dispatch inside a React Hook useEffect to make sure data is always rendered when the project page first open, and whenever there is a change in state project, the component will be updated accordingly using connect redux function. However, the component doesn't update after I reduce the new state using a DELETE API request, only if I dispatch another GET request then the state will be updated. So I have to call 2 dispatches, one for DELETE and one for GET to get the page updated synchronously (as you can see in handleDeleteUpdate function), and the same thing happened when I dispatch a POST request to add an update (in handleProjectUpdate). Only when I reload the page, the newly changed data will show up otherwise it doesn't happen synchronously, anyone knows what's wrong with the state update in my code? and how can I fix this so the page can be loaded faster with only one request?
I've changed the reducer to make sure the state is not mutated and is updated correctly.
I have also tried using async function in handleDeleteUpdate to make sure the action dispatch is finished
I have tried
console.log(props.project.data.updates)
to print out the updates list after calling props.deleteUpdate but it seems the updates list in the state have never been changed, but when I reload the page, the new updates list is shown up
Here is the code I have for the main connected redux component, actions, and reducers file for the component
function Project(props) {
let options = {year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit'}
const {projectID} = useParams();
const history = useHistory();
console.log(props.project.data? props.project.data.updates : null);
console.log(props.project.data);
// const [updates, setUpdates] = useState(props.project.data? props.project.data.updates : null)
useEffect(() => {
props.getProject(projectID);
}, []);
// Add an update to project is handled here
const handleProjectUpdate = async (updateInfo) => {
await props.postProjectUpdate(projectID, updateInfo)
await props.getProject(projectID);
}
const handleDeleteUpdate = async (updateID) => {
await props.deleteUpdate(projectID, updateID);
await props.getProject(projectID);
console.log(props.project.data.updates);
};
return (
<div>
<Navbar selected='projects'/>
<div className = "project-info-layout">
<UpdateCard
updates = {props.project.data.updates}
handleProjectUpdate = {handleProjectUpdate}
handleDeleteUpdate = {handleDeleteUpdate}
options = {options}
/>
</div>
</div>
)
}
const mapStateToProps = state => ({
project: state.project.project,
});
export default connect(
mapStateToProps,
{getProject, postProjectUpdate, deleteUpdate}
)(Project);
ACTION
import axios from 'axios';
import { GET_PROJECT_SUCCESS,ADD_PROJECT_UPDATE_SUCCESS, DELETE_PROJECT_UPDATE_SUCCESS} from './types';
let token = localStorage.getItem("token");
const config = {
headers: {
Authorization: `Token ${token}`,
}
};
export const getProject = (slug) => dispatch => {
axios.get(`${backend}/api/projects/` + slug, config)
.then(
res => {
dispatch({
type: GET_PROJECT_SUCCESS,
payload: res.data,
});
},
).catch(err => console.log(err));
}
export const postProjectUpdate = (slug, updateData) => dispatch => {
axios.post(`${backend}/api/projects/`+slug+ `/updates`,updateData, config)
.then(
res => {
dispatch({
type: ADD_PROJECT_UPDATE_SUCCESS,
payload: res.data,
});
},
).catch(err => console.log(err));
}
export const deleteUpdate = (slug, updateID) => dispatch => {
axios.delete(`${backend}/api/projects/`+ slug + `/updates/`+ updateID, config)
.then(
res => {
dispatch({
type: DELETE_PROJECT_UPDATE_SUCCESS,
payload: updateID,
});
},
).catch(err => console.log(err));
}
Reducer
import { GET_PROJECT_SUCCESS,ADD_PROJECT_UPDATE_SUCCESS, DELETE_PROJECT_UPDATE_SUCCESS} from "../actions/types";
const initialState = {
project: {},
};
export default function ProjectReducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_PROJECT_SUCCESS:
return {
...state, // return all initial state
project: payload
};
case ADD_PROJECT_UPDATE_SUCCESS:
return {
...state,
project: {
...state.project,
updates: [...state.project.data.updates, payload.data]
}
};
case DELETE_PROJECT_UPDATE_SUCCESS:
let newUpdatesArray = [...state.project.updates]
newUpdatesArray.filter(update => update.uuid !== payload)
return {
...state,
project: {
...state.project,
members: newUpdatesArray
}
};
default:
return state;
}
}
updateCard in the Project component is showing a list of all updates

how do I unit test this function which handles observables

I have created this function because for all the requests my application sends out using http.post, this is how different parts handle the response. So rather than duplicating the code, I thought to create a function. But I am unable to figure out how to unit test this function.
private editAnswerSubject: Subject<Result>;
subscribeToReturnedObservable(observable:Observable<any>, subject:Subject<Result>) {
observable.subscribe((res) => {
const ev = <HttpEvent<any>>(res);
if (ev.type === HttpEventType.Response) {
const isResponseStructureOK: boolean = this.helper.validateServerResponseStructure(ev.body);
if (isResponseStructureOK) {
const response: ServerResponseAPI = ev.body;
subject.next(new Result(response.result, response['additional-info']));
} else {
subject.next(new Result(messages.error, messages.invalidStructureOfResponse));
}
}
},
(error: ServerResponseAPI) => {
const errorMessage: string = this.helper.userFriendlyErrorMessage(error);
subject.next(new Result(messages.error, errorMessage));
},
() => { // observable complete
});
}
editAnswer(answer: Answer): any {
const observable = this.bs.editAnswer(answer)
this.subscribeToReturnedObservable(observable,this.editAnswerSubject);
}
The test I have written so far is
describe('subscribeToReturnedObservable tests:', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [QuestionManagementService, HelperService, WebToBackendInterfaceService, AuthService, HttpClient, HttpHandler]
});
});
fit('should call send next value for the subject is the response from the server is ok', () => {
const questionService:QuestionManagementService = TestBed.get(QuestionManagementService);
const body = {"result":"success", "additional-info":"some additional info"};
const receivedHttpEvent = new HttpResponse({body:body});
let observable = new Observable();
spyOn(observable,'subscribe').and.returnValue(receivedHttpEvent);
spyOn(questionService['editQuestionSubject'],'next');
questionService.subscribeToReturnedObservable(observable,questionService['editQuestionSubject']);
observable.subscribe();
expect(questionService['editQuestionSubject'].next).toHaveBeenCalled();
});
});
But it get error Expected spy next to have been called.
I did this (hoping that it is the right way). The scope of testing is to check that the Subject's next is called correctly. So create an Observable using of and let the code flow from there.
fit('should call send next value for the subject is the response from the server is ok', () => {
const questionService:QuestionManagementService = TestBed.get(QuestionManagementService);
const helperService:HelperService = TestBed.get(HelperService);
const body = {"result":"success", "additional-info":"some additional info"};
const receivedHttpEvent = new HttpResponse({body:body});
const expectedResult = new Result('success', 'some additional info');
spyOn(helperService,'validateServerResponseStructure').and.returnValue(true);
let observable = of(receivedHttpEvent);
spyOn(questionService['editQuestionSubject'],'next');
questionService.subscribeToReturnedObservable(observable,questionService['editQuestionSubject']);
expect(questionService['editQuestionSubject'].next).toHaveBeenCalledWith(expectedResult);
});

Testing NGRX effect with delay

I want to test an effect that works as follows:
Effect starts if LoadEntriesSucces action was dispatched
It waits for 5 seconds
After 5 seconds passes http request is send
When response arrives, new action is dispatched (depending, whether response was succes or error).
Effect's code looks like this:
#Effect()
continuePollingEntries$ = this.actions$.pipe(
ofType(SubnetBrowserApiActions.SubnetBrowserApiActionTypes.LoadEntriesSucces),
delay(5000),
switchMap(() => {
return this.subnetBrowserService.getSubnetEntries().pipe(
map((entries) => {
return new SubnetBrowserApiActions.LoadEntriesSucces({ entries });
}),
catchError((error) => {
return of(new SubnetBrowserApiActions.LoadEntriesFailure({ error }));
}),
);
}),
);
What I want to test is whether an effect is dispatched after 5 seconds:
it('should dispatch action after 5 seconds', () => {
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
expect(effects.continuePollingEntries$).toBeObservable(expected);
});
However this test does not work for me. Output from test looks like this:
Expected $.length = 0 to equal 3.
Expected $[0] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 30, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[2] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: LoadEntriesSucces({ payload: Object({ entries: [ Object({ type: 'type', userText: 'userText', ipAddress: '0.0.0.0' }) ] }), type: '[Subnet Browser API] Load Entries Succes' }), error: undefined, hasValue: true }) }).
What should I do to make this test work?
Like mentioned in another answer, one way to test that effect would be by using the TestScheduler but it can be done in a simpler way.
We can test our asynchronous RxJS code synchronously and deterministically by virtualizing time using the TestScheduler. ASCII marble diagrams provide a visual way for us to represent the behavior of an Observable. We can use them to assert that a particular Observable behaves as expected, as well as to create hot and cold Observables we can use as mocks.
For example, let's unit test the following effect:
effectWithDelay$ = createEffect(() => {
return this.actions$.pipe(
ofType(fromFooActions.doSomething),
delay(5000),
switchMap(({ payload }) => {
const { someData } = payload;
return this.fooService.someMethod(someData).pipe(
map(() => {
return fromFooActions.doSomethingSuccess();
}),
catchError(() => {
return of(fromFooActions.doSomethinfError());
}),
);
}),
);
});
The effect just waits 5 seconds after an initial action, and calls a service which would then dispatch a success or error action. The code to unit test that effect would be the following:
import { TestBed } from "#angular/core/testing";
import { provideMockActions } from "#ngrx/effects/testing";
import { Observable } from "rxjs";
import { TestScheduler } from "rxjs/testing";
import { FooEffects } from "./foo.effects";
import { FooService } from "../services/foo.service";
import * as fromFooActions from "../actions/foo.actions";
// ...
describe("FooEffects", () => {
let actions$: Observable<unknown>;
let testScheduler: TestScheduler; // <-- instance of the test scheduler
let effects: FooEffects;
let fooServiceMock: jasmine.SpyObj<FooService>;
beforeEach(() => {
// Initialize the TestScheduler instance passing a function to
// compare if two objects are equal
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
TestBed.configureTestingModule({
imports: [],
providers: [
FooEffects,
provideMockActions(() => actions$),
// Mock the service so that we can test if it was called
// and if the right data was sent
{
provide: FooService,
useValue: jasmine.createSpyObj("FooService", {
someMethod: jasmine.createSpy(),
}),
},
],
});
effects = TestBed.inject(FooEffects);
fooServiceMock = TestBed.inject(FooService);
});
describe("effectWithDelay$", () => {
it("should dispatch doSomethingSuccess after 5 seconds if success", () => {
const someDataMock = { someData: Math.random() * 100 };
const initialAction = fromFooActions.doSomething(someDataMock);
const expectedAction = fromFooActions.doSomethingSuccess();
testScheduler.run((helpers) => {
// When the code inside this callback is being executed, any operator
// that uses timers/AsyncScheduler (like delay, debounceTime, etc) will
// **automatically** use the TestScheduler instead, so that we have
// "virtual time". You do not need to pass the TestScheduler to them,
// like in the past.
// https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing
const { hot, cold, expectObservable } = helpers;
// Actions // -a-
// Service // -b|
// Results // 5s --c
// Actions
actions$ = hot("-a-", { a: initialAction });
// Service
fooServiceMock.someMethod.and.returnValue(cold("-b|", { b: null }));
// Results
expectObservable(effects.effectWithDelay$).toBe("5s --c", {
c: expectedAction,
});
});
// This needs to be outside of the run() callback
// since it's executed synchronously :O
expect(fooServiceMock.someMethod).toHaveBeenCalled();
expect(fooServiceMock.someMethod).toHaveBeenCalledTimes(1);
expect(fooServiceMock.someMethod).toHaveBeenCalledWith(someDataMock.someData);
});
});
});
Please notice that in the code I'm using expectObservable to test the effect using the "virtual time" from the TestScheduler instance.
you could use the done callback from jasmine
it('should dispatch action after 5 seconds', (done) => {
const resMock = 'resMock';
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
effects.continuePollingEntries$.subscribe((res)=>{
expect(res).toEqual(resMock);
done()
})
});
The second notation doesn't work with jasmine-marbles, use dashes instead:
const expected = cold('------b ', { b: completion });
You will need to do 3 things
1- Inside your beforeEach, you need to override the internal scheduler of RxJs as follows:
import { async } from 'rxjs/internal/scheduler/async';
import { cold, hot, getTestScheduler } from 'jasmine-marbles';
beforeEach(() => {.....
const testScheduler = getTestScheduler();
async.schedule = (work, delay, state) => testScheduler.schedule(work, delay, state);
})
2- Replace delay, with delayWhen as follows:
delayWhen(_x => (true ? interval(50) : of(undefined)))
3- Use frames, I am not really sure how to use seconds for this, so I used frames. Each frame is 10ms. So for example my delay above is 50ms and my frame is -b, so that is the expected 10 ms + I needed another 50ms so this equals extra 5 frames which was ------b so as follows:
const expected = cold('------b ', { b: outcome });

How to wait for request to be finished with axios-mock-adapter like it's possible with moxios?

I try to test a rendering of some content after fetching it from server.
I use Vue Test Utils but this is irrelevant.
In the created hook of the component the ajax call is made with axios. I register the axios-mock-adapter response and 'render' the component, the call is made and everything works fine but i have to use the moxios lib only to wait for request to be finished.
it('displays metrics', (done) => {
this.mock.onGet('/pl/metrics').reply((config) => {
let value = 0
if (config.params.start == '2020-01-26') {
value = 80
}
if (config.params.start == '2020-01-28') {
value = 100
}
return [200, {
metrics: [
{
key: "i18n-key",
type: "count",
value: value
}
]
}]
})
.onAny().reply(404)
let wrapper = mount(Dashboard)
moxios.wait(function() {
let text = wrapper.text()
expect(text).toContain('80')
expect(text).toContain('100')
expect(text).toContain('+20')
done()
})
})
Is it possible to get rid of moxios and achieve the same with axios-mock-adapter only?
Yes, you can implement your own flushPromises method with async/ await:
const flushPromises = () => new Promise(resolve => setTimeout(resolve))
it('displays metrics', async () => {
this.mock.onGet('/pl/metrics').reply((config) => {
// ..
}).onAny().reply(404)
let wrapper = mount(Dashboard)
await flushPromises()
expect(text).toContain('80')
})
Or use done and setTimeout:
it('displays metrics', (done) => {
this.mock.onGet('/pl/metrics').reply((config) => {
// ..
}).onAny().reply(404)
let wrapper = mount(Dashboard)
setTimeout(() => {
expect(text).toContain('80')
done()
})
})
moxiois.wait simply schedules a callback with setTimeout. This works because a task scheduled by setTimeout always runs after the microtask queue, like promise callbacks, is emptied.

How to test observable containing a debounce operator?

How does one write a Jasmine test to test an observable with the debounce operator? I've followed this blog post and understand the principles of how it should be tested, but it just doesn't seem to work.
Below is the factory that I am using to create the observable:
import Rx from "rx/dist/rx.all";
import DOMFactory from "../utils/dom-factory";
import usernameService from "./username.service";
function createUsernameComponent(config) {
const element = DOMFactory(config);
const username = Rx.Observable
.fromEvent(element.find('input'), 'input')
.pluck('target', 'value')
.startWith(config.value);
const isAvailable = username
.debounce(500)
.tap(() => console.info('I am never called!'))
.flatMapLatest(usernameService.isAvailable)
.startWith(false);
const usernameStream = Rx.Observable.combineLatest(username, isAvailable)
.map((results) => {
const [username, isAvailable] = results;
return isAvailable ? username : ''
})
.distinctUntilChanged();
return Object.freeze({
stream: usernameStream,
view: element
});
}
export default createUsernameComponent;
Note that tap operator is never called by the test. However, it will be executed properly if I run this code on the browser.
Below is my attempt at the test:
import Rx from "rx/dist/rx.all";
import Username from "./username.component";
import DataItemBuilder from "../../../test/js/utils/c+j-builders";
import usernameService from "./username.service"
describe('Username Component', () => {
let input, username;
beforeEach(() => {
const usernameConfig = DataItemBuilder.withName('foo')
.withPrompt('label').withType('text').build();
const usernameComponent = Username(usernameConfig);
usernameComponent.stream.subscribe(value => username = value);
input = usernameComponent.view.find('input');
});
it('should set to a valid username after debounce', () => {
const scheduler = injectTestSchedulerIntoDebounce();
scheduler.scheduleRelative(null, 1000, () => {
doKeyUpTest('abcddd', 'abcdd');
scheduler.stop();
});
scheduler.start();
scheduler.advanceTo(1000);
});
function injectTestSchedulerIntoDebounce() {
const originalOperator = Rx.Observable.prototype.debounce;
const scheduler = new Rx.TestScheduler();
spyOn(Rx.Observable.prototype, 'debounce').and.callFake((dueTime) => {
console.info('The mocked debounce is never called!');
if (typeof dueTime === 'number') {
return originalOperator.call(this, dueTime, scheduler);
}
return originalOperator.call(this, dueTime);
});
return scheduler;
}
function doKeyUpTest(inputValue, expectation) {
input.val(inputValue);
input.trigger('input');
expect(username).toBe(expectation);
}
});
When I run the test, the fake debounce never gets called. I plan to mock the username service once I can get past the debounce.
In your test code you are triggering the input event inside the scheduleRelative function. This doesn't work because you are advancing 1000ms before doing the change. The debouncer then waits 500ms to debounce the isAvailable call but you already stopped the scheduler so time is not advancing afterwards.
What you should do is: trigger the input event before advancing the scheduler time or even better in a scheduleRelative function for a time <= 500ms in a and then inside the scheduleRelative function for 1000ms you have to call the expect function with the expected output and then stop the scheduler.
It should look like this:
it('should set to a valid username after debounce', () => {
const scheduler = injectTestSchedulerIntoDebounce();
scheduler.scheduleRelative(null, 500, () => {
input.val(inputValue);
input.trigger('input');
});
scheduler.scheduleRelative(null, 1000, () => {
expect(username).toBe(expectation);
scheduler.stop();
});
scheduler.start();
scheduler.advanceTo(1000);
});
In addition to that I have better experience with scheduleAbsolute instead of scheduleRelative because it is less confusing.
As per Simon Jentsch's answer, below is the answer using scheduleAbsolute instead of scheduleRelative:
import Rx from "rx/dist/rx.all";
import Username from "./username.component";
import DataItemBuilder from "../../../test/js/utils/c+j-builders";
import usernameService from "./username.service"
describe('Username Component', () => {
let input, username, promiseHelper;
const scheduler = new Rx.TestScheduler(0);
beforeEach(() => {
spyOn(usernameService, 'isAvailable').and.callFake(() => {
return Rx.Observable.just(true);
});
});
beforeEach(() => {
const usernameConfig = DataItemBuilder.withName('foo')
.withPrompt('label').withType('text').build();
const usernameComponent = Username(usernameConfig, scheduler);
usernameComponent.stream.subscribe(value => username = value);
input = usernameComponent.view.find('input');
});
it('should set the username for valid input after debounce', (done) => {
doKeyUpTest('abcddd', '');
scheduler.scheduleAbsolute(null, 100, () => {
expect(usernameService.isAvailable).not.toHaveBeenCalled();
expect(username).toBe('');
});
scheduler.scheduleAbsolute(null, 1000, () => {
expect(usernameService.isAvailable).toHaveBeenCalled();
expect(username).toBe('abcddd');
scheduler.stop();
done();
});
scheduler.start();
});
function doKeyUpTest(inputValue, expectation) {
input.val(inputValue);
input.trigger('input');
expect(username).toBe(expectation);
}
});

Resources