Fake is not a spy - unit-testing

So i have a method that calls an https request. i've successfully stubbed the calls in other tests but this one is beating me an it just won't work.
in my beforeEach i create the stub
in my afterEach i do stub restore
in the test i create four stub calls.
stub.onCall(0).callsFake((arg1, arg2, arg3, cb, arg5) => {return cb(null, {some object0})
stub.onCall(1).callsFake((arg1, arg2, arg3, cb, arg5) => {return cb(null, {some object1})
stub.onCall(2).callsFake((arg1, arg2, arg3, cb, arg5) => {return cb(null, {some object2})
stub.onCall(3).callsFake((arg1, arg2, arg3, cb, arg5) => {return cb(null, {some object3})
then after my ACT statement i attempt to assert the stubs are called with the correct parameters
sinon.assert.calledWith(stub.firstCall, "blah","blah","blah",, sinon.match.any, "blah")
sinon.assert.calledWith(stub.secondCall, "blah","blah","blah",, sinon.match.any, "blah", "blah")
sinon.assert.calledWith(stub.thirdCall, "blah","blah","blah",, sinon.match.any, "blah")
sinon.assert.calledWith(stub.call(3), "blah","blah","blah",, sinon.match.any, "blah"
Unfortunately my test now reports
AssertError: fake is not a spy
I've console.logs in my code under test and they are reporting my stubs being called.
a call to console.log(stub.callCount) return 1 not 4
if i comment all but the first sinon.assert the test passes.
So my question is what am i doing wrong
I'll post my full setup below
` describe('Update Redis', () => {
const balances = {
"TRTLv2Fyavy8CXG8BPEbNeCHFZ1fuDCYCZ3vW5H5LXN4K2M2MHUpTENip9bbavpHvvPwb4NDkBWrNgURAd5DB38FHXWZyoBh4w": 12000
}
const minPayoutLevel = {
"TRTLv2Fyavy8CXG8BPEbNeCHFZ1fuDCYCZ3vW5H5LXN4K2M2MHUpTENip9bbavpHvvPwb4NDkBWrNgURAd5DB38FHXWZyoBh4w": 10000
}
const endpoint1 = '/transactions/prepare/advanced'
const endpoint2 = '/transactions/send/prepared'
beforeEach(() => {
stub = sinon.stub(apiInterfaces, 'jsonHttpRequest')
})
afterEach(() => {
sinon.restore()
})
it("Should update workers balance, paid ,coin:payments:all and coin.payments.workers:address", () => {
//ARRANGE
let error = null
const endpoint = '/transactions/prepare/advanced'
const endpoint2 = '/transactions/send/prepared'
stub.onCall(0)
.callsFake(function (host, port, data, callback, path) {
return callback(null, {"transactionHash": "396e2a782c9ce9993982c6f93e305b05306d0e5794f57157fbac78581443c55f","fee": 1000,"relayedToNetwork": false})
})
stub.onCall(1)
.callsFake(function (host, port, data, callback, path) {
return callback(null, null)
})
stub.onCall(2)
.callsFake(function (host, port, data, callback, path) {
return callback(null, {"transactionHash": "396e2a782c9ce9993982c6f93e305b05306d0e5794f57157fbac78581443c55f","fee": 1000,"relayedToNetwork": false})
})
stub.onCall(3)
.callsFake(function (host, port, data, callback, path) {
return callback(null, {
"transactionHash": "396e2a782c9ce9993982c6f93e305b05306d0e5794f57157fbac78581443c55f"
})
})
let callback = () => {return}
//ACT
coin.processPayments(balances, minPayoutLevel, callback)
console.log(stub.callCount)
sinon.assert.calledThrice(stub.jsonHttpRequest)
sinon.assert.calledWith(stub.firstCall, config.wallet.host, config.wallet.port, '{"destinations":[{"amount":12000,"address":"TRTLv2Fyavy8CXG8BPEbNeCHFZ1fuDCYCZ3vW5H5LXN4K2M2MHUpTENip9bbavpHvvPwb4NDkBWrNgURAd5DB38FHXWZyoBh4w"}],"feePerByte":2,"unlock_time":0,"mixin":3}', sinon.match.any, endpoint1)
sinon.assert.calledWith(stub.secondCall, config.wallet.host, config.wallet.port, '396e2a782c9ce9993982c6f93e305b05306d0e5794f57157fbac78581443c55f', sinon.match.any, endpoint1, 'DELETE')
sinon.assert.calledWith(stub.thirdCall, config.wallet.host, config.wallet.port, '{"destinations":[{"amount":11000,"address":"TRTLv2Fyavy8CXG8BPEbNeCHFZ1fuDCYCZ3vW5H5LXN4K2M2MHUpTENip9bbavpHvvPwb4NDkBWrNgURAd5DB38FHXWZyoBh4w"}],"feePerByte":2,"unlock_time":0,"mixin":3}', sinon.match.any, endpoint1)
sinon.assert.calledWith(stub.call(3), config.wallet.host, config.wallet.port, '{"transactionHash":"396e2a782c9ce9993982c6f93e305b05306d0e5794f57157fbac78581443c55f"}', sinon.match.any, endpoint2)
})
})
})
`

Related

Spying & mocking return values on ctors & methods in node module with Jest

After much trial/error, searching here on SO, & flexing my Google Fu, throwing in the towel & asking for help.
TL/DR -
Trying to correctly mock node module, change internal method return types, and spy on ctor's & method calls within the node module.
My specific scenario is to test the Microsoft Azure Storage blob SDK #azure/storage-blob, but the questions aren't specific to this package. It's just a good example as 4 LOC's capture achieve a task (upload a file to a storage container) as 2-3 of those LOC's cover 4 scenarios. Here's my code that I want to test, with comments on WHAT exactly I want to test:
export async function saveImage(account: string, container: string, imageBuffer: Buffer, imageName: string): Promise<void> {
try {
// init storage client
// (1) want to spy on args passed into ctor
const blobServiceClient: BlobServiceClient = new BlobServiceClient(`https://${account}.blob.core.windows.net`, new DefaultAzureCredential());
// init container
// (2) same as #1
const containerClient: ContainerClient = await blobServiceClient.getContainerClient(container);
// init block blob client
// (3) same as #1 & #2
const blockBlobClient: BlockBlobClient = containerClient.getBlockBlobClient(imageName);
// save file
// (4) same as #1,2, & 3
// (5) manipulate returned value
// (6) throw cause method to internally throw error
await blockBlobClient.upload(imageBuffer, imageBuffer.length, { blobHTTPHeaders: { blobContentType: 'image/png' } });
return Promise.resolve();
} catch (err: Error) {
return Promise.reject(err);
}
}
I've setup a manual mock for the module in the ./__mocks/#azure/storage-blob.ts as follows:
const MockStorageBlob = jest.createMockFromModule('#azure/storage-blob');
/**
* Utility method throw exception in the `BlockBlobClient.upload()` method.
*/
(MockStorageBlob as any).__setBlockBlobUpload_toFail = () => {
(MockStorageBlob as any).BlobServiceClient = jest.fn().mockImplementation(() => {
return {
getContainerClient: jest.fn().mockReturnValue({
getBlockBlobClient: jest.fn().mockReturnValue({
upload: jest.fn().mockImplementation(() => {
throw new Error('synthetic error');
})
})
})
}
});
}
module.exports = MockStorageBlob;
In my test, I can successfully test for #6 above like this:
import {
BlockBlobClient,
BlockBlobUploadResponse
} from '#azure/storage-blob';
import { saveImageToCDN as functionUnderTest } from './saveImageToCDN';
// mock Azure Storage blob NPM package
jest.mock('#azure/storage-blob');
describe('check expected with failure', () => {
beforeEach((done) => {
// reset number of times things have been called
jest.clearAllMocks();
done();
});
test(`it calls 'trackException()' when upload throws exception`, async (done) => {
expect.assertions(1);
// setup test
require('#azure/storage-blob').__setBlockBlobUpload_toFail();
// run SUT
const imageBuffer = Buffer.from('test string');
functionUnderTest(imageBuffer, 'imageName.png')
.then(() => {
expect(new Error('should not reach this')).toBeUndefined();
})
.catch((err: Error) => {
expect(err).toBeDefined();
})
.finally(() => {
done();
});
});
});
... but I can't figure out the correct syntax to spy on the upload() method (#4), or any of the other things I'm trying to test for (#1-5). If it matters, using Jest v26 on Node v14.
Could the __setBlockBlobUpload_toFail function return references to the mock functions ?
That would give something like this :
const MockStorageBlob = jest.createMockFromModule('#azure/storage-blob');
/**
* Utility method throw exception in the `BlockBlobClient.upload()` method.
*/
(MockStorageBlob as any).__setBlockBlobUpload_toFail = () => {
const upload = jest.fn().mockImplementation(() => {
throw new Error('synthetic error');
});
const getBlockBlobClient = jest.fn().mockReturnValue({ upload });
const getContainerClient = jest.fn().mockReturnValue({ getBlockBlobClient });
const BlobServiceClient = jest.fn().mockImplementation(() => {
return {
getContainerClient
}
});
(MockStorageBlob as any).BlobServiceClient = BlobServiceClient;
return {
upload,
getBlockBlobClient,
getContainerClient,
BlobServiceClient
};
}
module.exports = MockStorageBlob;
And in your test you would retrieve them like :
// setup test
const mockFns = require('#azure/storage-blob').__setBlockBlobUpload_toFail();
// run SUT
const imageBuffer = Buffer.from('test string');
functionUnderTest(imageBuffer, 'imageName.png')
.then(() => {
expect(new Error('should not reach this')).toBeUndefined();
})
.catch((err: Error) => {
expect(mockFns.getBlockBlobClient.mock.calls[0][0]).toBe('imageName.png')
expect(err).toBeDefined();
})
.finally(() => {
done();
});

Proper way to test type-graphql middleware with jest

Context
I am trying to write a jest test for an authentication middleware for a resolver function. I am attempting to mock an implementation so that the next function is called so that the test passes.
Error
The error I receive is "next is not a function". I can verify that the mocked function is called through expect(isAuth).toHaveBeenCalledTimes(1);, but there is clearly an issue with my mocked implementation. Any help is much appreciated.
Code
//isAuth Middleware
import { MiddlewareFn } from "type-graphql";
import { Context } from "../utils/interfaces/context";
export const isAuth: MiddlewareFn<Context> = ({ context }, next) => {
const loggedInUserId = context.req.session.id;
if (!loggedInUserId) {
throw new Error("Not authenticated!");
}
return next();
};
//transaction.test.ts
jest.mock("../middleware/isAuth", () => {
return {
isAuth: jest.fn((_, next) => next()), //also tried (next) => next() and (next)=>Promise.resolve(next())
};
});
test("should create a txn successfully", async () => {
//ARRANGE
const user = await createUser(orm);
const txn = createTxnOptions();
const txnToBeCreated = { ...txn, userId: user.id };
//ACT
const response = await testClientMutate(
TXN_QUERIES_AND_MUTATIONS.CREATE_TXN,
{
variables: txnToBeCreated,
}
);
//expect(isAuth).toHaveBeenCalledTimes(1); passes so it's getting called
console.log(response);
const newlyCreatedTxn: Transaction = (response.data as any)
?.createTransaction;
//ASSERT
const dbTxn = await em.findOne(Transaction, {
id: newlyCreatedTxn.id,
});
expect(newlyCreatedTxn.id).toBe(dbTxn?.id);
});
//transaction.resolver.ts
import { Transaction } from "../entities/Transaction";
import {
Arg,
Ctx,
Mutation,
Query,
Resolver,
UseMiddleware,
} from "type-graphql";
import { Context } from "../utils/interfaces/context";
import { isAuth } from "../middleware/isAuth";
#Mutation(() => Transaction)
#UseMiddleware(isAuth)
async createTransaction(
#Arg("title") title: string,
#Arg("userId") userId: string,
#Ctx() { em }: Context
): Promise<Transaction> {
const transaction = em.create(Transaction, {
title,
user: userId,
});
await em.persistAndFlush(transaction);
return transaction;
}
Replace
jest.mock("../middleware/isAuth", () => {
return {
isAuth: jest.fn((_, next) => next()), //also tried (next) => next() and (next)=>Promise.resolve(next())
};
});
With
jest.mock("../middleware/isAuth", () => {
return {
isAuth: (_, next) => next()
};
});

Skip implementation in Jest

Currently I have the following piece of code:
function handleConnection(socket: Socket): void {
info(`Connection started with socketId: ${socket.id}`)
socket.on("joinRoom", (request: string) => handleJoinRoom(socket, request));
socket.on("shareData", (request: IShareDataRequest) => handleShareData(socket, request));
socket.on("disconnect", () => handleDisconnect(socket));
}
I want to write a test for each and every events like joinRoom, shareData and disconnect. To isolae the tests I want to only test the second socket.on("shareData", () => ...) call and skip the first socket.on("joinRoom", () => ...) call. I've tried with multiple mockImplementationOnce methods with no success.
The test I wrote:
it('should emit data to room', () => {
const listenMock = listen();
const socketMock = {
on: jest.fn()
.mockImplementationOnce(() => null)
.mockImplementationOnce((event: string, callback: Function) => callback({ roomId: "abcdefg" })),
to: jest.fn().mockReturnValue({
emit: jest.fn()
})
}
jest.spyOn(listenMock, "on").mockImplementationOnce((event: string, callback: Function) => callback(socketMock))
startSocket();
expect(socketMock.to).toHaveBeenCalledWith("abcdefg");
expect(socketMock.to().emit).toHaveBeenCalledWith("receiveData", expect.any(Object));
})
ShareData function:
function handleShareData(socket: Socket, request: IShareDataRequest): void {
socket.to(request.roomId).emit("receiveData", request);
}
I would really appreciate if anyone could help me out with this.
You can try the following approach:
// define the mockSocket
const mockSocket = {
// without any implementation
on: jest.fn()
};
describe("connection handler", () => {
// I personally like separating the test setup
// in beforeAll blocks
beforeAll(() => {
handleConnection(mockSocket);
});
// you can write assertions that .on
// should have been called for each event
// with a callback
describe.each(["joinRoom", "shareData", "disconnect"])(
"for event %s",
event => {
it("should attach joinRoom handlers", () => {
expect(mockSocket.on.mock.calls).toEqual(
expect.arrayContaining([[event, expect.any(Function)]])
);
});
}
);
describe("joinRoom handler", () => {
beforeAll(() => {
// jest mock functions keep the calls internally
// and you can use them to find the call with
// the event that you need and retrieve the callback
const [_, joinRoomHandler] = mockSocket.on.mock.calls.find(
([eventName]) => eventName === "joinRoom"
);
// and then call it
joinRoomHandler("mockRequestString");
});
it("should handle the event properly", () => {
// your joinRoom handler assertions would go here
});
});
});

Express + Mocha, testing async middleware

I have auth.handler middleware that call some async function that I having problem to test using mocha & chai, using done() not helping either here, example code:
middleware
async function authHandler(req, res, next) {
const token = req.headers.authorization.split(' ')[1]
const validCredential = await AuthService.verifyAccess(token)
if (!validCredential) {
next(new Error('Invalid credential')
}
next()
}
test
it('should return error', async () => {
const token = 'invalid-token'
const { req, res} = mockSetup(token)
await authHandler(req, res, function next(error) => {
expect(res.status).to.be.equal(500)
expect(error.message).to.be.equal('invalid credential')
})
})

Node.js Express.js: mock request object for unit-test purpose

I have a method that check if request query string has some params:
checkMandatoryQueryParams (mandatoryQueryParams: String[], req: Request): void {
let result = true;
mandatoryQueryParams.forEach((element, idx) => {
if (!req.query.hasOwnProperty(element)) {
result = false;
}
});
return result;
}
in unit-testing i need to mock the request for test it, eg:
describe('Utils', () => {
it('checkMandatoryQueryParams', () => {
const req: Request = new Request(); // pseudo-code
req.query = "?foo=test&bar=test"; // pseudo-code
expect( checkMandatoryQueryParams(['foo', 'bar'], req) ).toEqual(true);
});
});
how can i mock the express request?
Two options:
Create your own mock object:
describe('Utils', () => {
/**
* Mocked Express Request object
*/
let mockedReq;
beforeEach(() => {
mockedReq = {
query: {}
};
})
it('checkMandatoryQueryParams', () => {
const req = mockedReq;
req.query = "?foo=test&bar=test"; // pseudo-code
expect(checkMandatoryQueryParams(['foo', 'bar'], req) ).toEqual(true);
});
});
Use manual mocks, similar to above:
describe('Utils', () => {
/**
* Mocked Express Request object
*/
let mockedReq;
beforeAll(() => {
mockedReq = jest.fn().mockImplementation(() => {
const req = {
// Implementation
};
return req;
});
});
beforeEach(() => mockedReq.mockReset())
it('checkMandatoryQueryParams', () => {
const req = mockedReq;
req.query = "?foo=test&bar=test"; // pseudo-code
expect(checkMandatoryQueryParams(['foo', 'bar'], req) ).toEqual(true);
});
});