How to unit test WebWorker code with jest? - jestjs

I have this simple webworker which does some polling:
import { fetchLatestResults } from '../backend/fetchLatestResults';
let polling = false;
let symbolIds: string[] = [];
const fetchLatest = async () => {
if (!symbolIds.length) {
return;
}
try {
const results = await fetchLatestResults(symbolIds);
self.postMessage(results);
} catch (e) {
// noop for now
}
};
if (!polling) {
polling = true;
fetchLatest();
setInterval(fetchLatest, 10 * 1000);
}
self.onmessage = (e: MessageEvent) => {
symbolIds = e.data;
fetchLatest();
};
How can I unit test this with jest? There is not a lot of code, business logic is extracted, but still there are few cases that I'd like to test, like that fetchLatestResults is not invoked until symbolIds are set, etc. ?
I could probably extract onmessage logic into a function, export it, import it on tests, and call it, but what about postMessage ?

Related

How test listener on my custom event emitter in node typescript

I'm trying to test a service that has a listener of the a custom Event Emitter in node with typescript and mocha, sinon.
My custom emmiter;
class PublishEmitter extends EventEmitter {
publish(id: string) {
this.emit('publish', id);
}
}
My service use case:
export default class PublishVehicle {
constructor(
private findVehicle: FindVehicle, // Service that contains find methods on repository
private updateVehicle: UpdateVehicle, // Service that contains update methods on repository
private logger: ILogger,
) {
this.producer = producer;
this.logger = logger;
}
listen() {
this.logger.log('debug', 'Creating listener on PublishEmitter');
this.publishListener = this.publishListener.bind(this);
pubsub.on('publish', this.publishListener);
}
/**
* Listener on PublishEmitter.
*
* #param event
*/
async publishListener(event: string) {
try {
const vehicle = await this.findVehicle.findById(event);
if (vehicle?.state === State.PENDING_PUBLISH) {
//
const input = { state: State.PUBLISH };
await this.updateVehicle.update(vehicle.id, input);
this.logger.log('debug', `Message sent at ${Date.now() - now} ms`);
}
this.logger.log('debug', `End Vehicle's Publish Event: ${event}`);
} catch (error) {
this.logger.log('error', {
message: `publishListener: ${event}`,
stackTrace: error,
});
}
}
}
and in my test file:
import chai from 'chai';
const { expect } = chai;
import sinon from 'sinon';
import { StubbedInstance, stubInterface } from 'ts-sinon';
import pubsub from './PublishEmitter';
describe('Use Case - Publish Vehicle', function () {
let mockRepository: MockVehicleRepository;
let publishVehicle: PublishVehicle;
let findVehicleUseCase: FindVehicle;
let updateVehicleUseCase: UpdateVehicle;
before(() => {
const logger = Logger.getInstance();
mockRepository = new MockVehicleRepository();
findVehicleUseCase = new FindVehicle(mockRepository, logger);
updateVehicleUseCase = new UpdateVehicle(mockRepository);
publishVehicle = new PublishVehicle(
findVehicleUseCase,
updateVehicleUseCase,
logger,
);
});
afterEach(() => {
// Restore the default sandbox here
sinon.restore();
});
it('Should emit event to publish vehicle', async () => {
const vehicle = { ... }; // dummy data
const stubFindById = sinon
.stub(mockRepository, 'findById')
.returns(Promise.resolve(vehicle));
const stubUpdate = sinon
.stub(mockRepository, 'update')
.returns(Promise.resolve(vehicle));
const spy = sinon.spy(publishVehicle, 'publishListener');
publishVehicle.listen();
pubsub.publish(vehicle.id);
expect(spy.calledOnce).to.be.true; // OK
expect(stubFindById.calledOnce).to.be.true; // Error (0 call)
expect(stubUpdate.calledOnce).to.be.true; // Error (0 call)
});
});
When I debug this test, indeed the methods are called but they seem to be executed after it has gone through the last expect lines.
The output:
1 failing
1) Use Case - Publish Vehicle
Should emit event to publish vehicle:
AssertionError: expected false to be true
+ expected - actual
-false
+true
UPDATE
Finally I was be able to solve my problem wrapping expect lines in setTimeout.
setTimeout(() => {
expect(spy.calledOnce).to.be.true; // OK
expect(stubFindById.calledOnce).to.be.true; // OK
expect(stubUpdate.calledOnce).to.be.true; // OK
done();
}, 0);

Fail jest test suite if console error count increases

We want to fail the build if more console errors are introduced. For example, let's say console.error was called 30 times in the whole test suite. If another error is introduced this will increase to 31, which we don't want. Is there a way to prevent this?
For one test suite it is possible with:
const spy = jest.spyOn(console, "error");
let count = 0;
afterEach(() => {
count += spy.mock.calls.length;
});
afterAll(() => {
if (count > 2) {
throw Error(`oops error count: ${count}`);
}
});
but it would be nice to have this globally defined.
We solved this in a slightly different way:
// src/utils/testUtils
let consoleErrorSpy;
export const spyOnConsoleError = () => {
consoleErrorSpy = jest.spyOn(console, "error");
};
/**
* We are using this to prevent the console errors from increasing.
* These are our preferences in order of priority:
* 1. Don't call this method
* 2. Call this method at the end of a specific test (eg. for an error that can't be solved)
* 3. Call this method in `afterEach` (eg. for an async error that can't be solved)
*/
export const catchConsoleErrors = ({ silenced = [] } = {}) => {
const alwaysSilencedErrors = [
'<bug from a 3rd party library>'
];
const forbiddenCalls = [];
const silencedCalls = [];
for (const call of consoleErrorSpy.mock.calls) {
if (
new RegExp([...alwaysSilencedErrors, ...silenced].join("|")).test(call)
) {
silencedCalls.push(call);
} else {
forbiddenCalls.push(call);
}
}
for (const silencedCall of silencedCalls) {
// eslint-disable-next-line no-console
console.log("SILENCED\n---\n" + silencedCall.join(",") + "\n---");
}
expect(forbiddenCalls).toHaveLength(0);
// We clear the mock here so nothing happens if the method is called again for the same test,
// which is the case when this method is called in a specific test (file)
// as it is also called in `afterEach` in setUpTests.js
consoleErrorSpy.mockClear();
};
// some test file
afterEach(() => {
catchConsoleErrors({
silenced: [
"Warning: Can't perform a React state update on an unmounted component.*"
]
});
});
// src/setupTests.js
spyOnConsoleError();
afterEach(() => {
catchConsoleErrors();
});

Is it possible to override the DEFAULT_TEARDOWN function inside exceptions-zone.ts?

I'm trying to create an application with NestJS framework and I'd like to check if a specific Service is in the application context but, the framework default behavior is exiting the process when it doesn't find the Service inside the context.
I've surrounded the get method with try...catch but it doesn't work because of the process.exit(1) execution.
private async loadConfigService(server: INestApplication): Promise<void> {
try {
this.configService = await server.get<ConfigService>(ConfigService, { strict: false });
} catch (error) {
this.logger.debug('Server does not have a config module loaded. Loading defaults...');
}
this.port = this.configService ? this.configService.port : DEFAULT_PORT;
this.environment = this.configService ? this.configService.environment : Environment[process.env.NODE_ENV] || Environment.DEVELOPMENT;
this.isProduction = this.configService ? this.configService.isProduction : false;
}
I'd like to catch the exception to manage the result instead of exiting the process.
Here's what I came up with:
import { NestFactory } from '#nestjs/core';
export class CustomNestFactory {
constructor() {}
public static create(module, serverOrOptions, options) {
const ob = NestFactory as any;
ob.__proto__.createExceptionZone = (receiver, prop) => {
return (...args) => {
const result = receiver[prop](...args);
return result;
};
};
return NestFactory.create(module, serverOrOptions, options);
}
}
Now, instead of using the default NestFactory I can use my CustomNestFactory
Dirty, but it works
Here is my code to solve same issue:
import { ExceptiinsZone } from '#nestjs/core/errors/exceptions-zone';
ExceptionsZone.run = callback => {
try {
callback();
} catch (e) {
ExceptionsZone.exceptionHandler.handle(e);
throw e;
}
};
You may need to override asyncRun() also for other method.

Puppeteer mock page request object

import { Page } from 'puppeteer/lib/Page';
export class MonitorRequestHelper {
public static monitorRequests(page: Page, on = false) {
if(on) {
page.on('request', req => {
if (['image', 'font', 'stylesheet'].includes(req.resourceType())) {
// Abort requests for images, fonts & stylesheets to increase page load speed.
req.abort();
} else {
req.continue();
}
});
} else {
return true;
}
}
}
I am trying to mock and spy the function to check if it got called at least once.
Also, it would be helpful if some explain me how to mock and spy event-emitter object.
The source code is available on https://github.com/Mukesh23singh/puppeteer-unit-testing
If you want to test that your logic in monitorRequests works, you need to pass in a fake Page object with an event emitter interface that produces a fake request that you can test on.
Something like:
import {spy} from 'sinon';
// Arrange
const fakePage = { on(type, cb) { this[type] = cb; } }; // "event emitter"
const fakeRequest = {
abort: sinon.spy(),
resourceType() { return 'image'; }
};
monitorRequests( fakePage, true );
// Act
// trigger fake request
fakePage['request'](fakeRequest);
// Assert
assert(fakeRequest.abort.called);

How to test setInterval on a method?

I have a class
class Dummy {
constructor() {
this.prop1 = null;
this.prop2 = null;
this.prop3 = setInterval(() => {
this.method1()
}, 1000);
}
method1() {
// Method logic
}
}
var dummyObject = new Dummy();
module.exports = dummyObject;
I'd like to write tests to verify that method1 is being invoked after every 1s.
Following is the test code
const dummyObject = require('./dummy.js');
describe("Test setInterval", function () {
it("Test setInterval", function () {
const clock = sinon.useFakeTimers();
const spy = sinon.spy(dummyObject, 'method1');
clock.tick(1001);
expect(spy.calledOnce).to.be.true;
clock.restore();
})
})
When I run the tests however, I get an error 'Expected false to equal to true' and on further digging I realized I am not able to spy on the method which is being called via setInterval.
Please share any thoughts on what I can do to test this scenario?
This is not going to work the way you want it to, because the method (method1) is already called when you require the module and hence there is no chance to spy it afterwards in your test.
I recommend to refactor your Module to export the class, not the instance like:
module.exports = class Dummy {
constructor() {
this.prop1 = null;
this.prop2 = null;
this.prop3 = setInterval(() => {
this.method1()
}, 1000);
}
method1() {
// Method logic
}
}
Then in you test, require the class and spy on the method before you instantiate it:
const sinon = require('sinon');
const Dummy = require('./dummy.js');
describe("Test setInterval", function () {
it("Test setInterval", function () {
const clock = sinon.useFakeTimers();
// Spy on the method using the class' prototype
const spy = sinon.spy(Dummy.prototype, 'method1');
// Initialize the class and make sure its `constructor` is called after you spied on the method
new Dummy();
clock.tick(1001);
expect(spy.calledOnce).to.be.true;
clock.restore();
})
})

Resources