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.
Related
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
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)
})
}
I want to be able to assert whether my 'SIGN_IN' action works as intended.
My action looks like this (not the cleanest implementation but it's what I have to work with for now):
// actions.js
import {
SIGN_IN, SIGN_OUT, SET_AUTH_TOKEN, CLEAR_AUTH_TOKEN, USER_AUTHORISED,
} from '#/store/types';
import AuthService from '../../../authService';
export default {
async [SIGN_IN]({ commit, rootState }) {
const authService = new AuthService(rootState.clientSettings);
let response;
try {
response = await authService.getToken();
} catch (e) {
console.log('User not authorised to use application.');
}
if (response) {
commit(SET_AUTH_TOKEN, response);
} else {
commit(USER_AUTHORISED, false);
}
},
...
};
authService.getToken(); is what I want to mock the response of.
I want this to be able to resolve to a mock response in my test.
The class for AuthService looks like this:
// authService.js
import { PublicClientApplication } from '#azure/msal-browser';
class AuthService {
constructor(clientSettings) {
this.config = {
auth: {
clientId: clientSettings.clientId,
authority: `https://login.microsoftonline.com/${clientSettings.tenantId}`,
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
},
};
this.scopes = [
clientSettings.appScope,
];
this.msal = new PublicClientApplication(this.config);
this.clientSettings = clientSettings;
}
...
async getToken() {
const { scopes } = this;
try {
const response = await this.msal.acquireTokenSilent({ scopes });
return response;
} catch (err) {
const response = await this.msal.acquireTokenPopup({ scopes });
return response;
}
}
...
}
export default AuthService;
This is what I tried in my test:
// actions.spec.js
import sinon, { mock } from 'sinon';
import actions from '#/store/modules/auth/actions';
import { SIGN_IN, SIGN_OUT } from '#/store/types';
import { PublicClientApplication } from '#azure/msal-browser';
import AuthService from '../../../../../src/authService';
import templateRootState from '../../../../helpers/rootState.json';
describe('Auth', () => {
describe('Actions', () => {
let state;
beforeEach(() => {
state = JSON.parse(JSON.stringify(templateRootState));
});
afterEach(() => {
sinon.reset();
});
describe('SIGN_IN', () => {
it.only('returns a token from the AuthService & sets it in the store', async () => {
const commit = sinon.spy();
const mockResponse = {
token: 'bobbins'
};
sinon.stub(AuthService, 'getToken').resolves(mockResponse);
const requestParams = { commit, rootState: state };
await actions[SIGN_IN](requestParams);
expect(commit.calledOnce).toEqual(true);
expect(commit.args.length).toEqual(1);
expect(commit.args[0][0]).toEqual('SET_AUTH_TOKEN');
expect(commit.args[0][1]).toEqual(mockResponse);
});
});
});
});
While debugging, I am unable to get getToken to be a stubbed response. It still calls the actual userService class instance created in my action: new AuthService(rootState.clientSettings);.
Hopefully you can see what I'm trying to achieve with my test. I just want to assert that SET_AUTH_TOKEN is triggered and nothing else. How can I stub out this AuthService class entirely so that my action uses that one instead? I don't really want to be passing an instance of AuthService into the action itself, there must be a neater way of doing it?
What you need is to stub prototype method.
sinon.stub(AuthService.prototype, 'getToken').resolves(mockResponse);
Reference: how to mock es6 class
After defining class CognitoPool saving it as cognitoPool.ts script:
const AWS = require('aws-sdk');
import { CognitoIdentityServiceProvider } from 'aws-sdk';
import {ListUsersRequest, ListUsersResponse} from 'aws-sdk/clients/cognitoidentityserviceprovider';
export class CognitoPool {
private identityService: CognitoIdentityServiceProvider;
constructor(identityService: CognitoIdentityServiceProvider) {
this.identityService = identityService;
}
async listCognitoUsers(poolID: string, sub: string): Promise<ListUsersResponse> {
let params = {
UserPoolId: poolID,
Filter: `sub="${sub}"`
} as ListUsersRequest;
let res: ListUsersResponse = await this.identityService.listUsers(params).promise();
return res;
}
}
export default new CognitoPool(new AWS.CognitoIdentityServiceProvider());
I go ahead and write a test script:
const AWS = require('aws-sdk');
import sinon, { stubObject } from 'ts-sinon'
import { CognitoIdentityServiceProvider, AWSError } from 'aws-sdk';
import { PromiseResult } from 'aws-sdk/lib/request';
import { CognitoPool } from './cognitoPool';
describe('Testing', () => {
const identityService = new AWS.CognitoIdentityServiceProvider();
const stub = stubObject(identityService);
const cognitoPool = new CognitoPool(stub);
it('Test 01', async () => {
let mockData = {
Users: []
} as unknown as PromiseResult<any, AWSError>;
stub.listUsers.returns(mockData);
let result = await cognitoPool.listCognitoUsers('poolId-123', 'sub-123');
})
})
A mockData is to be returned by identityService.listUsers() as as PromiseResult:
let mockData = {
Users: []
} as unknown as PromiseResult<any, AWSError>;
But a test script runs with an error:
TypeError: this.identityService.listUsers(...).promise is not a function
Is there a way to avoid this error?
PromiseResult is an object that includes .promise as a function. Then, when you want to mock a function to return a PromiseResult, the mock data should be an object like PromiseResult.
In your case, mockData should be:
const mockData = {
promise: () => Promise.resolve({ Users: [] }),
} as unknown as PromiseResult<any, AWSError>;
One of my files have is creating a new Worker, and it uses file-loader module in order to do so.
Here's my code:
import * as workerPath from 'file-loader?name=[name].js!./worker';
class VideoPlayerWorker {
private worker: Worker;
constructor({ onMessageCallback }: { onMessageCallback: Function }) {
this.worker = new Worker(workerPath);
this.initHandleMessage({ onMessageCallback });
}
initTimer = (): void => {
this.postMessage({ type: 'WORKER_INIT_TIMER' });
};
postMessage = (message: ApeVideo.VideoWorkerEventMessage): void => {
this.worker.postMessage(message);
};
initHandleMessage = ({ onMessageCallback }: { onMessageCallback: Function }): void => {
this.worker.addEventListener('message', ({ data }: { data: ApeVideo.VideoWorkerEventMessage }): void => {
onMessageCallback(data.type);
});
};
destroy = (): void => {
this.worker.terminate();
};
}
export default VideoPlayerWorker;
The problem is when running jest, i get an error Cannot find module 'file-loader?name=[name].js!./worker' from 'playerWorkerGetaway.ts'
How can i work around this?