I have the following lambda handler to unit test. It uses a library #org/aws-connection which has a function mysql.getIamConnection which simply returns a knex connection.
Edit: I have added the mysql.getIamConnection function to the bottom of the post
Edit: If possible, I'd like to do the testing with only Jest. That is unless it becomes to complicated
index.js
const {mysql} = require('#org/aws-connection');
exports.handler = async (event) => {
const connection = await mysql.getIamConnection()
let response = {
statusCode: 200,
body: {
message: 'Successful'
}
}
try {
for(const currentMessage of event.Records){
let records = JSON.parse(currentMessage.body);
await connection.transaction(async (trx) => {
await trx
.table('my_table')
.insert(records)
.then(() =>
console.log(`Records inserted into table ${table}`))
.catch((err) => {
console.log(err)
throw err
})
})
}
} catch (e) {
console.error('There was an error while processing', { errorMessage: e})
response = {
statusCode: 400,
body: e
}
} finally {
connection.destroy()
}
return response
}
I have written some unit tests and I'm able to mock the connection.transaction function but I'm having trouble with the trx.select.insert.then.catch functions. H
Here is my testing file
index.test.js
import { handler } from '../src';
const mocks = require('./mocks');
jest.mock('#org/aws-connection', () => ({
mysql: {
getIamConnection: jest.fn(() => ({
transaction: jest.fn(() => ({
table: jest.fn().mockReturnThis(),
insert: jest.fn().mockReturnThis()
})),
table: jest.fn().mockReturnThis(),
insert: jest.fn().mockReturnThis(),
destroy: jest.fn().mockReturnThis()
}))
}
}))
describe('handler', () => {
test('test handler', async () =>{
const response = await handler(mocks.eventSqs)
expect(response.statusCode).toEqual(200)
});
});
This test works partially but it does not cover the trx portion at all. These lines are uncovered
await trx
.table('my_table')
.insert(records)
.then(() =>
console.log(`Records inserted into table ${table}`))
.catch((err) => {
console.log(err)
throw err
})
How can set up my mock #org/aws-connection so that it covers the trx functions as well?
Edit:
mysql.getIamConnection
async function getIamConnection (secretId, dbname) {
const secret = await getSecret(secretId)
const token = await getToken(secret)
let knex
console.log(`Initialzing a connection to ${secret.proxyendpoint}:${secret.port}/${dbname} as ${secret.username}`)
knex = require('knex')(
{
client: 'mysql2',
connection: {
host: secret.proxyendpoint,
user: secret.username,
database: dbname,
port: secret.port,
ssl: 'Amazon RDS',
authPlugins: {
mysql_clear_password: () => () => Buffer.from(token + '\0')
},
connectionLimit: 1
}
}
)
return knex
}
Solution
#qaismakani's answer worked for me. I wrote it slightly differently but the callback was the key. For anyone interested here is my end solution
const mockTrx = {
table: jest.fn().mockReturnThis(),
insert: jest.fn().mockResolvedValue()
}
jest.mock('#org/aws-connection', () => ({
mysql: {
getIamConnection: jest.fn(() => ({
transaction: jest.fn((callback) => callback(mockTrx)),
destroy: jest.fn().mockReturnThis()
}))
}
}))
Updating your mock to look like this might do the trick:
const { mysql } = require("#org/aws-connection");
jest.mock("#org/aws-connection", () => ({
mySql: {
getIamConnection: jest.fn()
}
}));
const mockTrx = {
table: jest.fn().mockReturnThis(),
insert: jest.fn().mockResolveValue() // Resolve any data here
};
mysql.getIamConnection.mockReturnValue({
transaction: jest.fn((callback) => callback(mockTrx)),
});
You need to mock the transaction so that it executes your callback with a dummy trx. To do this, you need to make sure that all the functions inside the trx object return a reference back to it or a promise so that you can chain it appropriately.
Instead of mocking knex implementation, I've written knex-mock-client which allows you to mimic real db with an easy API.
Change your mock implementation with
import { handler } from "../src";
import { getTracker } from "knex-mock-client";
const mocks = require("./mocks");
jest.mock("#org/aws-connection", () => {
const knex = require("knex");
const { MockClient } = require("knex-mock-client");
return {
mysql: {
getIamConnection: () => knex({ client: MockClient }),
},
};
});
describe("handler", () => {
test("test handler", async () => {
const tracker = getTracker();
tracker.on.insert("my_table").responseOnce([23]); // setup's a mock response when inserting into my_table
const response = await handler(mocks.eventSqs);
expect(response.statusCode).toEqual(200);
});
});
Related
This is the first time of mine written a unit test for a nest js application. Can anyone help me to create a mockRepository for my findAll createQueryBuilder service method? I am trying to develop a mockRepository.But it's not working.
This is my findAll createQueryBuilder service method.
async findAll(getPaginationParamDto: PaginationParamDto): Promise<GetCcAdminResponseDto> {
try {
const result = this.ccAdminRepo
.createQueryBuilder()
.leftJoinAndSelect('CcAdmin.userId', 'userId')
.where({ isArchived: false });
const total = await result.getCount();
const totalPages =
total % getPaginationParamDto.limit === 0
? total / getPaginationParamDto.limit
: Math.floor(total / getPaginationParamDto.limit) + 1;
const data = await result.offset(getPaginationParamDto.offset).limit(getPaginationParamDto.limit).getMany();
// Create custom response according to swagger
const customData = data.map((data) => {
if (data.userId) {
const userId = data.userId.userId;
return {
...data,
userId: userId,
};
}
});
return {
total,
totalPages,
data: customData,
};
} catch (error) {
console.log(error);
throw error;
}
}
This is my mockRepository and test function.
const mockRepository = {
createQueryBuilder: jest.fn().mockImplementation(() => ({
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis() as unknown,
})),
result: jest.fn().mockImplementation(() => ({
offset: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
})),
total: jest.fn().mockImplementation(() => ({
result: jest.fn().mockImplementation(() => ({
getCount: jest.fn().mockReturnThis(),
})),
})),
count: jest.fn().mockReturnValue(2),
findOneOrFail: jest.fn().mockReturnValue(CcAdminData[0]),
findOne: jest.fn().mockReturnValue(CcAdminData[0]),
create: jest.fn().mockReturnValue(createCcAdminInput),
save: jest.fn().mockImplementation(),
remove: jest.fn().mockImplementation(),
};
describe('findAll', () => {
it('should return an array of Cc Admin with totalPages and total', async () => {
const data = await service.findAll(getPaginationParamDto);
console.log(data);
expect(data).toEqual(getCcAdminResponseDto);
});
});
I'm trying to introduce a small change to an existing project without unit tests and decided I'd try to learn enough about nodejs and jest to include tests with my change. However, I cannot get mocks to work like I'd expect in, say, python. The project uses the "kubernetes-client" library from godaddy and tries to create a config object from the envvar "KUBECONFIG", like this:
// a few lines into server.js
// Instantiate Kubernetes client
const Client = require('kubernetes-client').Client
const config = require('kubernetes-client').config;
if (process.env.KUBECONFIG) {
client = new Client({
config: config.fromKubeconfig(config.loadKubeconfig(process.env.KUBECONFIG)),
version: '1.13'
});
}
else {
client = new Client({ config: config.getInCluster(), version: '1.9' });
}
In my testing environment, I don't want any API calls, so I'm trying to mock it out:
// __tests__/server.test.js
// Set up mocks at the top because of how server.js sets up k8s client
const k8sClient = require('kubernetes-client');
const narp = 'narp';
jest.mock('kubernetes-client', () => {
const noConfigmaps = jest.fn(() => {
throw narp;
});
const namespaces = jest.fn().mockReturnValue({
configmaps: jest.fn().mockReturnValue({
get: noConfigmaps
})
});
const addCustomResourceDefinition = jest.fn().mockReturnThis()
const mockClient = {
api: {
v1: {
namespaces
}
},
addCustomResourceDefinition: jest.fn().mockReturnThis(),
};
return {
Client: jest.fn(() => mockClient),
config: {
fromKubeconfig: jest.fn().mockReturnThis(),
loadKubeconfig: jest.fn().mockReturnThis(),
getInCluster: jest.fn().mockReturnThis()
},
};
});
const app = require('../server.js')
const supertest = require('supertest');
const requestWithSuperTest = supertest(app.app);
describe('Testing server.js', () => {
afterAll(() => {
app.server.close();
});
describe('Tests with k8s client throwing error when fetching configmaps', () => {
it("finds a resource's ingressGroup by name", () => {
var resource = {
"spec": {
"ingressClass": "foo",
"ingressTargetDNSName": "foo"
}
};
var ingressGroups = [
{
"ingressClass": "bar",
"hostName": "bar",
"name": "barName"
},
{
"ingressClass": "foo",
"hostName": "foo",
"name": "fooName"
}
];
expect(app.findMatchingIngressGroupForResource(resource, ingressGroups)).toBe("fooName");
});
it('GET /healthcheck should respond "Healthy"', async () => {
const resp = await requestWithSuperTest.get('/healthcheck');
console.log("Response in Testing Endpoints: " + JSON.stringify(resp));
expect(resp.status).toEqual(200);
expect(resp.type).toEqual(expect.stringContaining('text'));
expect(resp.text).toEqual('Healthy');
});
it('Tests getIngressGroups() rejects with error when it cannot get configmaps', async () => {
app.getIngressGroups()
.then()
.catch(error => {
expect(error).toEqual("Failed to fetch Ingress Groups: " + narp);
});
});
});
});
With this setup, the tests pass (although I suspect it's meaningless). If I try to move the mocks inside the describe or it block using a beforeEach function (or not) so that I can change the behavior to return mock data instead of throwing an error, I immediately get errors with the k8s client complaining it can't find my kubeconfig/clusterconfig:
$ npm run testj
> testj
> jest --detectOpenHandles
kubernetes-client deprecated require('kubernetes-client').config, use require('kubernetes-client/backends/request').config. server.js:45:44
kubernetes-client deprecated loadKubeconfig see https://github.com/godaddy/kubernetes-client/blob/master/merging-with-kubernetes.md#request-kubeconfig- server.js:49:42
FAIL __tests__/server.test.js
● Test suite failed to run
ENOENT: no such file or directory, open 'NOT_A_FILE'
44 | if (process.env.KUBECONFIG) {
45 | client = new Client({
> 46 | config: config.fromKubeconfig(config.loadKubeconfig(process.env.KUBECONFIG)),
| ^
47 | version: '1.13'
48 | });
49 | }
at node_modules/kubernetes-client/backends/request/config.js:335:37
at Array.map (<anonymous>)
at Object.loadKubeconfig (node_modules/kubernetes-client/backends/request/config.js:334:28)
at Object.eval [as loadKubeconfig] (eval at wrapfunction (node_modules/kubernetes-client/node_modules/depd/index.js:425:22), <anonymous>:5:11)
at Object.<anonymous> (server.js:46:46)
If anybody has run into this kind of behavior before or sees some obviously-wrong lines, I'd really appreciate any tips or information. Thanks!
I had to change a few things to get this working:
jest.doMock() instead of jest.mock()
use of let app inside the describe block instead of const app at module-scope
a beforeEach() which calls jest.resetModules()
an afterEach() which calls app.close()
in the it block which overrides the mock(s), explicitly call jest.resetModules() before overriding
in the it block which overrides the mock(s), call app.close() and re-initialize app before invoking the actual function-under-test/expect
Resulting test file:
// Set up mocks at the top because of how server.js sets up k8s client
const k8sClient = require('kubernetes-client');
const supertest = require('supertest');
const narp = 'narp';
describe('Testing server.js', () => {
let app;
let requestWithSuperTest;
beforeEach(() => {
jest.resetModules();
jest.doMock('kubernetes-client', () => {
const noConfigmaps = jest.fn(() => {
throw narp;
});
const namespaces = jest.fn().mockReturnValue({
configmaps: jest.fn().mockReturnValue({
get: noConfigmaps
})
});
const addCustomResourceDefinition = jest.fn().mockReturnThis()
const mockClient = {
api: {
v1: {
namespaces
}
},
addCustomResourceDefinition: jest.fn().mockReturnThis(),
};
return {
Client: jest.fn(() => mockClient),
config: {
fromKubeconfig: jest.fn().mockReturnThis(),
loadKubeconfig: jest.fn().mockReturnThis(),
getInCluster: jest.fn().mockReturnThis()
},
};
});
app = require('../server.js');
requestWithSuperTest = supertest(app.app);
});
afterEach(() => {
app.server.close();
});
it("finds a Resource's ingressGroup by name", () => {
var resource = {
"spec": {
"ingressClass": "foo",
"ingressTargetDNSName": "foo"
}
};
var ingressGroups = [
{
"ingressClass": "bar",
"hostName": "bar",
"name": "barName"
},
{
"ingressClass": "foo",
"hostName": "foo",
"name": "fooName"
}
];
expect(app.findMatchingIngressGroupForResource(resource, ingressGroups)).toBe("fooName");
});
it('GET /healthcheck should respond "Healthy"', async () => {
const resp = await requestWithSuperTest.get('/healthcheck');
console.log("Response in Testing Endpoints: " + JSON.stringify(resp));
expect(resp.status).toEqual(200);
expect(resp.type).toEqual(expect.stringContaining('text'));
expect(resp.text).toEqual('Healthy');
});
it('Tests getIngressGroups() rejects with error when it cannot get configmaps', async () => {
expect.assertions(1);
await app.getIngressGroups()
.catch(error => {
expect(error).toEqual("Failed to fetch Ingress Groups: " + narp);
});
});
it('Tests getIngressGroups() succeeds when it gets configmaps', async () => {
expect.assertions(1);
jest.resetModules();
jest.doMock('kubernetes-client', () => {
const noConfigmaps = jest.fn(() => {
console.log('Attempted to get mocked configmaps');
return Promise.resolve({
body: {
items: []
}
});
});
const namespaces = jest.fn().mockReturnValue({
configmaps: jest.fn().mockReturnValue({
get: noConfigmaps
})
});
const addCustomResourceDefinition = jest.fn().mockReturnThis()
const mockClient = {
api: {
v1: {
namespaces
}
},
addCustomResourceDefinition: jest.fn().mockReturnThis(),
};
return {
Client: jest.fn(() => mockClient),
config: {
fromKubeconfig: jest.fn().mockReturnThis(),
loadKubeconfig: jest.fn().mockReturnThis(),
getInCluster: jest.fn().mockReturnThis()
},
};
});
app.server.close();
app = require('../server.js');
await app.getIngressGroups()
.then(result => {
expect(result).toEqual([])
});
});
});
I'm writing a test in jest for a module which uses a constant from a different module.
I want to set a different value for it for every test case, but I don't seem to be able to do so.
The test file:
import { Request, Response } from 'express';
const activityConsumer = require('../../src/utils/activity.consumer');
const mockRequest = {
params: {
activityArn: 'activityArn'
}
} as Request;
const mockedJsonFunction = jest.fn();
const mockResponse: any = {
json: jest.fn(),
status: jest.fn().mockReturnValue({ json: mockedJsonFunction }),
} as Response;
let stopConsumerMock;
describe('consumer handler', () => {
beforeAll(() => {
stopConsumerMock = activityConsumer.stopConsumer = jest.fn().mockReturnValue(1);
});
beforeEach(() => {
jest.resetModules();
});
afterEach(() => {
stopConsumerMock.mockClear();
mockResponse.json.mockClear();
});
describe('stopConsumingHandler', () => {
it('Should return success true and not call stopConsumer when no consumer exists', () => {
activityConsumer.consumer = undefined;
const { stopConsumingHandler } = require ('../../src/handlers/consumer.handlers');
stopConsumingHandler(mockRequest, mockResponse);
expect(stopConsumerMock.mock.calls.length).toEqual(0);
expect(mockResponse.json.mock.calls.length).toEqual(1);
expect(mockResponse.json).toHaveBeenCalledWith({ success: true });
});
it('Should return success true and call stopConsumer when consumer exists', () => {
activityConsumer.consumer = true;
const { stopConsumingHandler } = require ('../../src/handlers/consumer.handlers');
stopConsumingHandler(mockRequest, mockResponse);
expect(stopConsumerMock.mock.calls.length).toEqual(1);
expect(mockResponse.json.mock.calls.length).toEqual(1);
expect(mockResponse.json).toHaveBeenCalledWith({ success: true });
});
});
});
I want to replace the value of activityConsumer.consumer and then reload the consumer.handlers module but the re-assignment and reload does not seem to have any effect.
Please advise on how can I write this test properly.
Try this way, using jest.mock to modify import value of activityConsumer
import { Request, Response } from 'express';
// const activityConsumer = require('../../src/utils/activity.consumer');
const mockRequest = {
params: {
activityArn: 'activityArn'
}
} as Request;
const mockedJsonFunction = jest.fn();
const mockResponse: any = {
json: jest.fn(),
status: jest.fn().mockReturnValue({ json: mockedJsonFunction }),
} as Response;
let stopConsumerMock;
describe('consumer handler', () => {
beforeAll(() => {
// stopConsumerMock = activityConsumer.stopConsumer = jest.fn().mockReturnValue(1);
stopConsumerMock = jest.fn().mockReturnValue(1);
});
beforeEach(() => {
jest.resetModules(); // important line
});
afterEach(() => {
stopConsumerMock.mockClear();
mockResponse.json.mockClear();
});
describe('stopConsumingHandler', () => {
it('Should return success true and not call stopConsumer when no consumer exists', () => {
// activityConsumer.consumer = undefined;
// mock by this way
jest.mock('../../src/utils/activity.consumer', () => ({
consumer: undefined,
stopConsumer: stopConsumerMock,
}));
const { stopConsumingHandler } = require('../../src/handlers/consumer.handlers');
stopConsumingHandler(mockRequest, mockResponse);
expect(stopConsumerMock.mock.calls.length).toEqual(0);
expect(mockResponse.json.mock.calls.length).toEqual(1);
expect(mockResponse.json).toHaveBeenCalledWith({ success: true });
});
it('Should return success true and call stopConsumer when consumer exists', () => {
// activityConsumer.consumer = true;
// mock by this way
jest.mock('../../src/utils/activity.consumer', () => ({
consumer: true, // mock value for consumer
stopConsumer: stopConsumerMock,
}));
const { stopConsumingHandler } = require('../../src/handlers/consumer.handlers');
stopConsumingHandler(mockRequest, mockResponse);
expect(stopConsumerMock.mock.calls.length).toEqual(1);
expect(mockResponse.json.mock.calls.length).toEqual(1);
expect(mockResponse.json).toHaveBeenCalledWith({ success: true });
});
});
});
Model.knex(knex);
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(i18nextMiddleware);
I want to test method getUsers of users controller. It get data from usersModel getUsers method. I get date from MySQL by Objections ORM.
app.get(
'/',
checkFilter(['type']),
users.getUsers
);
According to instructions, I am changing the query method.
class MyQueryBuilder extends QueryBuilder {
query() {
return this.resolve({ test: 11111 });
}
}
class UsersModel extends Model {
static get QueryBuilder() {
return MyQueryBuilder;
}
}
jest.spyOn(Users, 'query')
.mockImplementation(UsersModel.query);
Describe test.
describe('get errors', () => {
beforeAll(done => {
i18next.on('initialized', () => {
done()
});
});
it('with filter', done => {
request(app)
.get('/')
.query({page: 0, perPage: 5, type: 'admin'})
.end((err, res) => {
if (err) return done(err);
expect(res.status).toBe(200);
expect(
Object.keys(res.body).sort()
).toEqual([
'items',
'itemsOnPage',
'currentPage',
'totalPage',
'totalItems'
].sort());
expect(res.body.items).toHaveLength(8);
expect(res.body.totalItems).toBe(usersMockDB.getUsers.length);
console.log(res.body);
done();
});
});
afterAll(done => {
knex.destroy();
done();
})
});
Method getUsers of users model.
const { Users } = require('../../db/models/Users');
const query = Users
.query()
.select(
'id',
'login',
'type',
'edit',
'email',
'phone',
'block'
)
.orderBy('id', 'DESC')
.page(page, perPage);
// filter
if (Object.keys(usersFilter).length) {
for (let field in usersFilter) {
if ( usersFilter.hasOwnProperty(field) ) {
query.where(field, 'like', `%${ usersFilter[field] }%`);
}
}
}
const { results, total } = await query;
return {
items: results,
itemsOnPage: Number(perPage),
currentPage: Number(page),
totalPage: Math.ceil(total/perPage),
totalItems: Number(total)
}
Should I override methods page and where ? As I understand it, they make new database queries.
This may not be desirable in every case, but I find that the easiest solution for unit tests that use objection models is to create one transaction per test. This does mean you'll need a database to run tests, but everything is rolled back between tests.
In jest.config.js, add this line
setupFilesAfterEnv: ['./jest.setup.js'],
in jest.setup.js:
import objection from 'objection';
import knex from './src/db/index.js'; // Change src/db/index.js to the path to the file where you export your knex instance
const {transaction, Model} = objection;
global.beforeAll(async () => {
global.knex = knex;
global.txn = null;
});
global.beforeEach(async () => {
global.txn = await transaction.start(knex);
Model.knex(global.txn);
});
global.afterEach(async () => {
await global.txn.rollback();
Model.knex(knex);
});
global.afterAll(async () => {
global.knex.destroy();
});
You can then use your models as expected in your code and unit tests
import {User} from './src/db/models/index.js';
it('creates and reads users', async () => {
const user = await User.query().insert({email: 'test#test.com'});
const users = await User.query();
expect(users).toHaveLength(1);
});
it('does not persist users between tests', async () => {
const users = await User.query();
expect(users).toHaveLength(1);
});
I am trying to write a unit test for code that uses pg-promise, it looks like this:
const pgp = require('pg-promise')();
const cn = {
host: process.env.DB_HOST,
port: 5432,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD
};
function insertStuff(stuff) {
let db = pgp(cn);
return db.one('INSERT INTO test(stuff) VALUES ($1) RETURNING id, stuff', [stuff])
.then(data => {
return data
})
}
module.exports.insertStuff = insertStuff
The test code looks like this:
const mockFakeDb = {
one: jest.fn()
}
jest.mock("pg-promise", () => {
return mockFakeDb
})
const insertStuff = require("../src/db-utils").insertStuff
test("params for inserting stuff are correct", done => {
mockFakeDb.one.mockImplementationOnce(() => {
return Promise.resolve({id: 123456789, stuff: "stuff"})
insertStuff("stuff").then((data) => {
const insertCall = fakeDb.one.mock.calls[0]
expect(insertCall).toHaveBeenCalledTimes(1)
done()
})
})
so in trying to mock pg-promise require I get an error:
TypeError: require(...) is not a function.
I can see that pg-promise has a function which takes parameters (the second brackets) but not sure how to now mock this?
For anybody else not quite sure how to do this:
const fakeDB = {
one: jest.fn()
}
function fakePgpFunc() {
return fakeDB
}
fakePgpFunc.end = jest.fn()
jest.doMock("pg-promise", () => {
return jest.fn(() => fakePgpFunc)
})
const insertStuff = require("../src/db-utils").insertStuff
beforeEach(() => {
jest.clearAllMocks()
})
test("params for inserting stuff are correct", done => {
fakeDB.one.mockImplementationOnce(() => {
return Promise.resolve({"stuff": "Stuff", "id": 123456789})
})
insertStuff("Stuff").then((data) => {
expect(fakeDB.one).toHaveBeenCalledTimes(1)
const insertStuffCall = fakeDB.one.mock.calls[0]
expect(insertStuffCall[0]).toEqual("INSERT INTO test(stuff) VALUES ($1) RETURNING id, stuff")
expect(queryInsertCall[1]).toEqual(["Stuff"])
expect(data).toEqual({id: 123456789, stuff: "Stuff"})
expect(fakePgpFunc.end).toHaveBeenCalledTimes(1)
done()
})
})