I have a Connection class used to make a connection to a AWS RDS Aurora database instance. The class works fine but I'm having trouble getting full unit test coverage. There is one piece that I'm not sure how to cover. It is mysql_clear_password: () => () => Buffer.from(this.options.password + '\0') shown in the Connection class below. How can I cover that specific line? Is a refactor of the function necessary?
I have tried moving the Buffer function to a separate function, but the coverage report still shows that original line as being uncovered
Connection class:
const mysql2 = require('mysql2/promise');
class Connection {
constructor(options = {}) {
this.options = options;
}
createPool () {
this.pool = mysql2.createPool({
host: this.options.host,
user: this.options.user,
database: 'my_db',
ssl: 'Amazon RDS',
password: this.options.password,
authPlugins: {
mysql_clear_password: () => () => Buffer.from(this.options.password + '\0')
}
});
}
}
module.exports = { Connection };
Here is what I have so far in my test:
const conns = require('../src/connection');
const sinon = require('sinon');
const mysql2 = require('mysql2/promise');
describe('connection', () => {
afterEach(() => {
sinon.restore();
});
test('Test creatPool function from connection class', async () => {
const options = {
host: 'testHost',
user: 'testUser',
password: 'testPassword'
};
const createPoolStub = sinon.stub(mysql2, 'createPool').returns(sinon.stub().returnsThis());
const conn = new conns.Connection(options);
await conn.createPool();
sinon.assert.calledOnce(createPoolStub);
});
});
Using stub.callsFake method to make the stub(mysql2.createPool) call the provided function when invoked. Then, you can get the mysql_clear_password method from the provided function in your test case.
E.g.
connection.js:
const mysql2 = require('mysql2/promise');
class Connection {
constructor(options = {}) {
this.options = options;
}
createPool() {
this.pool = mysql2.createPool({
host: this.options.host,
user: this.options.user,
database: 'my_db',
ssl: 'Amazon RDS',
password: this.options.password,
authPlugins: {
mysql_clear_password: () => () => Buffer.from(this.options.password + '\0'),
},
});
}
}
module.exports = { Connection };
connection.test.js:
const mysql2 = require('mysql2/promise');
const conns = require('./connection');
const sinon = require('sinon');
const { expect } = require('chai');
describe('64300458', () => {
it('Test creatPool function from connection class', () => {
const options = {
host: 'testHost',
user: 'testUser',
password: 'testPassword',
};
let configRef;
const createPoolStub = sinon.stub(mysql2, 'createPool').callsFake((config) => {
configRef = config;
});
const conn = new conns.Connection(options);
conn.createPool();
sinon.assert.calledOnce(createPoolStub);
// test mysql_clear_password
const actual = configRef.authPlugins.mysql_clear_password()();
expect(actual).to.be.eql(Buffer.from('testPassword\0'));
createPoolStub.restore();
});
});
unit test result with coverage report:
64300458
✓ Test creatPool function from connection class
1 passing (11ms)
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 100 | 0 | 100 | 100 |
connection.js | 100 | 0 | 100 | 100 | 4
---------------|---------|----------|---------|---------|-------------------
Related
I'm currently attempting to mock AWS SecretsManager for my unit testing with Jest, and everytime I'm hit with the ConfigError
My code is somewhat like this
//index.ts
import SM from "aws-sdk/clients/secretsmanager"
const secretManagerClient = new SM()
...
export const randomMethod = async (a: string, b: string) => {
let secret
const personalToken = {
SecretId: process.env.secretId,
}
secretManagerClient
.getSecretValue(personalToken, (err, data) => {
if (err) {
console.error(`[SECRETS MANAGER] Error fetching personal token : ${err}`)
} else if (data && data.SecretString) {
secret = data.SecretString
}
})
}
My mock goes like this :
//index.test.js
const mockGetSecretValue = jest.fn((SecretId) => {
switch (SecretId) {
case process.env.GITHUB_PERSONAL_TOKEN:
return {
SecretString: process.env.GITHUB_PERSONAL_TOKEN_VALUE,
}
default:
throw Error("secret not found")
}
})
jest.mock("aws-sdk/clients/secretsmanager", () => {
return jest.fn(() => {
return {
getSecretValue: jest.fn(({ SecretId }) => {
return mockGetSecretValue(SecretId)
}),
promise: jest.fn(),
}
})
})
However, I get this error thrown at me : ConfigError: Missing region in config, which I understand to some extent, however I don't understand why it occurs here in the mocking part...
Thanks in advance!
EDIT: Thanks to the 1st answer, I've managed to stop having this error. However, the getSecretValue() method is not returning the Secret value I want.
You should NOT use the callback of .getSecretValue() method with .promise() together. Just choose one of them. The error means you didn't mock the secretsmanager class correctly of aws-sdk.
E.g.
index.ts:
import SM from 'aws-sdk/clients/secretsmanager';
const secretManagerClient = new SM();
export const randomMethod = async () => {
const personalToken = {
SecretId: process.env.secretId || '',
};
try {
const data = await secretManagerClient.getSecretValue(personalToken).promise();
return data.SecretString;
} catch (err) {
console.error(`[SECRETS MANAGER] Error fetching personal token : ${err}`);
}
};
index.test.ts:
import { randomMethod } from '.';
import SM from 'aws-sdk/clients/secretsmanager';
import { mocked } from 'ts-jest/utils';
import { PromiseResult } from 'aws-sdk/lib/request';
jest.mock('aws-sdk/clients/secretsmanager', () => {
const mSecretManagerClient = {
getSecretValue: jest.fn().mockReturnThis(),
promise: jest.fn(),
};
return jest.fn(() => mSecretManagerClient);
});
describe('69977310', () => {
test('should get secret value', async () => {
process.env.secretId = 's1';
const mSecretManagerClient = mocked<InstanceType<typeof SM>>(new SM());
const mGetSecretValueRequest = mocked(mSecretManagerClient.getSecretValue());
mGetSecretValueRequest.promise.mockResolvedValue({
SecretString: JSON.stringify({ password: '123456' }),
} as PromiseResult<any, any>);
const actual = await randomMethod();
expect(actual).toEqual(JSON.stringify({ password: '123456' }));
expect(mSecretManagerClient.getSecretValue as jest.Mocked<any>).toBeCalledWith({ SecretId: 's1' });
});
test('should throw error', async () => {
process.env.secretId = 's1';
const logSpy = jest.spyOn(console, 'error').mockImplementation(() => 'suppress error log for testing');
const mSecretManagerClient = mocked<InstanceType<typeof SM>>(new SM());
const mGetSecretValueRequest = mocked(mSecretManagerClient.getSecretValue());
const mError = new Error('network');
mGetSecretValueRequest.promise.mockRejectedValue(mError);
await randomMethod();
expect(logSpy).toBeCalledWith(`[SECRETS MANAGER] Error fetching personal token : ${mError}`);
expect(mSecretManagerClient.getSecretValue as jest.Mocked<any>).toBeCalledWith({ SecretId: 's1' });
});
});
test result:
PASS examples/69977310/index.test.ts (7.722 s)
69977310
✓ should get secret value (4 ms)
✓ should throw error (1 ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 50 | 100 | 100 |
index.ts | 100 | 50 | 100 | 100 | 6
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 8.282 s, estimated 10 s
package versions:
"aws-sdk": "^2.875.0",
"typescript": "^4.1.2",
"jest": "^26.6.3",
I've overlooked the fact that I was using a callback in order to bypass the promise().
The following is the correct code:
const mockGetSecretValue = jest.fn((SecretId, callback) => {
console.log("secretId", SecretId)
switch (SecretId) {
case process.env.GITHUB_PERSONAL_TOKEN:
const data = {
SecretString: process.env.GITHUB_PERSONAL_TOKEN_VALUE,
}
callback(null, data)
break;
default:
const err = new Error("secret not found")
throw err
}
})
jest.mock("aws-sdk/clients/secretsmanager", () => {
return jest.fn(() => {
return {
promise: jest.fn(),
getSecretValue: jest.fn(({ SecretId }, callback) => {
return mockGetSecretValue(SecretId, callback)
}),
}
})
})
Thanks again for your help #slideshowp2!
I am attempting to mock SQL Server connection pool so that I can test the function of a DAL.
I have a connection pool file
connectionPool.js
const sql = require('mssql');
const log = require('../services/logger');
const config = {
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
server: process.env.SERVER,
database: process.env.DATABASE
};
const poolPromise = new sql.ConnectionPool(config)
.connect()
.then(pool => {
log.info('Connected to SQL Server');
return pool;
})
.catch(err => {
log.error(err, 'Database connection failed');
});
module.exports = poolPromise;
and I use it in the DAL. Very stripped down, but the essentials are there.
const {poolPromise} = require('./connectionPool');
const getData = async () => {
const pool = await poolPromise;
const request = pool.request()
const result = await request('SELECT * FROM table');
}
This way, the connection pool is only created once per application. (See How can I use a single mssql connection pool across several routes in an Express 4 web application?)
I want to mock the mssql module so that the connection pool function still works. I have tried multiple options. How to mock SQL Server connection pool using Jest? gets me close, but its not quite there.
__mocks/mssql.js
const mockExecute = jest.fn();
const mockInput = jest.fn(() => ({ execute: mockExecute }));
const mockRequest = jest.fn(() => ({ input: mockInput }));
jest.mock('mssql', () => ({
ConnectionPool: jest.fn(() => ({request: mockRequest})),
NVarChar: jest.fn()
}));
const sql = require('mssql');
module.exports = sql;
However I get the error
TypeError: (intermediate value).connect is not a function
17 |
18 | const poolPromise = new sql.ConnectionPool(config)
19 | .connect()
| ^
20 | .then(pool => {
21 | log.info('Connected to SQL Server');
22 | return pool;
This may be a solution.
A bit of refactoring of connectionPool.js
const sql = require('mssql');
const log = require('../services/logger');
const config = {
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
server: process.env.SERVER,
database: process.env.DATABASE
};
if (process.env.NODE_ENV === 'development') {
config.options = {
encrypt: false,
trustServerCertificate: true
};
}
const connectionPool = new sql.ConnectionPool(config);
const poolPromise = connectionPool
.connect()
.then(pool => {
log.info('Connected to MSSQL');
return pool;
})
.catch(err => {
log.error(err, 'Database connection failed');
});
module.exports = poolPromise;
Then in /__mocks__/mssql.js
'use strict';
const mockExecute = jest.fn();
const mockInput = jest.fn().mockReturnValue({ execute: mockExecute });
const mockQuery = jest.fn().mockReturnValue({recordset: 'Mock data'});
const mockRequest = jest.fn().mockReturnValue({
input: mockInput,
query: mockQuery
});
const mockTransaction = jest.fn().mockImplementation(() => {
return {
begin: callback => callback(),
commit: jest.fn(),
rollback: jest.fn()
};
});
const mockConnect = jest.fn().mockImplementation(() => {
return Promise.resolve({ transaction: mockTransaction });
});
jest.mock('mssql', () => ({
ConnectionPool: jest.fn().mockReturnValue({
request: mockRequest,
connect: mockConnect
}),
Request: mockRequest,
NVarChar: jest.fn()
}));
const mssql = require('mssql');
module.exports = mssql;
It appears to work, but I am not sure if it is correct
I have a DBManager class to connect to mongoClient
import { MongoClient } from 'mongodb';
class DBManager {
private url = process.env.MONGODB_URL;
private _connection: MongoClient;
constructor() {
this._connection = null;
}
get connection() {
return this._connection;
}
async start() {
if (!this._connection) {
this._connection = await MongoClient.connect(this.url);
}
}
}
export default new DBManager();
and I call this class like this
await DBManager.start();
const db = DBManager.connection.db();
I get this error when I try to mock:
Received: [TypeError: db_manager_1.default.connection.db is not a function]
this is how to mock method i use:
DBManager.start = jest.fn().mockResolvedValue(() => ({
connection: jest.fn().mockReturnThis(),
db: jest.fn().mockResolvedValue({success: true})
}));
thanks..
You can use a real MongoDB server to use in tests with the package mongodb-memory-server.
So in your case, you just need to do:
import { MongoMemoryServer } from '../index';
describe('Single MongoMemoryServer', () => {
let con;
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
process.env.MONGODB_URL = mongoServer.getUri();
});
afterAll(async () => {
if (con) {
await con.close();
}
if (mongoServer) {
await mongoServer.stop();
}
});
it('DBManager connection', async () => {
await DBManager.start();
const db = DBManager.connection.db();
// ...
});
You can use jest.spyOn(object, methodName, accessType?) to mock DBManager.start() method and DBMananger.connection getter.
E.g.
dbManager.ts:
import { MongoClient } from 'mongodb';
class DBManager {
private url = process.env.MONGODB_URL || '';
private _connection: MongoClient | null;
constructor() {
this._connection = null;
}
get connection() {
return this._connection;
}
async start() {
if (!this._connection) {
this._connection = await MongoClient.connect(this.url);
}
}
}
export default new DBManager();
main.ts:
import DBManager from './dbManager';
export async function main() {
await DBManager.start();
const db = DBManager.connection!.db();
db.collection('users');
}
main.test.ts:
import { main } from './main';
import DBManager from './dbManager';
import { Db, MongoClient } from 'mongodb';
describe('68888424', () => {
afterEach(() => {
jest.restoreAllMocks();
});
test('should pass', async () => {
const mockDbInstance = ({
collection: jest.fn(),
} as unknown) as Db;
const mockDb = jest.fn(() => mockDbInstance);
jest.spyOn(DBManager, 'start').mockResolvedValueOnce();
jest.spyOn(DBManager, 'connection', 'get').mockReturnValue(({ db: mockDb } as unknown) as MongoClient);
await main();
expect(DBManager.start).toBeCalledTimes(1);
expect(DBManager.connection!.db).toBeCalledTimes(1);
expect(mockDbInstance.collection).toBeCalledWith('users');
});
});
test result:
PASS examples/68888424/main.test.ts (8.621 s)
68888424
✓ should pass (5 ms)
--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 75 | 50 | 50 | 75 |
dbManager.ts | 57.14 | 50 | 33.33 | 57.14 | 12-17
main.ts | 100 | 100 | 100 | 100 |
--------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 9.532 s
I created a Joi validation schema that gets called in my routes. However when I run a code coverage that file is NOT being covered. So, I am trying to write a test for it.
Validator.js
const Joi = require('joi');
module.exports = {
validateExternalId: (schema, name) => {
return (req, res, next) => {
const result = Joi.validate({ param: req.params[name] }, schema);
if (result.error) {
return res.status(400).send(result.error.details[0].message);
}
next();
};
},
schemas: {
idSchema: Joi.object().keys({
param: Joi.string().regex(/^[a-zA-Z0-9]{20}$/).required()
})
}
};
Validator.test.js
const { validateExternalId, schemas } = require('../../src/helpers/validation');
const app = require('../../src/router')
const mockResponse = () => {
const res = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
return res;
};
describe('Testing validateExternalId schema', () => {
it('It can validate the external Id Regex length', done => {
const req = {
params: [
{
extClientId: 'abcdefghij0123456789'
}
]
};
app.use('/token/:extClientId', validateExternalId(schemas.idSchema, 'extClientId');
// expect().toHaveBeenCalled();
});
});
Please Go Easy on ME... Here is my attempt on testing this Joi validator. I tried to but my expected wasn't working so I commented it out for now. any pointers would be appreciated. thank you
Here is the unit test solution:
validator.js:
const Joi = require('joi');
module.exports = {
validateExternalId: (schema, name) => {
return (req, res, next) => {
const result = Joi.validate({ param: req.params[name] }, schema);
if (result.error) {
return res.status(400).send(result.error.details[0].message);
}
next();
};
},
schemas: {
idSchema: Joi.object().keys({
param: Joi.string()
.regex(/^[a-zA-Z0-9]{20}$/)
.required(),
}),
},
};
validator.test.js:
const { validateExternalId, schemas } = require('./validator');
const Joi = require('joi');
describe('60730701', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('should send error', () => {
const validationResults = { error: { details: [{ message: 'validation error' }] } };
const validateSpy = jest.spyOn(Joi, 'validate').mockReturnValueOnce(validationResults);
const mReq = { params: { extClientId: '123' } };
const mRes = { status: jest.fn().mockReturnThis(), send: jest.fn() };
validateExternalId(schemas.idSchema, 'extClientId')(mReq, mRes);
expect(validateSpy).toBeCalledWith({ param: '123' }, schemas.idSchema);
expect(mRes.status).toBeCalledWith(400);
expect(mRes.send).toBeCalledWith('validation error');
});
it('should pass the validation and call api', () => {
const validationResults = { error: undefined };
const validateSpy = jest.spyOn(Joi, 'validate').mockReturnValueOnce(validationResults);
const mReq = { params: { extClientId: '123' } };
const mRes = {};
const mNext = jest.fn();
validateExternalId(schemas.idSchema, 'extClientId')(mReq, mRes, mNext);
expect(validateSpy).toBeCalledWith({ param: '123' }, schemas.idSchema);
expect(mNext).toBeCalled();
});
});
unit test results with 100% coverage:
PASS stackoverflow/60730701/validator.test.js (9.96s)
60730701
✓ should send error (6ms)
✓ should pass the validation and call api (2ms)
--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
validator.js | 100 | 100 | 100 | 100 |
--------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 11.647s, estimated 15s
source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/60730701
I've looked at several similar questions but none of the cases fit my problem. I'm trying to mock a constructor, which I've done in other tests, but I can't get it to work in the case of using google-auth-library
code.js
const {OAuth2Client} = require('google-auth-library');
const keys = require('./oauth2.keys.json');
async function getRedirectUrl() {
const oAuth2Client = new OAuth2Client(
keys.installed.client_id,
keys.installed.client_secret,
keys.installed.redirect_uris[0]
);
const authorizeUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: 'https://www.googleapis.com/auth/bigquery',
prompt: 'consent'
});
return authorizeUrl;
}
test.js
let Code = require('../code.js');
describe('code', function() {
let generateUrlStub, tokenStub, mockClient;
before(async () => {
generateUrlStub = sinon.stub().returns('http://example.com');
tokenStub = sinon.stub().returns({tokens: 'tokens'});
mockClient = sinon.stub().returns({
generateAuthUrl: generateUrlStub,
getToken: tokenStub,
});
Code = proxyquire('../Code.js', {
'google-auth-library': mockClient,
});
});
it('should call generateAuthUrl', async function() {
const output = await Code.getRedirectUrl();
sinon.assert.called(generateUrlStub)
});
});
Here is the unit test solution:
const { OAuth2Client } = require("google-auth-library");
const keys = {
installed: {
client_id: "1",
client_secret: "client_secret",
redirect_uris: ["http://example.com/callback"]
}
};
async function getRedirectUrl() {
const oAuth2Client = new OAuth2Client(
keys.installed.client_id,
keys.installed.client_secret,
keys.installed.redirect_uris[0]
);
const authorizeUrl = oAuth2Client.generateAuthUrl({
access_type: "offline",
scope: "https://www.googleapis.com/auth/bigquery",
prompt: "consent"
});
return authorizeUrl;
}
module.exports = { getRedirectUrl };
index.spec.js:
const proxyquire = require("proxyquire");
const sinon = require("sinon");
const { expect } = require("chai");
describe("code", function() {
let generateUrlStub, tokenStub, code;
beforeEach(() => {
generateUrlStub = sinon.stub().returns("http://example.com");
tokenStub = sinon.stub().returns({ tokens: "tokens" });
code = proxyquire("./", {
"google-auth-library": {
OAuth2Client: sinon.stub().callsFake(() => {
return {
generateAuthUrl: generateUrlStub,
getToken: tokenStub
};
})
}
});
});
afterEach(() => {
sinon.restore();
});
it("should call generateAuthUrl", async function() {
const output = await code.getRedirectUrl();
expect(output).to.be.eq("http://example.com");
sinon.assert.called(generateUrlStub);
});
});
Unit test result with 100% coverage:
code
✓ should call generateAuthUrl
1 passing (216ms)
---------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
---------------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
index.js | 100 | 100 | 100 | 100 | |
index.spec.js | 100 | 100 | 100 | 100 | |
---------------|----------|----------|----------|----------|-------------------|
Source code: https://github.com/mrdulin/mocha-chai-sinon-codelab/tree/master/src/stackoverflow/58955304