I have my internal function
//in greatRoute.ts
async function _secretString(param: string): Promise<string> {
...
}
router
.route('/foo/bar/:secret')
.get(
async (...) => {
...
const secret = _secretString(res.params.secret);
...
},
);
export default {
...
_secretString
};
and now I'm trying to mock the call with sinon.stub like this:
sinon.stub(greatRoute, '_secretString').resolves('abc');
But that doesn't work the way I want it to. When i call the route in my test it still goes into the _secretString function. Am I missing something here? I already tried to put the export in front of the function header like this:
export async function _secretString(param: string): Promise<string>
instead of doing the export default {...} but that didn't help.
You can use rewire package to stub _secretString function. E.g.
index.ts:
async function _secretString(param: string): Promise<string> {
return 'real secret';
}
async function route(req, res) {
const secret = await _secretString(req.params.secret);
console.log(secret);
}
export default {
_secretString,
route,
};
index.test.ts:
import sinon from 'sinon';
import rewire from 'rewire';
describe('61274112', () => {
it('should pass', async () => {
const greatRoute = rewire('./');
const secretStringStub = sinon.stub().resolves('fake secret');
greatRoute.__set__('_secretString', secretStringStub);
const logSpy = sinon.spy(console, 'log');
const mReq = { params: { secret: '123' } };
const mRes = {};
await greatRoute.default.route(mReq, mRes);
sinon.assert.calledWithExactly(logSpy, 'fake secret');
sinon.assert.calledWith(secretStringStub, '123');
});
});
unit test results with coverage report:
fake secret
✓ should pass (1383ms)
1 passing (1s)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 75 | 100 | 50 | 75 |
index.ts | 75 | 100 | 50 | 75 | 2
----------|---------|----------|---------|---------|-------------------
Related
In my controller I have imported the secure util file when I call that util with path as parameter it returns unique Id. but how to call this function in test file using proxyquire and stub.
controller.ts
import { getSecret } from './util/secrect-util'
export function getId(req: Request, res: Response) {
const path='test/path'
const uniueID = getSecret(path);
console.log(uniueID) // prints testUser01
const url=`https://mytest.com?userid=${uniueID}`;
res.redirect(302,url);
}
test.ts
import { Request, Response } from "express";
import * as sinon from 'sinon';
import * as proxyquire from 'proxyquire';
describe('should redirect', () => {
const validurl:string="https://mytest.com?userid=testUser01";
let res:any;
let req:any
beforeEach(() => {
res = {
redirect:sinon.stub()
}
});
it('should get error with invalid path', () => {
const secPath = sinon.stub().returns('/test/invalidPath');
const urlctl = proxyquire('./controller', {
getSecret: { path: secPath },
});
urlctl.getId(req, res);
sinon.assert.calledWithExactly(
res.redirect,
400,
'inValidpath',
)
});
});
getting error while run the test cases. Please assist.
From the doc:
proxyquire({string} request, {Object} stubs)
request: path to the module to be tested e.g., ../lib/foo
stubs: key/value pairs of the form { modulePath: stub, ... }
module paths are relative to the tested module not the test file
therefore specify it exactly as in the require statement inside the tested file
values themselves are key/value pairs of functions/properties and the appropriate override
E.g.
controller.ts:
import { getSecret } from './util/secrect-util';
import { Request, Response } from 'express';
export function getId(req: Request, res: Response) {
const path = 'test/path';
const uniueID = getSecret(path);
console.log(uniueID);
const url = `https://mytest.com?userid=${uniueID}`;
res.redirect(302, url);
}
util/secrect-util.ts:
export function getSecret(path) {
return 'real implementation';
}
controller.test.ts:
import sinon from 'sinon';
import proxyquire from 'proxyquire';
describe('70666283', () => {
it('should pass', () => {
const getSecretStub = sinon.stub().returns('123');
const urlctl = proxyquire('./controller', {
'./util/secrect-util': {
getSecret: getSecretStub,
},
});
const req = {};
const res = { redirect: sinon.stub() };
urlctl.getId(req, res);
sinon.assert.calledWithExactly(getSecretStub, 'test/path');
sinon.assert.calledWithExactly(res.redirect, 302, 'https://mytest.com?userid=123');
});
});
Test result:
70666283
123
✓ should pass (1743ms)
1 passing (2s)
------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------|---------|----------|---------|---------|-------------------
All files | 88.89 | 100 | 50 | 88.89 |
70666283 | 100 | 100 | 100 | 100 |
controller.ts | 100 | 100 | 100 | 100 |
70666283/util | 50 | 100 | 0 | 50 |
secrect-util.ts | 50 | 100 | 0 | 50 | 2
------------------|---------|----------|---------|---------|-------------------
In controller I am redirecting to another url with query params as a state. State is nothing but random UUID. Like given below the controller.
test.controller.ts
import {Request,Response} from "express";
import {v4 as uuid} from "uuid";
export function test(req:Request,res:Response){
const state=uuid();
console.log(state) // 110ec58a-a0f2-4ac4-8393-c866d813b8d1
if(true){
const url=`https://testurl/user?state=${state}`;
res.redirect(302,url);
}
}
and the unit test file like below
test.controller.spec.ts
import {Request,Response} from "express";
import * as sinon from 'sinon';
import {expect} from 'chai';
import * as proxyquire from 'proxyquire';
descripbe('shoud redirect',()=>{
const validurl:string="https://testurl/user?state=110ec58a-a0f2-4ac4-8393-c866d813b8d1";
let res:any;
let req:any
let resstub:{calledWith:any};
beforeEach(()=>{
res={
redirect:()=>{}
}
resstub = sinon.stub(res,'redirect');
});
it('should redirect with valid url',()=>{
const urlctl=proxyquire('test-contriller',{});
urlctl.test(req,res);
expect(resstub.calledWith(302,validurl)).to.be.true;
})
})
when I validate the redirectUrl part it's getting failed because the state value is chaining every time randomly. Can please assist how to do I validate
From the doc How to use Link Seams with CommonJS. We will be using proxyquire to construct our seams so that we can stub the v4 function exported from the uuid package.
controller.ts:
import { Request, Response } from 'express';
import { v4 as uuid } from 'uuid';
export function test(req: Request, res: Response) {
const state = uuid();
if (true) {
const url = `https://testurl/user?state=${state}`;
res.redirect(302, url);
}
}
controller.test.ts:
import sinon from 'sinon';
import proxyquire from 'proxyquire';
describe('shoud redirect', () => {
let res: any;
let req: any;
beforeEach(() => {
res = { redirect: sinon.stub() };
});
it('should redirect with valid url', () => {
const uuidv4Stub = sinon.stub().returns('110ec58a-a0f2-4ac4-8393-c866d813b8d1');
const urlctl = proxyquire('./controller', {
uuid: { v4: uuidv4Stub },
});
urlctl.test(req, res);
sinon.assert.calledWithExactly(
res.redirect,
302,
'https://testurl/user?state=110ec58a-a0f2-4ac4-8393-c866d813b8d1',
);
});
});
test result:
shoud redirect
✓ should redirect with valid url (1592ms)
1 passing (2s)
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 100 | 50 | 100 | 100 |
controller.ts | 100 | 50 | 100 | 100 | 6
---------------|---------|----------|---------|---------|-------------------
I am learning NodeJs and Jest. I am having trouble with unit tests. I just translated my actual code to a simple logic. I have two files as below.
// age.js
function getAge(birthYear) {
const age = 2021-birthYear;
return age
}
module.exports = { getAge }
// user.js
const { getAge } = require("./age");
async function isMinor(){
const bYear = 1991
const age = await getAge(bYear)
if( age <= 18) {
return true
}
return false
}
module.exports = { isMinor }
isMinor calls getAge from another file, I want to test isMinor without actually calling getAge. I referred to this article and wrote my test, but I still encountered some issues.
// user.test.js
const { isMinor } = require("./user")
describe("Age Test", () => {
// Question 1: how can I properly import getAge function here and mock a return value for it? I also tried mockImplementation and mockReturnedValue, but they didn't work
// I don't want to actually invoke getAge function
beforeEach(() => {
jest.mock("./age", () => ({
getAge: () => 99,
}))
})
// Question 2: How can I teardown the moch after the test
afterEach(() =>{
getAge.mockRestore()
})
test("should be an adult", async () => {
const isMinor = await isMinor();
expect(isMinor).toEqual(false);
});
});
I expect to receive 99 from getAge, but it returns null. I appreciate any helps. Thank you.
Since you're only testing isMinor with mock values you'll want to test it with multiple values to cover all of the different scenarios (branches), so you can create a mock for the ./age.js only once by simply calling:
const { getAge } = require('./age');
jest.mock('./age');
It will generate a mock function for each module function only for this test file
Modules that are mocked with jest.mock are mocked only for the file that calls jest.mock. Another file that imports the module will get the original implementation even if it runs after the test file that mocks the module.
So there will be no need for you to restore the original implementation.
The biggest advantage from using auto-mocks is when the method from the implementation (in this case getAge) is removed - the test will fail.
The only thing left to do would be to set the mock's return value that you want to test with. And since it's expected to return a promise you should use .mockResolvedValue()
user.test.js
const { isMinor } = require("./user");
const { getAge } = require('./age');
jest.mock('./age');
describe("Age Test", () => {
describe('getAge returning more than 18', () => {
beforeAll(() => {
getAge.mockResolvedValue(99)
})
test("should be an adult", async () => {
expect(await isMinor()).toEqual(false);
});
})
describe('getAge returning less than 18', () => {
beforeAll(() => {
getAge.mockResolvedValue(13)
})
test("should be a minor", async () => {
expect(await isMinor()).toEqual(true);
});
})
});
Working example
Below example use "jest": "^26.6.3".
user.js:
const { getAge } = require('./age');
async function isMinor() {
const bYear = 1991;
const age = await getAge(bYear);
console.log('age: ', age);
if (age <= 18) {
return true;
}
return false;
}
module.exports = { isMinor };
Option 1: use jest.mock() in beforeEach hook functional scope, it will NOT be hoised to the top of the code. So you need to require modules after mocking by jest.mock() method.
describe('Age Test', () => {
beforeEach(() => {
jest.mock('./age', () => ({
getAge: jest.fn(() => 99),
}));
});
test('should be an adult', async () => {
const { isMinor } = require('./user');
const { getAge } = require('./age');
const actual = await isMinor();
expect(actual).toBeFalsy();
expect(getAge).toBeCalledWith(1991);
});
});
unit test result:
PASS examples/66288290/user.test.js
Age Test
✓ should be an adult (1911 ms)
console.log
age: 99
at examples/66288290/user.js:6:11
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 87.5 | 50 | 100 | 87.5 |
user.js | 87.5 | 50 | 100 | 87.5 | 8
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.197 s
Option 2: use jest.mock() in the module scope, it will be hoisted to the top of the code. Even if you require the modules at the top of the file. The ./age module you require is already be mocked.
const { isMinor } = require('./user');
const { getAge } = require('./age');
jest.mock('./age', () => ({
getAge: jest.fn(() => 99),
}));
describe('Age Test', () => {
afterAll(() => {
jest.resetAllMocks();
});
test('should be an adult', async () => {
const actual = await isMinor();
expect(actual).toBeFalsy();
expect(getAge).toBeCalledWith(1991);
});
});
unit test result:
PASS examples/66288290/user.test.js
Age Test
✓ should be an adult (11 ms)
console.log
age: 99
at examples/66288290/user.js:6:11
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 87.5 | 50 | 100 | 87.5 |
user.js | 87.5 | 50 | 100 | 87.5 | 8
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.502 s
There is code in our codebase like below:
#Validate(Param1)
async post(request, responseHandler) {
// some code
}
I Am trying to test the post function. But want to avoid evaluating the #Validate function. The Validate is a function in another module.
// validator.ts
export const Validate = () => {
// some code
}
How to? .
You could use jest.mock(moduleName, factory, options) create the mocked Validate decorator instead of using the real Validate decorator which may have a lot of validation rules.
E.g.
index.ts:
import { Validate } from './validator';
export class Controller {
#Validate('params')
async post(request, responseHandler) {
console.log('real post implementation');
}
}
validator.ts:
export const Validate = (params) => {
return (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) => {
const oFunc = descriptor.value;
descriptor.value = function inner(...args: any[]) {
console.log('real validator decorator implementation');
// lots of validation
const rval = oFunc.apply(this, args);
return rval;
};
};
};
index.test.ts:
import { Validate } from './validator';
import { mocked } from 'ts-jest/utils';
jest.mock('./validator');
describe('63531414', () => {
afterAll(() => {
jest.resetAllMocks();
});
it('should pass', async () => {
mocked(Validate).mockImplementationOnce((params) => {
return (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) => {
const oFunc = descriptor.value;
descriptor.value = function inner(...args: any[]) {
console.log('mocked validator decorator implementation');
const rval = oFunc.apply(this, args);
return rval;
};
};
});
const { Controller } = require('./');
const logSpy = jest.spyOn(console, 'log');
const ctrl = new Controller();
await ctrl.post({}, () => {});
expect(Validate).toBeCalledWith('params');
expect(logSpy).toBeCalledWith('real post implementation');
});
});
unit test result with coverage report:
PASS src/stackoverflow/63531414/index.test.ts (12.634s)
63531414
✓ should pass (154ms)
console.log node_modules/jest-mock/build/index.js:860
mocked validator decorator implementation
console.log node_modules/jest-mock/build/index.js:860
real post implementation
--------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
--------------|----------|----------|----------|----------|-------------------|
All files | 45.45 | 100 | 25 | 45.45 | |
index.ts | 100 | 100 | 100 | 100 | |
validator.ts | 14.29 | 100 | 0 | 14.29 | 2,3,4,5,7,8 |
--------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 14.354s
source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/63531414
I am using supertest for writing integration-tests for POST Api: api/v1/students?id="someId" which should throw a 4xx code if the id is already present.
import request from "supertest";
import { applyMiddleware, applyRoutes } from "../../src/utils";
import routes from "../../src/routes";
import errorHandlers from "../../src/middleware/errorHandlers";
describe("student routes", () => {
let router: Router;
beforeEach(() => {
router = express();
applyRoutes(routes, router);
applyMiddleware(errorHandlers, router);
});
afterEach(function () {
});
test("Create student", async () => {
const response = await request(router).post("/api/v1/students?id=lee");
expect(response.status).toEqual(201);
});
test("Create duplicate student test", async () => {
const response1 = await request(router).post("/api/v1/students?id=lee");
const response2 = await request(router).post("/api/v1/students?id=lee");
expect(response2.status).toEqual(409);
});
});
The problem is the first and second tests are not independent. The student with id lee created in the first test is already present when the second test is run. I want to reset the express() and make the tests independent of each other. How shall I proceed?
You should clear the data in memory db after each unit test case.
E.g.
app.ts:
import express, { Router } from 'express';
const app = express();
const router = Router();
export const memoryDB: any = {
students: [],
};
router.post('/api/v1/students', (req, res) => {
const studentId = req.params.id;
const foundStudent = memoryDB.students.find((student) => student.id === studentId);
if (foundStudent) {
return res.sendStatus(409);
}
memoryDB.students.push({ id: studentId });
res.sendStatus(201);
});
app.use(router);
export default app;
app.test.ts:
import request from 'supertest';
import app, { memoryDB } from './app';
describe('student routes', () => {
afterEach(() => {
memoryDB.students = [];
});
test('Create student', async () => {
const response = await request(app).post('/api/v1/students?id=lee');
expect(response.status).toEqual(201);
});
test('Create duplicate student test', async () => {
const response1 = await request(app).post('/api/v1/students?id=lee');
const response2 = await request(app).post('/api/v1/students?id=lee');
expect(response2.status).toEqual(409);
});
});
Integration test result with 100% coverage:
PASS src/stackoverflow/57565585/app.test.ts
student routes
✓ Create student (34ms)
✓ Create duplicate student test (17ms)
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
app.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 4.276s, estimated 9s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57565585