I have a basic CLI program built using yargs. I'm able to cover test cases for exported functions in the application.
As you can see below test coverage is not done from line 12-18 which. How do we write unit test coverage for third-party package like yargs?
index.js
const yargs = require('yargs');
const { hideBin } = require('yargs/helpers');
const greet = (name) => {
return `Welcome ${name}`;
};
yargs(hideBin(process.argv)).command(
'run [name]',
'print name',
(yargs) => {
yargs.positional('name', { describe: 'Your name', type: 'string' });
},
(args) => {
const { name } = args;
const greetMsg = greet(name);
console.log(greetMsg);
}
).argv;
module.exports = { greet };
index.test.js
const { greet } = require('./index')
describe.only('greeting', () => {
it('greet', async () => {
const greetMsg = greet('test')
expect(greetMsg).toBe('Welcome test')
})
})
test coverage
PASS ./index.test.js
greeting
✓ greet (5 ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 63.64 | 100 | 33.33 | 63.64 |
index.js | 63.64 | 100 | 33.33 | 63.64 | 12-18
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.316 s, estimated 2 s
Ran all test suites.
You can use jest.doMock(moduleName, factory, options) to mock yargs and yargs/helpers module. Since the yargs function in the module scope will be executed when requiring the module. You need to use jest.resetModules() to resets the module registry - the cache of all required modules for each test case before requiring the index.js module.
This is useful to isolate modules where local state might conflict between tests.
E.g.
index.js:
const yargs = require('yargs');
const { hideBin } = require('yargs/helpers');
const greet = (name) => {
return `Welcome ${name}`;
};
yargs(hideBin(process.argv)).command(
'run [name]',
'print name',
(yargs) => {
yargs.positional('name', { describe: 'Your name', type: 'string' });
},
(args) => {
const { name } = args;
const greetMsg = greet(name);
console.log(greetMsg);
}
).argv;
module.exports = { greet };
index.test.js:
describe.only('greeting', () => {
beforeEach(() => {
jest.resetModules();
});
it('greet', () => {
const { greet } = require('./index');
const greetMsg = greet('test');
expect(greetMsg).toBe('Welcome test');
});
it('should pass', () => {
jest.doMock('yargs');
jest.doMock('yargs/helpers');
const yargs = require('yargs');
const { hideBin } = require('yargs/helpers');
const mArgv = {
command: jest.fn().mockImplementation(function (command, description, builder, handler) {
builder(this);
handler({ name: 'teresa teng' });
return this;
}),
argv: {},
positional: jest.fn(),
};
yargs.mockReturnValueOnce(mArgv);
require('./');
expect(hideBin).toBeCalled();
expect(yargs).toBeCalled();
expect(mArgv.positional).toBeCalledWith('name', { describe: 'Your name', type: 'string' });
});
});
test result:
PASS examples/67830954/index.test.js (7.17 s)
greeting
✓ greet (355 ms)
✓ should pass (23 ms)
console.log
Welcome teresa teng
at examples/67830954/index.js:18:13
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 7.668 s, estimated 8 s
Related
I'm writing a toy app to learn more about Serverless Framework and AWS AppSync etc.
I'm trying to do TDD as much as possible. I'm using mock-apollo-client to mock the ApolloClient, and I've run into a problem. When trying to write a test to make sure the arguments to the query are passed, the test always returns a 401 Unauthorized error. It seems as though the real end point is still being called, because when a valid x-api-key is added to the instantiation of the ApolloClient, the test returns the real value from the AppSync server, and not the mock value I'm expecting. I'm using a mock, not spy, so I'm not expecting the real end point to actually be hit. Furthermore When I do add a valid x-api-key the test fails because the function is never called.
api › recipes › Given a valid recipe id › Should call query with the id as a param
expect(jest.fn()).toBeCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 0
I'm expected the test to fail, because the query currently isn't called with any arguments, but instead it fails because the mock function is never called.
What am I doing wrong?
Code File
import { ApolloClient, gql, InMemoryCache } from '#apollo/client';
const client = new ApolloClient({
uri: 'https://redacted.appsync-api.redacted.amazonaws.com/graphql',
headers: {
'x-api-key': 'key-redacted',
},
cache: new InMemoryCache(),
});
export const GET_RECIPE_QUERY = gql`
query {
getRecipe (title:"Lemon Cheese Cake") {
title,
ingredients{
name,
amount,
unit
},
steps
}
}
`;
const gqlQuery = (title) => {
return client
.query({
query: GET_RECIPE_QUERY,
variables : { title }
});
};
export const getRecipe = async (id) => {
const result = await gqlQuery(id);
return result.data.getRecipe;
};
Test file
import { createMockClient } from 'mock-apollo-client';
import { GET_RECIPE_QUERY, getRecipe } from './recipes';
const mockRecipe = {
title: 'Luke\'s Chocolate Orange',
ingredients: [
{
name: 'Orange',
amount: 1,
},
{
name: 'Chocolate',
amount: 250,
unit: 'grams',
},
],
steps: [
'Peel orange',
'Open chocolate',
'Eat chocolate',
'Throw orange away',
],
};
const mockClient = createMockClient();
const queryHandler = jest.fn().mockResolvedValue({data: {recipe: mockRecipe}});
mockClient.setRequestHandler(GET_RECIPE_QUERY, queryHandler);
describe('api', () => {
describe('recipes', () => {
describe('Given a valid recipe id', () => {
it('Should call query with the id as a param', async () => {
const id = 'Luke\'s Chocolate Orange';
const result = await getRecipe(id);
expect(queryHandler).toBeCalledTimes(1);
expect(queryHandler).toBeCalledWith(id);
});
});
});
});
Packages
Versions
#apollo/client
3.5.10
graphql
16.3.0
#testing-library/jest-dom
5.16.2
#testing-library/react
12.1.4
#testing-library/user-event
13.5.0
jest
27.5.1
mock-apollo-client
1.2.0
mock-apollo-client always use the with ApolloProvider, so that you pass the mock apollo client via React context Provider to descendant components.
However, your code cannot pass the mock apollo client to the component in this way. Your code initiates requests directly from the Apollo Client. We need to intercept these GraphQL requests. There are several ways to do this such as msw. However, I'll continue to use the mock-apollo-client library to demonstrate.
You need to mock ApolloClient class of the #apollo/client module. We need to use Mocking Partials, we don't want to mock other things exported from #apollo/client. Since the mock-apollo-client library already provides createMockClient function to create mocked apollo client, we don't need to mock by ourself.
An working example:
recipes.ts:
import { ApolloClient, gql, InMemoryCache } from '#apollo/client';
const client = new ApolloClient({
uri: 'https://redacted.appsync-api.redacted.amazonaws.com/graphql',
headers: {
'x-api-key': 'key-redacted',
},
cache: new InMemoryCache(),
});
export const GET_RECIPE_QUERY = gql`
query {
getRecipe(title: "Lemon Cheese Cake") {
title
ingredients {
name
amount
unit
}
steps
}
}
`;
const gqlQuery = (title) => {
return client.query({
query: GET_RECIPE_QUERY,
variables: { title },
});
};
export const getRecipe = async (id) => {
const result = await gqlQuery(id);
return result.data.getRecipe;
};
recipes.test.ts:
import { createMockClient } from 'mock-apollo-client';
const mockRecipe = {
title: "Luke's Chocolate Orange",
ingredients: [
{ name: 'Orange', amount: 1, unit: 'abc' },
{ name: 'Chocolate', amount: 250, unit: 'grams' },
],
steps: ['Peel orange', 'Open chocolate', 'Eat chocolate', 'Throw orange away'],
};
const mockClient = createMockClient();
describe('api', () => {
describe('recipes', () => {
describe('Given a valid recipe id', () => {
beforeEach(() => {
jest.resetModules();
});
it('Should call query with the id as a param', async () => {
jest.doMock('#apollo/client', () => {
return {
...jest.requireActual('#apollo/client'),
ApolloClient: jest.fn(() => mockClient),
};
});
const queryHandler = jest.fn().mockResolvedValue({ data: { getRecipe: mockRecipe } });
const { GET_RECIPE_QUERY, getRecipe } = require('./recipes');
mockClient.setRequestHandler(GET_RECIPE_QUERY, queryHandler);
const title = "Luke's Chocolate Orange";
const result = await getRecipe(title);
expect(result).toEqual(mockRecipe);
expect(queryHandler).toBeCalledWith({ title });
});
});
});
});
Test result:
PASS src/stackoverflow/71612556/recipes.test.ts
api
recipes
Given a valid recipe id
✓ Should call query with the id as a param (91 ms)
------------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------------|---------|----------|---------|---------|-------------------
All files | 90.91 | 100 | 66.67 | 90.91 |
mocks | 75 | 100 | 0 | 75 |
handlers.js | 66.67 | 100 | 0 | 66.67 | 14
server.js | 100 | 100 | 100 | 100 |
stackoverflow/71612556 | 100 | 100 | 100 | 100 |
recipes.ts | 100 | 100 | 100 | 100 |
------------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.775 s
You can find the source code here
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'm working on some tests for an electron app I'm building. I'm running into the error below. I'm new to jest, so I imagine it's due to an incorrect setup. Any idea where I'm going wrong?
Error: Cannot find module 'ipcMain' from 'ipcMainEvents.spec.js'
//myEvents.ts
import { ipcMain } from 'electron'
export class IpcMainEvents {
constructor() {
ipcMain.on('openPlaywright', this.openPlaywright)
ipcMain.on('openPreviewBrowser', this.openPlaywright)
}
openPlaywright(event, arg) {
console.log('openPlaywright')
}
openPreviewBrowser(event, arg) {
console.log('openPreviewBrowser')
}
}
//myEvents.spec.ts
import {IpcMainEvents} from './ipcMainEvents'
import {ipcMain} from 'electron'
jest.mock('ipcMain')
describe('Should test the ipcMain events', () => {
let component;
let addSpy
beforeEach(()=>{
component = new IpcMainEvents()
})
it('should attach the eventListeners', () => {
expect(component.ipcMain.on.calls.all()[0].args[0]).toEqual('openPlaywright'); //<----Errors here
expect(component.ipcMain.on.calls.all()[1].args[0]).toEqual('openPreviewBrowser');
expect(component.ipcMain.on.calls.count()).toEqual(2);
});
});
Firstly, you should mock electron package, not ipcMain function.
Second, you should access the calls property of mocked function via .mock property.
E.g.
myEvents.ts:
import { ipcMain } from 'electron';
export class IpcMainEvents {
constructor() {
ipcMain.on('openPlaywright', this.openPlaywright);
ipcMain.on('openPreviewBrowser', this.openPlaywright);
}
openPlaywright(event, arg) {
console.log('openPlaywright');
}
openPreviewBrowser(event, arg) {
console.log('openPreviewBrowser');
}
}
myEvents.spec.ts:
import { IpcMainEvents } from './myEvents';
import { ipcMain } from 'electron';
jest.mock(
'electron',
() => {
const mockIpcMain = {
on: jest.fn().mockReturnThis(),
};
return { ipcMain: mockIpcMain };
},
{ virtual: true },
);
describe('Should test the ipcMain events', () => {
let component;
let addSpy;
beforeEach(() => {
component = new IpcMainEvents();
});
it('should attach the eventListeners', () => {
expect(ipcMain.on.mock.calls[0][0]).toEqual('openPlaywright');
expect(ipcMain.on.mock.calls[1][0]).toEqual('openPreviewBrowser');
expect(ipcMain.on.mock.calls).toHaveLength(2);
});
});
unit test results with coverage report:
PASS stackoverflow/61562193/myEvents.spec.js (10.657s)
Should test the ipcMain events
✓ should attach the eventListeners (3ms)
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 80 | 100 | 50 | 77.78 |
myEvents.js | 80 | 100 | 50 | 77.78 | 10,14
-------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 11.917s
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