How to Mock and test using an RxJS subject? - rxjs5

I have some functions that accept an RxJS subject (backed to a socket) that I want to test. I'd like to mock the subject in a very request reply fashion. Since I'm unsure of a clean Rx way to do this, I'm tempted to use an EventEmitter to form my fake socket.
Generally, I want to:
check that the message received on my "socket" matches expectations
respond to that message on the same subject: observer.next(resp)
I do need to be able to use data from the message to form the response as well.
The code being tested is
export function acquireKernelInfo(sock) {
// set up our JSON payload
const message = createMessage('kernel_info_request');
const obs = shell
.childOf(message)
.ofMessageType('kernel_info_reply')
.first()
.pluck('content', 'language_info')
.map(setLanguageInfo)
.publishReplay(1)
.refCount();
sock.next(message);
return obs;
}

You could manually create two subjects and "glue them together" as one Subject with Subject.create:
const sent = new Rx.Subject();
const received = new Rx.Subject();
const mockWebSocketSubject = Subject.create(sent, received)
const s1 = sent.subscribe(
(msg) => sentMsgs.push({ next: msg }),
(err) => sentMsgs.push({ error: err }),
() => sendMsgs.push({ complete: true })
);
const s2 = recieved.subscribe(
(msg) => sentMsgs.push({ next: msg }),
(err) => sentMsgs.push({ error: err }),
() => sendMsgs.push({ complete: true })
);
// to send a message
// (presumably whatever system you're injecting this into is doing the sending)
sent.next('weee');
// to mock a received message
received.next('blarg');
s1.unsubscribe();
s2.unsubscribe();
That said, it's really a matter of what you're testing, how it's structured, and what the API is.
Ideally you'd be able to run your whole test synchronously. If you can't for some Rx-related reason, you should look into the TestScheduler, which has facilities to run tests in virtualized time.

Related

Expecting a Promise *not* to complete, in Jest

I have the following need to test whether something does not happen.
While testing something like that may be worth a discussion (how long wait is long enough?), I hope there would exist a better way in Jest to integrate with test timeouts. So far, I haven't found one, but let's begin with the test.
test ('User information is not distributed to a project where the user is not a member', async () => {
// Write in 'userInfo' -> should NOT turn up in project 1.
//
await collection("userInfo").doc("xyz").set({ displayName: "blah", photoURL: "https://no-such.png" });
// (firebase-jest-testing 0.0.3-beta.3)
await expect( eventually("projects/1/userInfo/xyz", o => !!o, 800 /*ms*/) ).resolves.toBeUndefined();
// ideally:
//await expect(prom).not.toComplete; // ..but with cancelling such a promise
}, 9999 /*ms*/ );
The eventually returns a Promise and I'd like to check that:
within the test's normal timeout...
such a Promise does not complete (resolve or reject)
Jest provides .resolves and .rejects but nothing that would combine the two.
Can I create the anticipated .not.toComplete using some Jest extension mechanism?
Can I create a "run just before the test would time out" (with ability to make the test pass or fail) trigger?
I think the 2. suggestion might turn handy, and can create a feature request for such, but let's see what comments this gets..
Edit: There's a further complexity in that JS Promises cannot be cancelled from outside (but they can time out, from within).
I eventually solved this with a custom matcher:
/*
* test-fns/matchers/timesOut.js
*
* Usage:
* <<
* expect(prom).timesOut(500);
* <<
*/
import { expect } from '#jest/globals'
expect.extend({
async timesOut(prom, ms) { // (Promise of any, number) => { message: () => string, pass: boolean }
// Wait for either 'prom' to complete, or a timeout.
//
const [resolved,error] = await Promise.race([ prom, timeoutMs(ms) ])
.then(x => [x])
.catch(err => [undefined,err] );
const pass = (resolved === TIMED_OUT);
return pass ? {
message: () => `expected not to time out in ${ms}ms`,
pass: true
} : {
message: () => `expected to time out in ${ms}ms, but ${ error ? `rejected with ${error}`:`resolved with ${resolved}` }`,
pass: false
}
}
})
const timeoutMs = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); })
.then( _ => TIMED_OUT);
const TIMED_OUT = Symbol()
source
The good side is, this can be added to any Jest project.
The down side is, one needs to separately mention the delay (and guarantee Jest's time out does not happen before).
Makes the question's code become:
await expect( eventually("projects/1/userInfo/xyz") ).timesOut(300)
Note for Firebase users:
Jest does not exit to OS level if Firestore JS SDK client listeners are still active. You can prevent it by unsubscribing to them in afterAll - but this means keeping track of which listeners are alive and which not. The firebase-jest-testing library does this for you, under the hood. Also, this will eventually ;) get fixed by Firebase.

Are Socket.io Namespaces really a individual spaces?

I'm using namespaces to separate different customers. Using this code at the server side:
// server-side
const workspaces = io.of(/^\/\w+\/$/);
workspaces.on('connection', (socket) => {
socket.on('chat message', (msg) => {
workspaces.to(roomName).emit('chat message', msg)
})
})
And on the client side (each client is a different customer):
// client-side - customer01
const socket = io('ws://server:3000/customer01/')
// client-side - customer02
const socket = io('ws://server:3000/customer02/')
However, when customer01 emits something, customer02 receives the data and vice versa. The expected shouldn't be that every namespace client receives only the data it is 'address to'?
There was a mistake on my code, the correct is this one:
// server-side
workspaces.on('connection', (socket) => {
const specificWorkspace = socket.nsp;
console.log('someone is connected # workspace.name: ' + specificWorkspace.name)
specificWorkspace.to(roomName).emit('chat message', msg)
})
The old I haven't add the socket.nsp, so, I was emitting to the whole socket instead of the specific one.

Unit testing NestJS Observable Http Retry

I'm making a request to a 3rd party API via NestJS's built in HttpService. I'm trying to simulate a scenario where the initial call to one of this api's endpoints might return an empty array on the first try. I'd like to use RxJS's retryWhen to hit the api again after a delay of 1 second. I'm currently unable to get the unit test to mock the second response however:
it('Retries view account status if needed', (done) => {
jest.spyOn(httpService, 'post')
.mockReturnValueOnce(of(failView)) // mock gets stuck on returning this value
.mockReturnValueOnce(of(successfulView));
const accountId = '0812081208';
const batchNo = '39cba402-bfa9-424c-b265-1c98204df7ea';
const response =client.viewAccountStatus(accountId, batchNo);
response.subscribe(
data => {
expect(data[0].accountNo)
.toBe('0812081208');
expect(data[0].companyName)
.toBe('Some company name');
done();
},
)
});
My implementation is:
viewAccountStatus(accountId: string, batchNo: string): Observable<any> {
const verificationRequest = new VerificationRequest();
verificationRequest.accountNo = accountId;
verificationRequest.batchNo = batchNo;
this.logger.debug(`Calling 3rd party service with batchNo: ${batchNo}`);
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const response = this.httpService.post(url, verificationRequest, config)
.pipe(
map(res => {
console.log(res.data); // always empty
if (res.status >= 400) {
throw new HttpException(res.statusText, res.status);
}
if (!res.data.length) {
this.logger.debug('Response was empty');
throw new HttpException('Account not found', 404);
}
return res.data;
}),
retryWhen(errors => {
this.logger.debug(`Retrying accountId: ${accountId}`);
// It's entirely possible the first call will return an empty array
// So we retry with a backoff
return errors.pipe(
delayWhen(() => timer(1000)),
take(1),
);
}),
);
return response;
}
When logging from inside the initial map, I can see that the array is always empty. It's as if the second mocked value never happens. Perhaps I also have a solid misunderstanding of how observables work and I should somehow be trying to assert against the SECOND value that gets emitted? Regardless, when the observable retries, we should be seeing that second mocked value, right?
I'm also getting
: Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error:
On each run... so I'm guessing I'm not calling done() in the right place.
I think the problem is that retryWhen(notifier) will resubscribe to the same source when its notifier emits.
Meaning that if you have
new Observable(s => {
s.next(1);
s.next(2);
s.error(new Error('err!'));
}).pipe(
retryWhen(/* ... */)
)
The callback will be invoked every time the source is re-subscribed. In your example, it will call the logic which is responsible for sending the request, but it won't call the post method again.
The source could be thought of as the Observable's callback: s => { ... }.
What I think you'll have to do is to conditionally choose the source, based on whether the error took place or not.
Maybe you could use mockImplementation:
let hasErr = false;
jest.spyOn(httpService, 'post')
.mockImplementation(
() => hasErr ? of(successView) : (hasErr = true, of(failView))
)
Edit
I think the above does not do anything different, where's what I think mockImplementation should look like:
let err = false;
mockImplementation(
() => new Observable(s => {
if (err) {
s.next(success)
}
else {
err = true;
s.next(fail)
}
})
)

RxJS Pipe Not Called With zip

I'm zipping a three observables, each of the three observables has it's own "success" callback using .pipe(tap() => {...});. This works fine when all three observables execute without error, but if one of the observables errors out, then none of the tap methods execute. How can I have the tap methods always execute if that observable runs successfully?
var request1 = Observable.create(...); //Pretend this one will fail (though request2 or request3 could also fail)
var request2 = Observable.create(...);
var request3 = Observable.create(...);
request1.pipe(tap(() => {
//Unique success callback should always run if request1 succeeds, even if request2 or request 3 fails.
}));
request2.pipe(tap(() => {
//Unique success callback should always run if request2 succeeds, even if request1 or request 3 fails.
}));
request3.pipe(tap(() => {
//Unique success callback should always run if request3 succeeds, even if request1 or request 2 fails.
}));
var observable = zip(request1, request2, request3);
observable.subscribe(() => {
//Do something when all three execute successfully
});
I believe this is expected and the appropriate behavior for what you're dealing with. You probably want to look at using piping the catchError lettable into each of your requests and returning an empty observable.
request1.pipe(tap(() => {
//Unique success callback
}), catchError((err) => {
return empty();
}));
That way you handle the error of that observable without breaking the new zip.
#MichaelSolati is nearly correct, except
you should catch and return a value, e.g null if you wish to see output in subscribe() - because zip won't fire with empty()
your tap() callbacks are not in the zip() pipeline, they are separate branches which are not subscribed to, so are never activated.
Note that with changes to rxjs versions, the imports can get a bit tricky, for example zip is available as a function and as an operator.
With the rxjs.umd.js cdn used in the snippet below, I initially used the operator by mistake and it doesn't throw an error (but does not work).
I note from comments above you are doing this in an Angular context. If
you still have a problem, please post your full Angular module with imports and we can resolve the difficulty.
console.clear()
//console.log(rxjs)
// Get the operators and creators
const tap = rxjs.operators.tap
const empty = rxjs.empty
const zip = rxjs.zip
const catchError = rxjs.operators.catchError
const of = rxjs.of
const throwError = rxjs.throwError
//var request1 = of(1)
var request1 = throwError('error')
var request2 = of(2)
var request3 = of(3)
var req1 = request1.pipe(
tap(() => console.log('request1')),
catchError((err) => {
console.log('request1 has error')
return of(null)
})
);
var req2 = request2.pipe(
tap(() => { console.log('request2');})
);
var req3 = request3.pipe(
tap(() => { console.log('request3');})
);
var myObservable = rxjs.zip(req1, req2, req3);
myObservable.subscribe(
result => { console.log('result', result) }
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.0/rxjs.umd.js"></script>
Add catch block in your code where you can track the errors.
request1.pipe(tap(() => {
//Unique success callback I want to run if request2 succeeds.
//It should still run if request1 fails
}),
catchError(// Error occured)
);

Socket.io sends two messages

I'm trying to setup socket.io and here is part of my server.js
const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http, { path: '/websocket', origins:'*:*' });
io.on('connection', (socket) => {
socket.send('Hi');
socket.on('message', (message) => {
console.log(message);
socket.emit('hello', `New: ${message}`);
});
console.log('a user connected');
});
http.listen(3030, function(){
console.log('listening on *:3030');
});
and my simple client:
var socket = io('https://*******.com', {
secure: true,
path: '/websocket'
});
const input = document.getElementById('text');
const button = document.getElementById('button');
const msg = document.getElementById('msg');
button.onclick = () => {
socket.emit('message', input.value);
socket.on('hello', (text) => {
const el = document.createElement('p');
el.innerHTML = text;
msg.appendChild(el);
})
}
And if I'll click for third time I receive a 3 messages back and so on. What I'm doing wrong? I wish to send message to the server and receive modified message back.
I'm new in web sockets.
Any help appreciated.
P.S. socket.io v2.0.1
You are adding a socket.on() event handler each time the button is clicked. So, after the button has been clicked twice, you have duplicate socket.on() event handlers. When the event comes back, your two event handlers will each get called and you will think you are getting duplicate messages. Actually, it's just one message, but with duplicate event handlers.
You pretty much never want to add an event handler inside another event handler because that leads to this sort of build-up of duplicate event handlers. You don't describe (in words) exactly what you're code is trying to do so I don't know exactly what alternative to suggest. Usually, you set up the event handlers first, just once, when the socket is connected and then you will never get duplicate handlers.
So, perhaps it's as simple as changing this:
button.onclick = () => {
socket.emit('message', input.value);
socket.on('hello', (text) => {
const el = document.createElement('p');
el.innerHTML = text;
msg.appendChild(el);
})
}
to this:
button.onclick = () => {
socket.emit('message', input.value);
}
socket.on('hello', (text) => {
const el = document.createElement('p');
el.innerHTML = text;
msg.appendChild(el);
});
If you are using Angular and (probably) embedding the Socket in a Service (simpleton) you are creating a persistent listener in ngOnInit every time you load a page.
You need to create some kind of flag to know if the listener was already created in the Service from another instance of your page.

Resources