When you subscribe with rxjs, how do you signal to your test if it fails? - rxjs

I am a complete beginner.
The issue I am having is that once I throw an error in rxjs observable, my test doesn't know about it. When I am subscribing in a test, and it fails within rxjs it just throws an error and I need to notify my test that the error occurred. Here's a more simple example that shows that "test failed" is never printed.
import { sample } from "rxjs/operators";
const source = interval(1000);
// sample last emitted value from source every 2s
// output: 2..4..6..8..
const example = source.pipe(sample(interval(2000)));
async function test_runner() {
setup();
try {
await test();
console.log("test succeeded");
} catch (e) {
console.log("test failed");
}
}
async function setup() {
console.log("setup");
const subscribe = example.subscribe((val) => {
console.log(val);
if (val === 4) { throw Error("error!"); }
});
}
async function test() {
console.log("test");
await waitMs(10000);
}
test_runner();
async function waitMs(waitTime: number): Promise<void> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, waitTime);
});
}
Is there a way to handle this? I appreciate any help.

If you want to test rx streams one of the best ways is to use marbles diagram.
That's what ngrx uses for effects testing.
https://www.npmjs.com/package/jasmine-marbles
https://github.com/ngrx/platform/blob/master/docs/effects/testing.md
With marbles diagram you can write style where you expect emit / error and to assert it.
For example syntax hot('---#') means that after 30ms there's an error in the stream.

When you subscribe you can pass functions to:
process items emitted by the stream
process an error
process a completion signal
You can use that in your tests too:
describe('when a stream emits an error', () => {
it('should call your error handler', () => {
const stream$ = throwError('wat?!');
stream$.subscribe({ error: (err) => {
chai.expect(err === 'wat?!').to.be.true;
}});
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.5/rxjs.umd.min.js"></script>
<script src="https://unpkg.com/chai/chai.js"></script>
<script src="https://unpkg.com/mocha/mocha.js"></script>
<script>const {throwError} = rxjs;</script>
<div id="mocha"></div>
<script class="mocha-init">mocha.setup('bdd');</script>
<script class="mocha-exec">mocha.run();</script>

Related

Why does this little rxjs-code-snippet using the concat-operator throw an error?

Preconditions:
The ref.getDownload() returns an Observable which only can be subscribed, if the
task.snapshotChanges()-Observable completed.
This code-snippet works:
task.snapshotChanges().subscribe({
complete: () => {
ref.getDownloadURL().subscribe((downloadUrl) => console.log(downloadUrl));
}
});
This code-snippet does NOT work:
concat(
task.snapshotChanges(),
ref.getDownloadURL()
).pipe(
last()
).subscribe((downloadUrl) => console.log(downloadUrl));
getDownloadUrl throws an error (404 file not found), because it seems
ref.getDownloadUrl is subscribed to early.
Why subscribes the ref.getDownloaded()-Observable and does not wait until task.snapshotChanges() completes? The concat-operator should ensure this behaviour.
Or am I wrong?
The function ref.getDownloadURL() is called when the concat(..) Observable is created. See:
const { of, concat } = rxjs;
const { delay } = rxjs.operators;
const fetch1 = () => { console.log('run fetch1'); return of('from 1').pipe(delay(2000)) }
const fetch2 = () => { console.log('run fetch2'); return of('from 2').pipe(delay(2000)) }
concat(fetch1(), fetch2()).subscribe(console.log);
<script src="https://unpkg.com/rxjs/bundles/rxjs.umd.min.js"></script>
ref.getDownloadURL() seems to query the database directly when it gets called and not when the Observable it returns gets subscribed to.
You can wrap ref.getDownloadURL() with defer to only execute it when the Observable is subscribed to.
const { of, concat, defer } = rxjs;
const { delay } = rxjs.operators;
const fetch1 = () => { console.log('run fetch1'); return of('from 1').pipe(delay(2000)) }
const fetch2 = () => { console.log('run fetch2'); return of('from 2').pipe(delay(2000)) }
concat(fetch1(), defer(() => fetch2())).subscribe(console.log);
<script src="https://unpkg.com/rxjs/bundles/rxjs.umd.min.js"></script>
Also see my answer here https://stackoverflow.com/a/57671521/9423231

How to spyOn a function which has an Promise inside and don't return the result but handles the response itself?

How can I spyOn a method called placed in the object?
// Game.js
export default {
mine: null,
handle: function(me) {
console.log(" FOOOOO " + me)
},
setSource: function() {
this.mine.getSource().then((response) => {
const {source} = response
this.handle(source)
})
}
}
Here i try to spy:
// GameSpec.js
import Game from '../../lib/jasmine_examples/Game'
Game.mine = {}
describe("Game", function() {
it("should set source and handle it", function() {
Game.mine.getSource = () => {
return new Promise((resolve)=>{
resolve( {
source : 'BAAAAR'
})
})
}
spyOn(Game, 'handle').and.callThrough()
Game.setSource()
expect(Game.handle).toHaveBeenCalled()
});
});
In the output you can see the function "handle" was called:
Started
F FOOOOO BAAAAR
Failures:
1) Game should set source and handle it
Message:
Expected spy handle to have been called.
Stack:
Error: Expected spy handle to have been called.
at <Jasmine>
at UserContext.<anonymous> (/Users/silverbook/Sites/zTest/jasmine/spec/jasmine_examples/PlayerSpec.js:20:29)
at <Jasmine>
1 spec, 1 failure
But jasmine says it was not called.
If i remove the mocked Promise the test passes but i needed there. In another test i will return an error in the Promise and let it handle from another function.
So the Promise breaks the test but why?
The test executes synchronously and the expect fails before the callback queued by this.mine.getSource().then() has a chance to execute.
For Jasmine >= 2.7 and async function support you can convert your test function into an async function and add an await Promise.resolve(); where you want to pause the synchronous test and let any queued callbacks execute.
For your test it would look like this:
import Game from '../../lib/jasmine_examples/Game'
Game.mine = {}
describe("Game", function() {
it("should set source and handle it", async () => {
Game.mine.getSource = () => {
return new Promise((resolve)=>{
resolve( {
source : 'BAAAAR'
})
})
}
spyOn(Game, 'handle').and.callThrough();
Game.setSource();
await Promise.resolve(); // let the event loop cycle
expect(Game.handle).toHaveBeenCalled();
});
});
For older (>= 2.0) Jasmine versions you can use done() like this:
import Game from '../../lib/jasmine_examples/Game'
Game.mine = {}
describe("Game", function() {
it("should set source and handle it", (done) => {
Game.mine.getSource = () => {
return new Promise((resolve)=>{
resolve( {
source : 'BAAAAR'
})
})
}
spyOn(Game, 'handle').and.callThrough();
Game.setSource();
Promise.resolve().then(() => {
expect(Game.handle).toHaveBeenCalled();
done();
});
});
});
You can run the test inside fakeAsync and run tick() before the expect()
Service:
getFirebaseDoc() {
this.db.firestore.doc('some-doc').get()
.then(this.getFirebaseDocThen)
.catch(this.getFirebaseDocCatch);
}
Unit testing:
it('should call getFirebaseDocThen', fakeAsync(() => { // note `fakeAsync`
spyOn(service, 'getFirebaseDocThen');
spyOn(service.db.firestore, 'doc').and.returnValue({
get: (): any => {
return new Promise((resolve: any, reject: any): any => {
return resolve({ exists: true });
});
},
});
service.getFirebaseDoc();
tick(); // note `tick()`
expect(service.getFirebaseDocThen).toHaveBeenCalled();
}));

What the unexpected behavior Observable RxJS with async functions and toPromise?

When i use only subscribe-method, it works truthy, but with this code - i don't understand the result.
const Observable = require("rxjs").Observable;
let i = 0;
const x = new Observable((o) => {
setInterval(() => o.next(++i), 1000);
});
(async () => {
while (true) {
try {
console.log("loop");
console.log("value", await x.toPromise());
} catch (e) {
console.log(e);
}
}
})();
x.subscribe((value) => {
console.log("subscribe", value);
});
This code result is:
loop
subscribe 2
subscribe 4
subscribe 6
subscribe 8
subscribe 10
subscribe 12
subscribe 14
What's happened?
It works same with this variant of using toPromise
function a() {
x.toPromise().then((value) => {
console.log("promise", value);
a();
}).catch((e) => {
console.log("error", value);
});
}
a();
toPromise() is executed on an Observable on its completion. Since your observable is never actually completed, it does not execute. Use take(1) to force it to emit value before the completion of the observable.
const Observable = require("rxjs").Observable;
let i = 0;
const x = new Observable((o) => {
setInterval(() => o.next(++i), 1000);
});
(async () => {
while (true) {
try {
console.log("loop");
console.log("value", await x.take(1).toPromise());//here
} catch (e) {
console.log(e);
}
}
})();
x.subscribe((value) => {
console.log("subscribe", value);
});
Output:
loop
subscribe 2
value 1
loop
subscribe 4
value 5
loop
subscribe 7
value 9
loop
subscribe 11
value 14
As for the values:
take() will complete once atleast one value is emitted regardless of whether the source observable completes. So it really depends on what value the observable is emitting the next time the toPromise() is called

Using RxJS Observable.from with an object that implements the observable interface

Based on the docs for RxJS's Observable.from(), it sounds like you should be able to pass it an object that implements the observable interface. However, the following
const observable = {
subscribe(observer) {
const subscription = someAsyncProcess(res => observer.next(res));
return {
unsubscribe() {
subscription.unsubscribe();
}
}
}
};
Rx.Observable.from(observable)
.subscribe({
next(res) {
console.log(res);
}
});
throws the error
Uncaught TypeError: object is not observable
Is my observable implementation incorrect? Or am I misunderstanding from?
Note: this is more of an academic question about the Observable interface--I realize Observable.create() would work in the above situation.
You can "trick" RxJS into thinking that the object you're passing it is a real Observable by implementing a "symbol function" (I don't know what is the proper name for this). However, you probably never need to do this in practise and it's better to use Observable.create.
const Rx = require('rxjs/Rx');
const Symbol_observable = Rx.Symbol.observable;
const Observable = Rx.Observable;
const observable = {
[Symbol_observable]: function() {
return this;
},
subscribe: function(observer) {
// const subscription = someAsyncProcess(res => observer.next(res));
observer.next(42);
return {
unsubscribe() {
subscription.unsubscribe();
}
}
}
};
Observable.from(observable)
.subscribe({
next(res) {
console.log('Next:', res);
}
});
This prints:
Next: 42
You can use Observable.from if it is an array of events or Observable.of if it is a simple object. It doesn't have to be implementing any interface. The code below is printing a in the console.
Rx.Observable.from("a").subscribe(data=> console.log(data));

How to run Promise test in Jasmine

I'm trying to test a promise in a separate library I injected to my app.
function myFunc(input) {
return new Promise(function(resolve, reject) {
···
resolve(value); // success
···
reject(error); // failure
});
};
This is my function that returns a Promise.
I would seriously love to run the test in jasmine like this
describe('Service: myService', function () {
var $log;
beforeEach(inject(function (_$log_) {
$log = _$log_;
}));
it('should get results', function () {
$log.log("start test");
var self = this;
myFunc(input).then(function(response) {
$log.log("success");
expect(response).toBe("response");
done();
}).catch(function(error) {
$log.log("fail");
self.fail(error);
done();
});
$log.log("end test");
});
});
My test passes(not expected.) and the only thing in my log is [start test] and [end test] as if the promise is totally ignored.
Since I'm not using $q for the promise, most jasmine tips angular doesn't seem to be helpful.
Any ideas on how to get into that 'then'?
Thanks
I could be wrong here, but this is most likely because you are attempting to test an asynchronous process here. So essentially what is happening, is that you call the function, but the test continues running and finishes before the promise ever returns, which is why the test succeeds.
One way to work around this (this is more like a hack, I'm sure there is a better way to do this, and if I find it, I will edit this post) is to add this bit of code at the end of your test:
describe("baseline test",function(){
it("baseline",function(done){
setTimeout(function(){
expect(1).toEqual(1);
done();
},1000);
});
});
Essentially what this snippet does is just set a timeout that waits for 1 second for any asynchronous calls to call back. If 1 second isn't enough, you can always increase that 1000 (which is in milliseconds). If this doesn't work, maybe look into adding another test suite that won't complete until the promises return.
TypeScipt code (Angular 8) to be tested
import { Injectable } from '#angular/core';
import { KeycloakService } from 'keycloak-angular';
import { environment } from 'environments/environment';
import { LoggedInUserHelperService } from 'shared/helper/logged-in-user-helper.service';
#Injectable({
providedIn: 'root'
})
export class AppInitializationService {
constructor(public keycloakService: KeycloakService) {
}
initApplication(): Promise<any> {
return new Promise<any>(
async (resolve: any, reject: any): Promise<any> => {
await this.initKeycloak()
.then(() => keyCloakInitialized = true)
.catch((error: Error) => {
console.error(`Couldn\'t initialize Keycloak Service. (Error: ${error})`);
reject(error);
return;
});
resolve();
}
);
}
private async initKeycloak(): Promise<any> {
return this.keycloakService.init({
config: environment.keycloak,
initOptions: {
onLoad: 'login-required',
checkLoginIframe: false,
promiseType: 'legacy'
},
enableBearerInterceptor: true,
bearerExcludedUrls: ['/assets']
});
}
}
Tests
import { TestBed } from '#angular/core/testing';
import { AppInitializationService } from 'app/app-initialization.service';
import { KeycloakService } from 'keycloak-angular';
describe('AppInitializationService', () => {
let testObj: AppInitializationService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
KeycloakService
]
});
testObj = TestBed.get(AppInitializationService);
});
it('should call keycloak service init', async () => {
const spy = spyOn(testObj.keycloakService, 'init').and.returnValue(Promise.resolve(true));
await testObj.initApplication();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith({
config: environment.keycloak,
initOptions: {
onLoad: 'login-required',
checkLoginIframe: false,
promiseType: 'legacy'
},
enableBearerInterceptor: true,
bearerExcludedUrls: ['/assets']
});
});
it('should log error on failed keycloak initialization', async () => {
const errorMsg = 'error-msg';
const spy1 = spyOn(testObj.keycloakService, 'init').and.callFake(
() => Promise.reject(errorMsg));
const spy2 = spyOn(console, 'error');
await testObj.initApplication().catch(() => { return; });
expect(spy1).toHaveBeenCalledTimes(1);
expect(spy2).toHaveBeenCalledTimes(1);
expect(spy2).toHaveBeenCalledWith(`Couldn\'t initialize Keycloak Service. (Error: ${errorMsg})`);
});
});

Resources