How do I testing error manipulation in RxJS? - rxjs

I have a DB code that might throw a MongoError. I would like to catch it and manipulate it, transforming it into my own type of error (I don't want the MongoError to be exposed to other parts of my application). So I do it as follows:
// request = { deviceId: 2, name: 'Device' }
public create(request: DeviceCreationRequest): Observable<Device> {
return fromPromise(this.model.create(request)).pipe(
catchError(err =>
throwError({ code: 409, body: { msg: 'duplicated key', key: err.keyPattern } }))
map(DeviceRepository.toDevice));
It works fine in the endpoint, but I don't know how to test it.
it('create with duplicated key', done => {
const request = { deviceId: 2, name: 'Device' };
repository.create(request).pipe(
catchError(err => {
console.log(err); // shows the original MongoDB error
return of(undefined);
}),
flatMap(_ => repository.getAll()))
.subscribe(devices => {/* some assertions */});
});
Looks like my test catchError overwrites the old one (or something). And if I don't do it, the test case will error out.

Related

Marble-Testing doesn't use filter operator

EDIT
I boiled down the problem. The following code yields an error in tests, but works as expected in the browser (see https://github.com/prumand/jest-marbles-merge-map and https://github.com/ReactiveX/rxjs/issues/4837)
tests: returns a WE_FINISH
browser (expected): MY_NEW_ERROR
// code
export default function basicMergeMapObs(
action$: Observable<Action>
) : Observable<any> {
return action$.pipe(
filter((val: Action) => {
throw new Error('We stop here')
}),
map((val: Action) => ({
type: 'WE_FINISH',
})),
catchError(() => of({
type: 'MY_NEW_ERROR',
}))
)
}
// test
it('should yield an MY_ERROR', () => {
const source = of({
type: 'TEST',
status: 'NEW'
})
getScheduler().run(helpers => {
const { expectObservable, cold } = helpers
expectObservable(
basicMergeMapObs(
source
)
).toBe(
'(t|)',
{
t: { type: 'MY_NEW_ERROR' }
}
)
})
})
function getScheduler() {
return new TestScheduler((actual, expected) => {
expect(actual).toMatchObject(expected);
});
}
UPDATE 19.06.2019
I added cartants example from the given github issue, which works fine. Still my example fails. No idea why. IMO it should always throw an error.
And yet another update, the tests don't fail on linux, but only on my windows machine
UPDATE 02.07.2019
:O seemed to be a issue with endpoint-security solution we use ...

Writing Structural Expectations with Jest

I am looking to write what I am calling structural expectations with Jest and I am not sure how this could be accomplished.
To start I have a graphql server and a database with a number of todo items. I currently have the following test that just returns true if the content within the database is the same as the response that I have written. I want to check instead that the response looks like an object with data that could be anything.
Here is the code that I have:
describe('To Do:', () => {
it('add todo items', async () => {
const response = await axios.post('http://localhost:5000/graphql', {
query: `
query {
getTodoItems {
message
id
dateCreated
dateDue
}
}
`
});
const { data } = response;
expect(data).toMatchObject({
data: {
getTodoItems: [
{
message: "message",
id: "5bd9aec8406e0a2170e04494",
dateCreated: "1540992712052",
dateDue: "1111111111"
},
{
message: "message",
id: "5bd9aeec60a9b2579882a308",
dateCreated: "1540992748028",
dateDue: "1111111111"
},
{
message: "new message",
id: "5bd9af15922b27236c91837c",
dateCreated: "1540992789836",
dateDue: "1111111111"
}
]
}
})
});
});
Now I want to write something like this, where there can be any number of returned items and they follow similar structuring:
describe('To Do:', () => {
it('add todo items', async () => {
const response = await axios.post('http://localhost:5000/graphql', {
query: `
query {
getTodoItems {
message
id
dateCreated
dateDue
}
}
`
});
const { data } = response;
expect(data).toMatchObject({
data: {
getTodoItems: [
{
message: expect.any(String),
id: expect.any(String),
dateCreated: expect.any(String),
dateDue: expect.any(String)
} // There needs to be unlimited additional items here
]
}
})
});
});
I have been looking throught the docs and I even tried nesting the expectations but I can't seem to get the desired response. Let me know what yo think or if I can clarify in any way.
I figured out the best way for me to do it. I would love to hear better answers. I wrote a function within the scope of the test as a jest.fn and then I called it. In that function, I made custom checks to parse the data that was received in the response. From there I added an expect function with the 'toHaveReturnedWith' method to see what the response of my custom function was and finishing out the test.
const addTodoResponse = jest.fn(() => {
// Custom parsing and check here
// Returns true or false
});
addTodoResponse();
expect(addTodoResponse).toHaveReturnedWith(true);
Are there better ways to do this out there?

How can I catch RelayObervable Unhandled error when graphql errors are returned?

My backend is returning the following response:
const errorMessage = {
errors: [
{
message: 'User is logged out',
},
],
data: null,
};
return res.status(200).json(errorMessage);
My react-native app using relay is returning the following error:
RelayObservable: Unhandled Error Error:
Relay request for `HomeQuery` failed by the following reasons:
This error shows up when I try to query the backend and it returns the above 'errorMessage' graphql errors array. I have no way of catching this error in my relay network layer BEFORE it throws the redscreen. My Relay Environment looks like this:
const network = new RelayNetworkLayer([
urlMiddleware({
url: () => Promise.resolve(`${config.endpoint}backend`),
headers: async () => authenticateHeaders(fetchHeaders),
}),
retryMiddleware({ fetchTimeout: 1000 }),
next => req => {
if (!req.uploadbles) {
req.Accept = 'application/json';
req['Content-Type'] = 'application/json';
}
return next(req);
},
next => async req => {
const res = await next(req);
if (isLoggedOut(res)) {
// Using react-navigation, route to the login screen
dispatch(routeToLogin())
// I also tried returning `dispatch(routeToLogin())` and `{ data: null, errors: res.payload.errors }` without luck
}
return res;
}
]);
Is there a way I can navigate using dispatch(routeToLogin()) without seeing the redscreen error when graphql errors are returned?
Edit 1
For react-relay-network-modern: You can add the noThrow option like so: new RelayNetworkLayer(middlewares, { noThrow: true });
The noThrow option doesn't exist for react-relay-network-layer (relay classic), is there anything I can do to work around it?
Please try noThrow option:
const network = new RelayNetworkLayer(middlewares, { noThrow: true });

Why Doesn't This Bit of Code Work Without My Hack?

I have a situation where I'm making an API call to a Laravel back-end and eager-loading some data. The data is a collection of locations, each of the form:
location = {
id: ..
name: ...
...
docks: [
{
id: ...
name: ...
},
...
]
}
The end result of this epic should be to dispatch 2 actions, one with a list of the locations, and another with the list of all the associated docks
Taking it from the response to the api call, the following throws a Typescript error and won't compile:
.mergeMap(({ response }) => {
if (response.messages) {
return Observable.of(GetLocationsForReceiverFailedAction(response.messages))
}
const locations = response.locations.map((locn: any) => ({
id: locn.id,
receiver_id: locn.receiver_id,
name: locn.name,
address: locn.address,
city: locn.city,
state: locn.state,
zip: locn.zip
}))
const location$ = Observable.of(GetLocationsForReceiverSuccessAction(locations))
const docks: any[] = []
response.locations.forEach((locn: any) => {
locn.docks.forEach((dk: any) => {
docks.push(dk)
})
})
const docks$ = Observable.of(GetDocksForReceiverSuccessAction(docks))
return Observable.merge(location$, docks$)
})
.catch(() =>
Observable.of(
GetLocationsForReceiverFailedAction([
'Something went wrong trying to reach the server'
])
)
)
The error reads:
"Argument of type '({ response }: AjaxResponse) => Observable<{ type: ActionTypes; messages: string[]; }> | Observab...' is not assignable to parameter of type '(value: AjaxResponse, index: number) => ObservableInput<{ type: ActionTypes; messages: string[]; }>'."
If, however, I change the line:
return Observable.merge(location$, docks$)
to:
return Observable.merge(location$, Observable.empty(), docks$)
(ie, separating by 2 observables by an empty observable in the merge (or concat, which works the same way here), it compiles and runs.
What am I not understanding here?

Testing ngrx Effects with Jasmine spy

I am writing an ngrx effect and trying to test it. However, the effect calls a service that calls an API that will require authentication. As a result, I am trying to create a spy in Jasmine to handle returning the data. This is my first time using ngrx effects, so I am really unsure where to put different parts of the code. Nothing I have done is allowing this test to run correctly.
The effect is a very simple one as follows:
#Effect() itemSelected: Observable<Action> = this.d.pessimisticUpdate('ITEM_SELECTED', {
run: (action: ItemSelected) => {
return this.myService.getItemById(action.payload).map((res) => ({
type: 'ITEM_INFO_RETURNED',
payload: res
}));
},
onError: (a: ItemSelected, error) => {
console.error('Error', error);
}
});
constructor(private d: DataPersistence<ItemState>, private myService: MyService) {
// add auth headers here
}
My test is currently written as follows:
describe('ItemEffects', () => {
let actions: Observable<any>;
let effects: ItemEffects;
let myService = jasmine.createSpyObj('MyService', ['getItemById']);
let item1: Item = {id: 1, name: 'Item 1'};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({}),
],
providers: [
ItemEffects,
DataPersistence,
provideMockActions(() => actions),
{
provide: MyService,
useValue: myService
}
],
});
effects = TestBed.get(ItemEffects);
});
describe('someEffect', () => {
it('should work', async () => {
myService.getItemById.and.callFake(function (id) {
return items.find((r) => r.id === id);
});
actions = hot('-a-|', { a:{ type:'ITEM_INFO_RETURNED', payload:1}});
expect(effects.itemSelected).toEqual(
{ type: 'ITEM_INFO_RETURNED', payload: { item1 } }
);
});
});
});
This is still attempting to use the production MyService (requiring authentication). If I move the myService override out of the provider and into the actual test,
TestBed.overrideProvider(MyService, { useValue: myService });
I get an error that it cannot read the property "itemSelected" of undefined, which would be when I am calling the effects at the very end of the test.
I am really new to ngrx, as well as to TestBed. Is there somewhere else I should be defining this Jasmine spy? Should I be using something other than createSpyOn for this?
Thanks in advance!

Resources