Mock Class Constructor being extended in Jest - javascript

I am receiving An argument for 'options' was not provided. when I try to mock a class with a constructor.
Color.test.ts
...
describe('TEST getColorById', () => {
describe('GIVEN color id', () => {
// mockColor will return orange as color
const mockRoot = mockColor({
id: 1,
});
const mockReturn = { id: 1, color: 'orange' };
class MockColor extends Color {
// overrides the response of post for await colorClass.getColorById with the expected mockReturn
public async post(): Promise<any> {
return { response: [mockReturn] };
}
}
// ? how can I simulate instantiating class with constructor
const colorClass = new MockColor();
describe('WHEN calling getColorById', () => {
test('THEN it loads color info', async () => {
const received = await colorClass.getColorById({ id: 1 });
expect(received).toEqual([mockRoot]);
});
});
});
});
Color.ts
...
export class Color {
private token: string;
private baseUrl: string;
constructor(options: { token; baseUrl? }) {
super(options);
this.token = options.token;
this.baseUrl = options.baseUrl;
}
async getColorById(object: { id: number }): Promise<ColorRoot> {
...
}
}

Related

Jest Making Test Await Promise Resolve

I have the following code I am working with this API https://developer.mozilla.org/en-US/docs/Web/API/Performance/measureUserAgentSpecificMemory. The class is stripped right back
export class Memory {
private stopped = false
private isUserAgentSpecificMemorySupported = true
public memoryData: any = []
constructor() {}
public startMonitoring(): () => void {
if (this.isUserAgentSpecificMemorySupported) {
this.scheduleMeasurement()
}
return () => {
this.stopped = true
}
}
private async performMeasurement(): Promise<void> {
const memory = await (window.performance as any).measureUserAgentSpecificMemory()
const type = memory.breakdown.filter((e: any) => e.types.includes('JavaScript'))
this.memoryData.push(type[0].bytes)
}
}
Jest file.
import {Memory} from './memory'
type UserAgentSpecificMemoryBreakdown = {
bytes: number
types: Array<string>
}
type UserAgentSpecificMemory = {
bytes: number
breakdown: Array<UserAgentSpecificMemoryBreakdown>
}
type MockWindow = {
crossOriginIsolated?: boolean
performance: {
measureUserAgentSpecificMemory?: () => Promise<UserAgentSpecificMemory>
}
}
const data = {
bytes: 1500,
breakdown: [
{
bytes: 1000000,
types: ['JavaScript'],
},
{
bytes: 0,
types: ['DOM'],
},
],
}
describe('Test Memory Class', () => {
let mockWindow: MockWindow
let windowSpy: jest.SpyInstance
beforeEach(() => {
windowSpy = jest.spyOn(window, 'window', 'get')
mockWindow = {
...window,
performance: {
measureUserAgentSpecificMemory: jest.fn(() => Promise.resolve(data)),
},
}
windowSpy.mockImplementation(() => mockWindow)
})
afterEach(() => {
windowSpy.mockRestore()
})
it('should measure User Agent Specific Memory', async () => {
let memory = new Memory()
memory.startMonitoring()
expect(memory.memoryData).toEqual([1000000])
})
})
I am not sure how to make the test file await for the value in the test?
Any help would be great.
window is an object and if it doesn’t contain window function, you can not spy on it.
For your production code, just mock measureUserAgentSpecificMemory function are enough:
import { Memory } from './memory'
describe('Memory', () => {
const data = {
bytes: 1500,
breakdown: [
{
bytes: 1000000,
types: ['JavaScript'],
},
{
bytes: 0,
types: ['DOM'],
},
],
};
let memory: Memory;
let measureUserAgentSpecificMemory: jest.Mock;
beforeEach(() => {
measureUserAgentSpecificMemory = jest.fn().mockResolvedValue(data);
(window as any).performance = {
measureUserAgentSpecificMemory,
};
memory = new Memory();
});
it('should measure User Agent Specific Memory', async () => {
memory.startMonitoring();
expect(memory.memoryData).toEqual([1000000]);
expect(measureUserAgentSpecificMemory).toHaveBeenCalled();
});
});

Unable to mock a class method in Javascript/Typescript

I am not getting any clue how to mock a method. I have to write a unit test for this function:
index.ts
export async function getTenantExemptionNotes(platform: string) {
return Promise.all([(await getCosmosDbInstance()).getNotes(platform)])
.then(([notes]) => {
return notes;
})
.catch((error) => {
return Promise.reject(error);
});
}
api/CosmosDBAccess.ts
import { Container, CosmosClient, SqlQuerySpec } from "#azure/cosmos";
import { cosmosdbConfig } from "config/Config";
import { Workload } from "config/PlatformConfig";
import { fetchSecret } from "./FetchSecrets";
export class CosmoDbAccess {
private static instance: CosmoDbAccess;
private container: Container;
private constructor(client: CosmosClient) {
this.container = client
.database(cosmosdbConfig.database)
.container(cosmosdbConfig.container);
}
static async getInstance() {
if (!CosmoDbAccess.instance) {
try {
const connectionString = await fetchSecret(
"CosmosDbConnectionString"
);
const client: CosmosClient = new CosmosClient(connectionString);
// Deleting to avoid error: Refused to set unsafe header "user-agent"
delete client["clientContext"].globalEndpointManager.options
.defaultHeaders["User-Agent"];
CosmoDbAccess.instance = new CosmoDbAccess(client);
return CosmoDbAccess.instance;
} catch (error) {
// todo - send to app insights
}
}
return CosmoDbAccess.instance;
}
public async getAllNotesForLastSixMonths() {
const querySpec: SqlQuerySpec = {
// Getting data from past 6 months
query: `SELECT * FROM c
WHERE (udf.convertToDate(c["Date"]) > DateTimeAdd("MM", -6, GetCurrentDateTime()))
AND c.IsArchived != true
ORDER BY c.Date DESC`,
parameters: [],
};
const query = this.container.items.query(querySpec);
const response = await query.fetchAll();
return response.resources;
}
}
export const getCosmosDbInstance = async () => {
const cosmosdb = await CosmoDbAccess.getInstance();
return cosmosdb;
};
index.test.ts
describe("getExemptionNotes()", () => {
beforeEach(() => {
jest.resetAllMocks();
});
it("makes a network call to getKustoResponse which posts to axios and returns what axios returns", async () => {
const mockNotes = [
{
},
];
const cosmosDBInstance = jest
.spyOn(CosmoDbAccess, "getInstance")
.mockReturnValue(Promise.resolve(CosmoDbAccess.instance));
const kustoResponseSpy = jest
.spyOn(CosmoDbAccess.prototype, "getAllNotesForLastSixMonths")
.mockReturnValue(Promise.resolve([mockNotes]));
const actual = await getExemptionNotes();
expect(kustoResponseSpy).toHaveBeenCalledTimes(1);
expect(actual).toEqual(mockNotes);
});
});
I am not able to get instance of CosmosDB or spyOn just the getAllNotesForLastSixMonths method. Please help me code it or give hints. The complexity is because the class is singleton or the methods are static and private

How can we make jest wait for an event before making an assert?

I'm creating a façade for the nats streaming lib as follows:
import nats, { Message, Stan, Subscription, SubscriptionOptions } from 'node-nats-streaming'
class NatsHelper {
private client: Stan | null = null
public connect(url: string, clusterID: string, clientID: string, listener: (...args: any[]) => void, verboseConnection: boolean = true): void {
const clientIDString = `${clientID}-${randomBytes(4).toString('hex')}`
if (verboseConnection) {
console.log(`Connecting to NATS cluster '${clusterID}' with clientID '${clientIDString}' on url '${url}'`)
}
const connectionAttempt = nats.connect(
clusterID,
clientIDString,
{
url
}
)
const setupConnection = (...args: any[]): void => {
this.client = connectionAttempt
this.client.on('close', (): void => {
if (verboseConnection) {
console.log(`Connection with NATS cluster '${clusterID}' with clientID '${clientIDString}' on url '${url}' was closed`)
}
this.client = null
process.exit()
})
process.on('SIGINT', () => this.client?.close())
process.on('SIGTERM', () => this.client?.close())
if (verboseConnection) {
console.log(`Connected to NATS cluster '${clusterID}' with clientID '${clientIDString}' on url '${url}' successfuly`)
}
listener(...args)
}
connectionAttempt.on('connect', setupConnection)
}
}
It happens though that I'm not able to test if the provided listener function is called, because it relies on the Stan 'connect' event to happen and jest finishes the test before it happens.
How can I make jest wait for this event to happen, and then executes the expect function?
You have overcomplicated this. It's perfectly possible to write the test for the original code without modifying it by mocking out the library using jest.mock(), and injecting mock implementations for your on method. Like this:
import nats from "node-nats-streaming";
import { mock } from "jest-mock-extended";
import { NatsHelper } from "./nats";
jest.mock("node-nats-streaming");
describe("NatsHelper", () => {
it("calls listener on connectEvent", () => {
const client = mock<nats.Stan>();
client.on.mockImplementation((name, callback) => {
if (name !== "close") {
callback();
}
return client;
});
jest.mocked(nats).connect.mockReturnValue(client);
const connector = new NatsHelper();
const listener = jest.fn();
connector.connect("foo", "foo", "foo", listener);
expect(listener).toHaveBeenCalled();
});
});
[EDIT] Found the solution I was looking
It happens that we can "convert" an event into a Promise, as follows:
import { randomBytes } from 'crypto'
import nats from 'node-nats-streaming'
export class NullClientError extends Error {
constructor() {
super('Nats client is not connected')
this.name = 'NullClientError'
}
}
export class NatsHelper {
private verboseConnectionString: string
private client: nats.Stan
private connector: nats.Stan
constructor(
private readonly verboseConnection: boolean = true
) { }
public async connect(url: string, clusterID: string, clientID: string, callback: (...args: any[]) => void): Promise<void> {
const clientIDString = `${clientID}-${randomBytes(4).toString('hex')}`
this.verboseConnectionString = `NATS cluster '${clusterID}' with clientID '${clientIDString}' on url '${url}'`
if (this.verboseConnection) {
console.log(`Connecting to ${this.verboseConnectionString}`)
}
this.connector = nats.connect(
clusterID,
clientIDString,
{
url
}
)
this.connector.on('connect', (...args: any[]) => {
const realCallback = this.setupListener(callback)
realCallback(...args)
})
return await new Promise(
resolve => {
if (this.connector) {
this.connector.on('connect', () => {
resolve()
})
}
}
)
}
private setupListener(listener: (...args: any[]) => void): (...args: any[]) => void {
const setupConnection = (...args: any[]): void => {
if (this.connector === undefined) {
throw new NullClientError()
}
this.client = this.connector
if (this.client === undefined) {
throw new NullClientError()
}
this.client.on('close', (): void => {
if (this.verboseConnection) {
console.log(`Connection with ${this.verboseConnectionString} was closed`)
}
process.exit()
})
process.on('SIGINT', () => this.client?.close())
process.on('SIGTERM', () => this.client?.close())
if (this.verboseConnection) {
console.log(`Connected to ${this.verboseConnectionString} successfuly`)
}
listener(...args)
}
return setupConnection
}
}
And then test it with asynchronous tests:
describe('NatsHelper', () => {
test('ensure NatsHelper calls connect with correct values', async () => {
const connectSpy = jest.spyOn(nats, 'connect')
const sut = new NatsHelper(false)
const { url, clusterID, clientID, listener } = makeConnectionParams()
await sut.connect(url, clusterID, clientID, listener)
const clientIDString = connectSpy.mock.calls[0][1]
expect(clientIDString).toContain(clientID)
expect(connectSpy).toHaveBeenCalledWith(clusterID, clientIDString, { url })
})
test('ensure NatsHelper forwards the callback when connected', async () => {
const connectionParms = makeConnectionParams()
const { url, clusterID, clientID } = connectionParms
const listenerSpy = jest.spyOn(connectionParms, 'listener')
const sut = new NatsHelper(false)
await sut.connect(url, clusterID, clientID, connectionParms.listener)
expect(listenerSpy).toHaveBeenCalledTimes(1)
})
}

Am I supposed to pass around Pino child loggers?

This is rather a stylistic question. I'm using Pino in some of my Javascript/Typescript microservices. As they're running on AWS I'd like to propagate the RequestId.
When one of my functions is invoked, I'm creating a new child logger like this:
const parentLogger = pino(pinoDefaultConfig)
function createLogger(context) {
return parentLogger.child({
...context,
})
}
function createLoggerForAwsLambda(awsContext) {
const context = {
requestId: awsContext.awsRequestId,
}
return createLogger(context)
}
I'm then passing down the logger instance to all methods. That said, (... , logger) is in almost every method signature which is not too nice. Moreover, I need to provide a logger in my tests.
How do you do it? Is there a better way?
you should implement some sort of Dependency Injection and include your logger there.
if your using microservices and maybe write lambdas in a functional approach, you can handle it by separating the initialization responsibility in a fashion like this:
import { SomeAwsEvent } from 'aws-lambda';
import pino from 'pino';
const createLogger = (event: SomeAwsEvent) => {
return pino().child({
requestId: event.requestContext.requestId
})
}
const SomeUtil = (logger: pinno.Logger) => () => {
logger.info('SomeUtil: said "hi"');
}
const init(event: SomeAwsEvent) => {
const logger = createLogger(event);
someUtil = SomeUtil(logger);
return {
logger,
someUtil
}
}
export const handler = (event: SomeAwsEvent) => {
const { someUtil } = init(event);
someUtil();
...
}
The simplest way is to use some DI library helper to tackle this
import { createContainer } from "iti"
interface Logger {
info: (msg: string) => void
}
class ConsoleLogger implements Logger {
info(msg: string): void {
console.log("[Console]:", msg)
}
}
class PinoLogger implements Logger {
info(msg: string): void {
console.log("[Pino]:", msg)
}
}
interface UserData {
name: string
}
class AuthService {
async getUserData(): Promise<UserData> {
return { name: "Big Lebowski" }
}
}
class User {
constructor(private data: UserData) {}
name = () => this.data.name
}
class PaymentService {
constructor(private readonly logger: Logger, private readonly user: User) {}
sendMoney() {
this.logger.info(`Sending monery to the: ${this.user.name()} `)
return true
}
}
export async function runMyApp() {
const root = createContainer()
.add({
logger: () =>
process.env.NODE_ENV === "production"
? new PinoLogger()
: new ConsoleLogger(),
})
.add({ auth: new AuthService() })
.add((ctx) => ({
user: async () => new User(await ctx.auth.getUserData()),
}))
.add((ctx) => ({
paymentService: async () =>
new PaymentService(ctx.logger, await ctx.user),
}))
const ps = await root.items.paymentService
ps.sendMoney()
}
console.log(" ---- My App START \n\n")
runMyApp().then(() => {
console.log("\n\n ---- My App END")
})
it is easy to write tests too:
import { instance, mock, reset, resetCalls, verify, when } from "ts-mockito"
import { PaymentService } from "./payment-service"
import type { Logger } from "./logger"
const mockedLogger = mock<Logger>()
when(mockedLogger.info).thenReturn(() => null)
describe("Payment service: ", () => {
beforeEach(() => {
resetCalls(mockedLogger)
// reset(mockedLogger)
})
it("should call logger info when sending money", () => {
const paymentService = new PaymentService(instance(mockedLogger))
expect(paymentService.sendMoney()).toBe(true)
})
})
I would not use the requestId as part of the context of the logger, but use it as the payload of the logger, like logger.info({ requestId }, myLogMessage). This was you can have a simple function create a child logger that you can use for the entire module.

Stub dependent function with sinon and proxyquire does not print stub result

I wrote a unit test for _getuser() but I don't see console.log print stub result.Also test coverage shows line 'let user = result.user;
console.log('User'+JSON.stringify(result));' is uncovered.Why stub result does not print in console log in getUser() function in LogInCommand class.
I see result shows undefined in unit test.
// userApi.js
'use strict';
const config = require('../../config/config');
const api = require('./apiService');
class UserApi {
constructor() {
}
getUser(userId) {
return api.get({
url: config.url,
qs: {
includeInactive: true,
id: userId,
startIndex: 0,
maxResults: 1
},
headers: {
Accept: 'application/json;',
'Connection': 'Keep-Alive'
}
});
}
}
module.exports = UserApi;
// LoginCommand.js
'use restrict';
const userApi = require('../../api/userApi');
class LogInCommand {
constructor() {
}
async _getUser(userId) {
let result = await new userApi().getUser(userId);
let user = result.user;
console.log('User'+JSON.stringify(result));
return user;
}
}
module.exports = LogInCommand;
//LoginCommand.test.js
describe('Test LogInCommand Class',function(){
it.only('_getUser function should return user', async ()=> {
let apiData= {
user:'abc'
};
let logincmd = proxyquire('../LogInCommand.js', {
userApi : { getUser : Promise.resolve(apiData) },'#noCallThru':true});
let obj= new logincmd();
let result= await obj._getUser(client);
});
});
The proxyquire configuration is incorrect in your current setup. Proxyquire maps the string value passed in a require call to the desired mock/stub values. Try the following instead:
let logincmd = proxyquire('../LogInCommand.js', {
'../../api/userApi' : { getUser : Promise.resolve(apiData) },
'#noCallThru':true
});
Below code worked for me
// userApi.js
'use strict';
const config = require('../../config/config');
const api = require('./apiService');
class UserApi {
constructor() {
}
getUser(userId) {
return api.get({
url: config.url,
qs: {
includeInactive: true,
id: userId,
startIndex: 0,
maxResults: 1
},
headers: {
Accept: 'application/json;',
'Connection': 'Keep-Alive'
}
});
}
}
module.exports = UserApi;
// LoginCommand.js
'use restrict';
const userApi = require('../../api/userApi');
class LogInCommand {
constructor() {
}
async _getUser(userId) {
let result = await new userApi().getUser(userId);
let user = result.user;
console.log('User'+JSON.stringify(result));
return user;
}
}
module.exports = LogInCommand;
//LoginCommand.test.js
describe('Test LogInCommand Class',function(){
it.only('_getUser function should return user', async ()=> {
class userApiStub{
constructor(){}
getUser() {
return Promise.resolve({
user:4
});
}
}
let logincmd = proxyquire('../LogInCommand.js', {
'../../api/userApi' :userApiStub },'#noCallThru':true});
let obj= new logincmd();
let result= await obj._getUser(client);
});
});

Categories

Resources