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

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();
}));

Related

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

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>

SequelizeDatabaseError: could not serialize access due to concurrent update

In Mocha test beforeEach hook, I am trying to destroy all table records.
import { db } from '../src/db/models';
export const truncateTable = () => {
const promises = Object.keys(db).map(key => {
if (key !== 'Sequelize' && key !== 'sequelize') {
console.log(key);
return db[key].destroy({ where: {} });
}
});
return Promise.all(promises);
};
Then in the test, I am doing this:
describe.only('application mutations', () => {
beforeEach(() => truncateTable());
...
The error I am getting:
SequelizeDatabaseError: could not serialize access due to concurrent
update
TL/DR: in your tests, if you want a quick way to delete models and reset your DB, use sync.
describe.only('application mutations', () => {
beforeEach(async () => {
await db.sync({force: true})
});
}
If you want to individually destroy your models, you must properly await for your promise to finish before initiating a new one. Currently, your promises are being initiated all at once, hence the Sequelize error.
export const truncateTable = async () => {
const promises = Object.keys(db).map(key => {
if (key !== 'Sequelize' && key !== 'sequelize') {
await db[key].destroy({ where: {} });
}
});
};
// in your test file
describe.only('application mutations', () => {
beforeEach(async () => {
await truncateTable();
});
})

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 cancel promises with bluebird

I think I misunderstand how promise cancellation with bluebird works. I wrote a test that demonstrates this. How do I make it green? Thanks:
describe('cancellation tests', () => {
function fakeFetch() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 100);
});
}
function awaitAndAddOne(p1) {
return p1.then(res => res + 1);
}
it('`cancel` cancels promises higher up the chain', () => {
const p1 = fakeFetch();
const p2 = awaitAndAddOne(p1);
expect(p2.isCancellable()).toBeTruthy();
p2.cancel();
return p2
.then(() => {
console.error('then');
})
.catch(err => {
console.error(err);
})
.finally(() => {
expect(p2.isCancelled()).toBeTruthy(); // Expected value to be truthy, instead received false
expect(p1.isCancelled()).toBeTruthy();
});
});
});
From here:
The cancellation feature is by default turned off, you can enable it using Promise.config.
Seems like you didn't enable the cancellation flag on the Promise itself:
Promise.config({
cancellation: true
});
describe(...
#Karen if correct. But the issue is that your test is also a bit wrong
If you look at the isCancellable method
Promise.prototype.isCancellable = function() {
return this.isPending() && !this.isCancelled();
};
This is just checking if the promise is pending and is not already cancelled. This doesn't mean then cancellation is enabled.
http://bluebirdjs.com/docs/api/cancellation.html
If you see the above url, it quotes below
The cancellation feature is by default turned off, you can enable it using Promise.config.
And if you look at the cancel method
Promise.prototype["break"] = Promise.prototype.cancel = function() {
if (!debug.cancellation()) return this._warn("cancellation is disabled");
Now if I update your test correct like below
var Promise = require("bluebird");
var expect = require("expect");
describe('cancellation tests', () => {
function fakeFetch() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 100);
});
}
function awaitAndAddOne(p1) {
return p1.then(res => res + 1);
}
it('`cancel` cancels promises higher up the chain', () => {
const p1 = fakeFetch();
const p2 = awaitAndAddOne(p1);
value = p2.isCancellable();
expect(p2.isCancellable()).toBeTruthy();
p2.cancel();
expect(p2.isCancelled()).toBeTruthy(); // Expected value to be truthy, instead received false
expect(p1.isCancelled()).toBeTruthy();
});
});
You can see that cancellation is not enable and it executes the warning code
The execution results fail as expected
spec.js:46
cancellation tests
spec.js:46
1) `cancel` cancels promises higher up the chain
spec.js:78
0 passing (3m)
base.js:354
1 failing
base.js:370
1) cancellation tests
base.js:257
`cancel` cancels promises higher up the chain:
Error: expect(received).toBeTruthy()
Expected value to be truthy, instead received
false
at Context.it (test/index.test.js:37:36)
Now if you update the code to enable cancellation
var Promise = require("bluebird");
var expect = require("expect");
Promise.config({
cancellation: true
});
describe('cancellation tests', () => {
function fakeFetch() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 100);
});
}
function awaitAndAddOne(p1) {
return p1.then(res => res + 1);
}
it('`cancel` cancels promises higher up the chain', () => {
const p1 = fakeFetch();
const p2 = awaitAndAddOne(p1);
value = p2.isCancellable();
expect(p2.isCancellable()).toBeTruthy();
p2.cancel();
expect(p2.isCancelled()).toBeTruthy(); // Expected value to be truthy, instead received false
expect(p1.isCancelled()).toBeTruthy();
});
});
It works!

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