I have the following function within my ES6 User class which searches for a user given a string.
// Search for a user by their pNick, includes partial matching
static getBypNick(pNick = '') {
// Define our search criteria regex and normalise to lower case
const userSearchRegex = new RegExp(`^${pNick.toLowerCase()}`, 'i')
return new Promise((resolve, reject) => {
// Search user collection for pNick using regex like search and return array of results
userTable.find({
_pNick: userSearchRegex
}).sort({
_pNick: 1
}).exec(function (err, result) {
// If error reject
if (err) {
return reject(err)
}
const userArray = result.map((user) => {
return new User(
user._pNick,
user._firstName,
user._userName,
user._phoneNumber,
user._userID)
})
// Return user records if found
return resolve(userArray)
})
})
}
Whilst I can easily test the success routes using Jest I'm struggling to understand how I can invoke the error cases, especially around the .exec method within the function to invoke my reject routes in the promise.
I understand that I can use various Jest features such as mockImplementation but I just can't figure out the best case in this scenario. The database being used behind the scenes is NeDB, I'm pretty positive I just need to force the .exec portion to return an error and then I should be catching this in my promise.
I have no intention of testing the underlying NeDB library as it has its own tests which execute successfully so this is really all about my own methods.
My coverage thus far:
Here is the unit test solution:
userRepo.js:
import { userTable } from './userTable';
import { User } from './user';
export class UserRepo {
static getBypNick(pNick = '') {
const userSearchRegex = new RegExp(`^${pNick.toLowerCase()}`, 'i');
return new Promise((resolve, reject) => {
userTable
.find({
_pNick: userSearchRegex,
})
.sort({
_pNick: 1,
})
.exec(function(err, result) {
if (err) {
return reject(err);
}
const userArray = result.map((user) => {
return new User(user._pNick, user._firstName, user._userName, user._phoneNumber, user._userID);
});
return resolve(userArray);
});
});
}
}
userTable.js:
const userTable = {
find() {
return this;
},
sort() {
return this;
},
exec(fn) {
console.log('real exec');
fn();
},
};
user.js:
export class User {
constructor(nick, firstName, userName, phoneNumber, userId) {
this.nick = nick;
this.firstName = firstName;
this.userName = userName;
this.phoneNumber = phoneNumber;
this.userId = userId;
}
}
userRepo.test.js:
import { UserRepo } from './userRepo';
import { userTable } from './userTable';
jest.mock('./userTable', () => {
const mUserTable = {
find: jest.fn().mockReturnThis(),
sort: jest.fn().mockReturnThis(),
exec: jest.fn(),
};
return { userTable: mUserTable };
});
describe('47587358', () => {
afterAll(() => {
jest.resetAllMocks();
});
it('should get user by nick', async () => {
const mResult = [{ _pNick: '_pNick', _firstName: '_firstName', _userName: '_userName', _phoneNumber: 123456, _userID: 1 }];
userTable.exec.mockImplementationOnce((fn) => {
fn(null, mResult);
});
const actual = await UserRepo.getBypNick('jest');
expect(actual).toEqual(
expect.arrayContaining([
expect.objectContaining({
nick: expect.any(String),
firstName: expect.any(String),
userName: expect.any(String),
phoneNumber: expect.any(Number),
userId: expect.any(Number),
}),
]),
);
expect(userTable.find).toBeCalledWith({ _pNick: new RegExp(`^jest`, 'i') });
expect(userTable.sort).toBeCalledWith({ _pNick: 1 });
expect(userTable.exec).toBeCalledWith(expect.any(Function));
});
it('should handle error', async () => {
const mError = new Error('network');
userTable.exec.mockImplementationOnce((fn) => {
fn(mError, null);
});
await expect(UserRepo.getBypNick('jest')).rejects.toThrowError('network');
expect(userTable.find).toBeCalledWith({ _pNick: new RegExp(`^jest`, 'i') });
expect(userTable.sort).toBeCalledWith({ _pNick: 1 });
expect(userTable.exec).toBeCalledWith(expect.any(Function));
});
});
unit test result with coverage report:
PASS src/stackoverflow/47587358/userRepo.test.js (9.448s)
47587358
✓ should get user by nick (10ms)
✓ should handle error (3ms)
-------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-------------|----------|----------|----------|----------|-------------------|
All files | 100 | 66.67 | 100 | 100 | |
user.js | 100 | 100 | 100 | 100 | |
userRepo.js | 100 | 66.67 | 100 | 100 | 5 |
-------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 10.66s
source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/47587358
Related
So I tried to mock a jwt.verify function. below is the function itself and then the mock.
try {
const decodedJwt = new Promise((resolve, reject) => {
jwt.verify(
token,
getPublicKey,
{
algorithms: ['RS256'],
ignoreExpiration: false,
},
(decodeError, decodedValue) => {
if (decodeError) {
reject(decodeError);
}
resolve(decodedValue);
}
);
});
return await decodedJwt;
} catch (error) {
logger.error(error);
return null;
}
The Mock for the verify part
export const jwtVerifyMock = jest.fn();
jest.mock('jsonwebtoken', () => {
return {
decode: jwtDecodeMock,
verify: jwtVerifyMock,
};
});
then this is how I use it in my test
it('decodeJWT should should verify token', async () => {
jwtVerifyMock.mockImplementation(async () => Promise.resolve({ exp: config.exp }));
return decodeJwt(config.jwtToken, true).then(async (value) => {
console.log('payload2', value);
const payload = value as Record<string, unknown>;
expect(payload.exp).toEqual(config.exp);
});
});
But for some reason, it doesn't get resolved. and I get a
Async callback was not invoked within the 60000 ms timeout Error
I tried using mockReturn value but that doesn't work as well. So the next thing I'm guessing is that the resolve function that was passed as a callback to the jwt.verify is where my problem lie.
You should mock the implementation of jwt.verify() and execute the callback function manually with different arguments.
E.g.
decode.ts:
import jwt from 'jsonwebtoken';
export async function decode(token, getPublicKey) {
try {
const decodedJwt = new Promise((resolve, reject) => {
jwt.verify(
token,
getPublicKey,
{
algorithms: ['RS256'],
ignoreExpiration: false,
},
(decodeError, decodedValue) => {
if (decodeError) {
reject(decodeError);
}
resolve(decodedValue);
}
);
});
return await decodedJwt;
} catch (error) {
console.error(error);
return null;
}
}
decode.test.ts:
import { decode } from './decode';
import jwt, { JsonWebTokenError } from 'jsonwebtoken';
describe('66966317', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('should decode value', async () => {
const decodedvalue = { name: 'teresa teng' };
const verifySpy = jest.spyOn(jwt, 'verify').mockImplementationOnce((token, getPublicKey, options, callback) => {
callback!(null, decodedvalue);
});
const actual = await decode('teresa teng', true);
expect(actual).toEqual({ name: 'teresa teng' });
expect(verifySpy).toBeCalledWith(
'teresa teng',
true as any,
{ algorithms: ['RS256'], ignoreExpiration: false },
expect.any(Function)
);
});
it('should handle decode error', async () => {
const decodedError = new JsonWebTokenError('network');
const verifySpy = jest.spyOn(jwt, 'verify').mockImplementationOnce((token, getPublicKey, options, callback) => {
callback!(decodedError, undefined);
});
const actual = await decode('teresa teng', true);
expect(actual).toBeNull();
expect(verifySpy).toBeCalledWith(
'teresa teng',
true as any,
{ algorithms: ['RS256'], ignoreExpiration: false },
expect.any(Function)
);
});
});
unit test result:
PASS examples/66966317/decode.test.ts (7.701 s)
66966317
✓ should decode value (3 ms)
✓ should handle decode error (17 ms)
console.error
JsonWebTokenError { name: 'JsonWebTokenError', message: 'network' }
23 | return await decodedJwt;
24 | } catch (error) {
> 25 | console.error(error);
| ^
26 | return null;
27 | }
28 | }
at Object.<anonymous> (examples/66966317/decode.ts:25:13)
at Generator.throw (<anonymous>)
at rejected (examples/66966317/decode.ts:1046:32)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
decode.ts | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 8.749 s
I am new to Sinon, but I have looked around for a while trying to find an answer for this question..
I have a function I need to test, it returns a promise to call another function will callback..
Below is the function that I need to write test case for:
const bookService = require(./bookService);
const getBook = () => {
const bookName = "book";
const bookID = '111';
return new Promise((resolve, reject) => {
bookService.InfoRequest(bookName, bookID, 'GET', res => {
if(res.error){
reject(res);
}else{
const list = res['allPages'] || [];
if(list = []){
resolve({
pageNumber: 0,
note: "book is no longer exist"
});
}else{
resolve(res['allPages']);
}
}
})
})
}
The bookService.InfoRequest method is not returning anything it returns the callback(res);
I have tried stub the bookService.InfoRequest method, but since it is not returning anything I am not sure how can I modified the callback parameter to test all 3 branchs..
I am using Ava, so I tried something like this:
test('getBook Error Block', t=> {
const stub = sinon.stub(bookService, InfoRequest);
stub.callsFake(() => {
return { error: true };
});
return obj.getBook().then(res => {
t.deepEqual(res, []);
}).catch(error => {
console.log(error.error);
t.deepEqual(error.error, true);
})
})
This is the test cases for the first Branch, the reject(res) branch. There are 2 more very similar only with different callFake.
But the problem is I am not able to print the error out and test shows it passed, but if I change true to false, it also pass...
The stubbed implementation by .callFake() is not correct. The bookService.InfoRequest() method accepts a callback parameter, the res is passed to this callback. So you need to provide a stubbed implementation with this callback function and pass your fake error.
E.g.
bookService.js:
function InfoRequest(bookName, bookId, method, cb) {}
module.exports = { InfoRequest };
obj.js:
const bookService = require('./bookService');
const getBook = () => {
const bookName = 'book';
const bookID = '111';
return new Promise((resolve, reject) => {
bookService.InfoRequest(bookName, bookID, 'GET', (res) => {
if (res.error) {
reject(res);
} else {
const list = res['allPages'] || [];
if ((list = [])) {
resolve({
pageNumber: 0,
note: 'book is no longer exist',
});
} else {
resolve(res['allPages']);
}
}
});
});
};
module.exports = { getBook };
obj.test.js:
const obj = require('./obj');
const bookService = require('./bookService');
const sinon = require('sinon');
const test = require('ava');
test('getBook Error Block', (t) => {
const res = { error: new Error('network') };
const stub = sinon.stub(bookService, 'InfoRequest').callsFake((bookName, bookId, method, callback) => {
callback(res);
});
return obj.getBook().catch((res) => {
t.deepEqual(res.error, res.error);
sinon.assert.calledWith(stub, 'book', '111', 'GET', sinon.match.func);
});
});
test result:
> nyc ava --timeout=3000 "/Users/dulin/workspace/github.com/mrdulin/expressjs-research/src/stackoverflow/66702460/obj.test.js"
1 test passed
----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------|---------|----------|---------|---------|-------------------
All files | 71.43 | 16.67 | 75 | 71.43 |
bookService.js | 100 | 100 | 0 | 100 |
obj.js | 69.23 | 16.67 | 100 | 69.23 | 11-18
----------------|---------|----------|---------|---------|-------------------
My code looks like this
const { MyClient } = require('some-service')
const invokeMe = async (input1, input2) => {
const client = new MyClient({
name: 'my-name'
})
return await client.invoke({
input1,
input2
}).catch((err) => {
throw err
})
}
This is what I have, how do I properly mock this service and spy on what it's called with?
const { MyClient } = require('some-service')
describe('test my client', () => {
it('invoke my client', () => {
const response = {
data: []
}
expect(invokeMe('abc', '123')).resolves.toEqual(response)
expect(MyClient).toBeCalledWith({
input1: 'abc',
input2: '123'
})
})
})
Edit: Why does the below still call the original function?
it('invoke my client', () => {
const mockInvoke = jest.fn().mockImplementation(() => Promise.resolve({
data: []
}))
const mockMyClient = () => {
return { invoke: mockInvoke }
}
const mockSomeService = {
MyClient: mockMyClient
}
jest.doMock('some-service', () => mockSomeService
...
})
You can use jest.mock(moduleName, factory, options) mock the imported service
E.g.
index.js:
const { MyClient } = require('./some-service');
const invokeMe = async (input1, input2) => {
const client = new MyClient({
name: 'my-name',
});
return await client
.invoke({
input1,
input2,
})
.catch((err) => {
throw err;
});
};
module.exports = invokeMe;
some-service.js:
class MyClient {
async invoke(input1, input2) {
return 'real response';
}
}
module.exports = { MyClient };
index.test.js:
const invokeMe = require('./');
const { MyClient } = require('./some-service');
jest.mock('./some-service', () => {
const mMyClient = { invoke: jest.fn() };
return { MyClient: jest.fn(() => mMyClient) };
});
describe('60008679', () => {
it('should invoke', async () => {
const client = new MyClient();
client.invoke.mockResolvedValueOnce('fake response');
const actual = await invokeMe('a', 'b');
expect(actual).toBe('fake response');
expect(MyClient).toBeCalledWith({ name: 'my-name' });
expect(client.invoke).toBeCalledWith({ input1: 'a', input2: 'b' });
});
it('should handle error', async () => {
const client = new MyClient();
const mError = new Error('some error');
client.invoke.mockRejectedValueOnce(mError);
await expect(invokeMe('a', 'b')).rejects.toThrowError(mError);
expect(MyClient).toBeCalledWith({ name: 'my-name' });
expect(client.invoke).toBeCalledWith({ input1: 'a', input2: 'b' });
});
});
Unit test results with 100% coverage:
PASS src/stackoverflow/60008679/index.test.js (11.029s)
60008679
✓ should invoke (8ms)
✓ should handle error (4ms)
----------|----------|----------|----------|----------|-------------------|
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: 12.314s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/60008679
I have a pretty simple module that use pg (node-postgre lib) module,
I'd like to implement a Jest test and while mocking the pg module I would like to run it's callback function to see the console.log runs and my callback is being invoked
I have mocked the module and tried to spy and replace the 'query' method but it failed and crushed,
any Idea what am I doing wrong?
Test Subject:
import {Pool} from 'pg';
const pool = new Pool();
module.exports = {
query: (text, params, callback) => {
const start = Date.now();
return pool.query(text, params, (err, res) => {
const duration = Date.now() - start;
console.log('executed query', {text, duration, rows: res.rowCount});
callback(err, res);
});
}
};
Test:
jest.mock('pg');
import module from './index';
import { Pool } from 'pg'
beforeAll(() => {
Pool.mockImplementation(()=>{return jest.fn()});
});
it('callback is called', () => {
const cb = (err, res) => true;
const query = jest.spyOn(Pool, "query"); // <---- Not right, Error
query.mockImplementation((a,b,c) => c({},{}));
const resolve = module.query('QUERY TEXT', { a: 1, b: 2}, cb);
resolve(); // <---- Not what I expect
expect(cb).toBeCalled();
});
});
Error thrown:
Error: Cannot spy the query property because it is not a function; undefined given instead
20 | it('callback is called', () => {
21 | const cb = (err, res) => true;
> 22 | const query = jest.spyOn(Pool, "query");
| ^
23 | query.mockImplementation((a,b,c) => c({},{}));
24 | const resolve = module.query('QUERY TEXT', { a: 1, b: 2}, cb);
25 | resolve();
at ModuleMockerClass.spyOn (node_modules/jest-mock/build/index.js:697:15)
at Object.spyOn (src/db/index.test.js:22:24)
Thanks
Here is the unit test solution:
index.js:
import { Pool } from 'pg';
const pool = new Pool();
module.exports = {
query: (text, params, callback) => {
const start = Date.now();
return pool.query(text, params, (err, res) => {
const duration = Date.now() - start;
console.log('executed query', { text, duration, rows: res.rowCount });
callback(err, res);
});
}
};
index.spec.js:
import mod from '.';
import { Pool } from 'pg';
jest.mock('pg', () => {
const mPool = {
query: jest.fn()
};
return { Pool: jest.fn(() => mPool) };
});
const pool = new Pool();
afterEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
});
it('callback is called', done => {
let queryCallback;
pool.query.mockImplementation((text, params, callback) => {
queryCallback = callback;
});
const logSpy = jest.spyOn(console, 'log');
const userCallback = jest.fn();
mod.query('text', 'params', userCallback);
const mRes = { rowCount: 1 };
queryCallback(null, mRes);
expect(pool.query).toBeCalledWith('text', 'params', queryCallback);
expect(userCallback).toBeCalledWith(null, mRes);
expect(logSpy).toBeCalledWith('executed query', { text: 'text', duration: expect.any(Number), rows: 1 });
done();
});
Unit test result with 100% coverage:
PASS src/stackoverflow/52831401/index.spec.js
✓ callback is called (15ms)
console.log node_modules/jest-mock/build/index.js:860
executed query { text: 'text', duration: 0, rows: 1 }
----------|----------|----------|----------|----------|-------------------|
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: 1 passed, 1 total
Snapshots: 0 total
Time: 4.026s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/52831401
I am using Auth0 to manage authentication for my React App. Here's the function that I want to test:
login(username: string, password: string) {
return new Promise((resolve, reject) => {
this.auth0.client.login({
realm: 'Username-Password-Authentication',
username,
password,
}, (err, authResult) => {
if (err) {
reject(err);
}
if (authResult && authResult.idToken && authResult.accessToken) {
resolve(authResult.idToken);
}
});
});
}
auth0 is instantiated in the following manner:
constructor(clientID: string, domain: string, redirectUri: string) {
// Configure auth0
this.auth0 = new auth0.WebAuth({
clientID,
domain,
responseType: 'token id_token',
redirectUri,
});
}
I have the following two tests:
I want to see if I can instantiate the AuthService class.
I want to see if I can login using a username/password combination
Here's the test file that I wrote:
jest.unmock('../AuthService');
import AuthService from '../AuthService';
describe('Auth0 Library', () => {
test('Should be able to instantiate', () => {
const auth0 = new AuthService('clientID', 'domain');
expect(auth0).toEqual(expect.anything());
});
});
describe('Auth0 Login', () => {
test('Fetch token for existing user', () => {
const auth0 = new AuthService('clientID', 'domain');
auth0.login('email', 'pw')
.then((idToken) => {
console.log('idToken ', idToken);
expect(auth0).toEqual(expect.anything());
});
});
});
The first test runs as expected. However, the second test never works. The promise that is returned is seemingly never resolved, and I never see the console.log.
Could someone please explain to me what I am doing wrong? I am fairly new when it comes to writing jest tests.
Here is the unit test solution for using Promise and error-first callback together.
Folder structure:
.
├── AuthService.spec.ts
├── AuthService.ts
└── auth0
├── WebAuth.ts
└── index.ts
1 directory, 4 files
E.g.
AuthService.ts:
import * as auth0 from './auth0';
export class AuthService {
private auth0: any;
constructor(clientID: string, domain: string, redirectUri: string) {
this.auth0 = new auth0.WebAuth({
clientID,
domain,
responseType: 'token id_token',
redirectUri
});
}
public login(username: string, password: string) {
return new Promise((resolve, reject) => {
this.auth0.client.login(
{
realm: 'Username-Password-Authentication',
username,
password
},
(err, authResult) => {
if (err) {
reject(err);
}
if (authResult && authResult.idToken && authResult.accessToken) {
resolve(authResult.idToken);
}
}
);
});
}
}
auth0/WebAuth.ts:
export class WebAuth {
public client = {
login(options, callback) {
callback(null, {});
}
};
constructor(...args: any) {}
}
auth0/index.ts:
export * from './WebAuth';
AuthService.spec.ts:
import { AuthService } from './AuthService';
const authService = new AuthService('clientid', 'domain', 'redirectUri');
describe('AuthService', () => {
describe('#login', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('should login successfully and return id token', async done => {
let loginCallback;
jest.spyOn(authService['auth0']['client'], 'login').mockImplementation((options, callback) => {
loginCallback = callback;
done();
});
const actualValue = authService.login('username', 'password');
const mAuthResult = { idToken: '123', accessToken: '321' };
loginCallback(null, mAuthResult);
await expect(actualValue).resolves.toBe('123');
});
it('should login failed', async done => {
let loginCallback;
jest.spyOn(authService['auth0']['client'], 'login').mockImplementation((options, callback) => {
loginCallback = callback;
done();
});
const actualValue = authService.login('username', 'password');
const mError = new Error('network error');
loginCallback(mError, null);
await expect(actualValue).rejects.toThrowError(mError);
});
});
});
Unit test result with 100% coverage for AuthService.ts:
PASS src/stackoverflow/42137891/AuthService.spec.ts
AuthService
#login
✓ should login successfully and return id token (5ms)
✓ should login failed (2ms)
---------------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
---------------------|----------|----------|----------|----------|-------------------|
All files | 95 | 100 | 87.5 | 94.12 | |
42137891-todo | 100 | 100 | 100 | 100 | |
AuthService.ts | 100 | 100 | 100 | 100 | |
42137891-todo/auth0 | 85.71 | 100 | 66.67 | 83.33 | |
WebAuth.ts | 83.33 | 100 | 66.67 | 80 | 4 |
index.ts | 100 | 100 | 100 | 100 | |
---------------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 3.728s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/42137891