Mocking node_modules which return a function with Jest? - javascript

I am writing a typeScript program which hits an external API. In the process of writing tests for this program, I have been unable to correctly mock-out the dependency on the external API in a way that allows me to inspect the values passed to the API itself.
A simplified version of my code that hits the API is as follows:
const api = require("api-name")();
export class DataManager {
setup_api = async () => {
const email = "email#website.ext";
const password = "password";
try {
return api.login(email, password);
} catch (err) {
throw new Error("Failure to log in: " + err);
}
};
My test logic is as follows:
jest.mock("api-name", () => () => {
return {
login: jest.fn().mockImplementation(() => {
return "200 - OK. Log in successful.";
}),
};
});
import { DataManager } from "../../core/dataManager";
const api = require("api-name")();
describe("DataManager.setup_api", () => {
it("should login to API with correct parameters", async () => {
//Arrange
let manager: DataManager = new DataManager();
//Act
const result = await manager.setup_api();
//Assert
expect(result).toEqual("200 - OK. Log in successful.");
expect(api.login).toHaveBeenCalledTimes(1);
});
});
What I find perplexing is that the test assertion which fails is only expect(api.login).toHaveBeenCalledTimes(1). Which means the API is being mocked, but I don't have access to the original mock. I think this is because the opening line of my test logic is replacing login with a NEW jest.fn() when called. Whether or not that's true, I don't know how to prevent it or to get access to the mock function-which I want to do because I am more concerned with the function being called with the correct values than it returning something specific.
I think my difficulty in mocking this library has to do with the way it's imported: const api = require("api-name")(); where I have to include an opening and closing parenthesis after the require statement. But I don't entirely know what that means, or what the implications of it are re:testing.

I came across an answer in this issue thread for ts-jest. Apparently, ts-jest does NOT "hoist" variables which follow the naming pattern mock*, as regular jest does. As a result, when you try to instantiate a named mock variable before using the factory parameter for jest.mock(), you get an error that you cannot access the mock variable before initialization.
Per the previously mentioned thread, the jest.doMock() method works in the same way as jest.mock(), save for the fact that it is not "hoisted" to the top of the file. Thus, you can create variables prior to mocking out the library.
Thus, a working solution is as follows:
const mockLogin = jest.fn().mockImplementation(() => {
return "Mock Login Method Called";
});
jest.doMock("api-name", () => () => {
return {
login: mockLogin,
};
});
import { DataManager } from "../../core/dataManager";
describe("DataManager.setup_api", () => {
it("should login to API with correct parameters", async () => {
//Arrange
let manager: DataManager = new DataManager();
//Act
const result = await manager.setup_api();
//Assert
expect(result).toEqual("Mock Login Method Called");
expect(mockLogin).toHaveBeenCalledWith("email#website.ext", "password");
});
});
Again, this is really only relevant when using ts-jest, as using babel to transform your jest typescript tests WILL support the correct hoisting behavior. This is subject to change in the future, with updates to ts-jest, but the jest.doMock() workaround seems good enough for the time being.

Related

how to create instance of class with jest

I am a beginner in writing tests and in jest also. I want to test that this function to be called and return the success promise. I write a unit test for function using jest. The function gets like parameter instance of class that I can't create because its constructor requires parameters that I haven't access. I have a lot of functions that have Session like parametrs. How can test function when you cant provide parametrs for it? Can I mock instance of class or function and handle it without parameter?
async initFlow(session: Session) {
const nextAtomId = session.userInput.getParam('NEXT_ATOM');
if (nextAtomId) {
const nextAtom = await AtomManager.findActiveAtom(nextAtomId);
if (!session.features.useTerms || ['beforeTerms', 'TermsAndConditions'].includes(nextAtom.type)) {
return AtomProcessor.processAtom(session, nextAtom);
}
}
const start = await AtomManager.getStartAtom(`${session.botId}`);
if (!start) {
throw new Error('Could not find start atom');
}
session.user = await UserManager.getGlobalUser(session); // getGlobalUser makes initUser under the hood.
return AtomProcessor.processAtom(session, start);
}
You can mock both AtomManager & UserManager and provide a mock session object when calling initFlow.
jest.mock("./path/to/AtomManager");
jest.mock("./path/to/UserManager");
it("works", async () => {
const mockSession = {
userInput: {
getParam: jest.fn(),
},
botId: "123",
};
const mockUser = "user123";
const mockStartAtom = "atom123";
AtomManager.getStartAtom.mockResolveValue(mockStartAtom);
UserManager.getGlobalUser.mockResolveValue(mockUser);
await initFlow(mockSession);
expect(mockSession.user).toBe(mockUser);
expect(AtomManager.getStartAtom).toHaveBeenCalledTimes(1);
expect(AtomManager.getStartAtom).toHaveBeenCalledWith(mockSession.botId);
expect(UserManager.getGlobalUser).toHaveBeenCalledTimes(1);
expect(UserManager.getGlobalUser).toHaveBeenCalledWith(mockSession);
expect(AtomProcessor.processAtom).toHaveBeenCalledTimes(1);
expect(AtomProcessor.processAtom).toHaveBeenCalledWith(mockSession, mockStartAtom);
});
The snippet above makes the following assertions:
AtomManager.getStartAtom is called once and it's called with the mock botId.
UserManager.getGlobalUser is called once and it's called with the mock session object.
UserManager.getGlobalUser has successfully added the user property on the passed session object.
AtomProcessor.processAtom is called once and it's called with the mock session and the mock start atom.
You can similarly the test other branches of code.

Mocked module function with Jest is never called

I have a really weird situation where my Jest tests are passing in my Windows 10 desktop and Macbook Pro, but they are not passing in 2 of my other friends' Windows 10 desktops.
Code that is being tested
import { addTerminalItem } from '../../store'
...
class LoginUser extends EventHandler {
...
async handle () {
if (this.isFromOauthRedirect) {
try {
await this._handleOauthRedirect()
} catch (e) {
addTerminalItem(new ErrorMessage(e.message))
}
return
}
if (await zaClient.isUserLoggedIn('testUserId')) {
// TODO: user is already logged in, do something
} else {
const loginStartSecret = uuidv4()
localStorage.setItem(LOGIN_START_SECRET, loginStartSecret)
addTerminalItem(new LoginMessage(loginStartSecret))
}
}
...
}
export const loginUser = new LoginUser()
The testing code does the following:
Adds invalid LOGIN_START_SECRET so that actual code throws exception entering the first catch.
Subscribes the event handler to the event WELCOME_MESSAGE_RENDERED.
Mocks the store.addTerminalItem module function.
Publishes the event so the above async handle() function is triggered.
Checks that the mocked function is called.
import * as store from '../../../store'
...
test('different login start secret in localstorage', async () => {
localStorage.setItem(LOGIN_START_SECRET, 'different-secret')
zaClient.login = jest.fn(() => true)
store.addTerminalItem = jest.fn()
await pubsub.publish(WELCOME_MESSAGE_RENDERED)
expect(store.addTerminalItem).toHaveBeenCalledWith(expect.any(ErrorMessage))
const errorMessage = store.addTerminalItem.mock.calls[0][0]
expect(errorMessage.message).toBe(loginSecurityErrorMsg)
})
As I said on my computer it shows correctly that addTerminalItem function is called once with the correct argument on both machines I have at home. However this mocked function is never called and fails on 2 of my friends' machines. The actual error message they get is below:
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: Any<ErrorMessage>
Number of calls: 0
Here are the following things we tried so far:
Fresh git clone, yarn install, and yarn test. I pass and they don't.
With addTerminalItem mocked, we added a console.log inside addTerminalItem and it correctly doesn't log, but still 0 number of calls.
With addTerminalItem spyed, we added a console.log inside addTerminalItem and it correctly logs, but still 0 number of calls (this makes no sense to me)
We matched our yarn version.
We carefully debug stepped through the code to make sure all other things were working as expected.
If anyone could give us any pointers here it would be greatly appreciated.
Hard to be definitive without the code at hand, but try using jest.mock:
import {addTerminalItem} from "../../../store";
jest.mock('../../../store', () => ({
addTerminalItem: jest.fn()
));
//... stuff ...
test('different login start secret in localstorage', async () => {
localStorage.setItem(LOGIN_START_SECRET, 'different-secret')
zaClient.login = jest.fn(() => true)
await pubsub.publish(WELCOME_MESSAGE_RENDERED)
expect(addTerminalItem).toHaveBeenCalledWith(expect.any(ErrorMessage))
const errorMessage = addTerminalItem.mock.calls[0][0]
expect(errorMessage.message).toBe(loginSecurityErrorMsg)
})

Jest Mock a function's return value

I have a simple function like this, I would like to mock the return value of authentication.getAccessToken() with a valid accessToken, I am having hard time doing this. Tried different ways but couldn't succeed, Can someone help me on this?
import decodeJWT from "jwt-decode";
import authentication from "#kdpw/msal-b2c-react";
export const testfunction = () => {
const jwt = decodeJWT(authentication.getAccessToken());
var current_time = Date.now() / 1000;
var remaining_time = jwt.exp - current_time;
return "testing done"
}
Following is the unit test which I have been trying, As authentication.getAccessToken() doesn't get any value it is throwing InvalidToken error.
import * as commonUtil from "../commonUtil";
describe('test test function', () => {
it("Mock", () => {
//The following runs but test fails due to null return
const authentication = require("#kdpw/msal-b2c-react");
authentication.getAccessToken = jest.fn().mockReturnValue(false);
expect(commonUtil.testfunction()).toBe(false)
});
});
Error message
Comparing two different types of values. Expected boolean but received null.
You need to import authentication within your test.
See CodeSandbox example. Open with editor to check out the unit tests.
In short, you need to do something like this.
test('test test function', () => {
const resp = { data: '' };
import authentication from "#kdpw/msal-b2c-react";
authentication.getAccessToken = jest.fn().mockReturnValue("eyJ0eXAiOiJKV1QiLCJhbdjhkbE5QNC1jNTdkTzZR...");
expect(commonUtil.testfunction()).toEqual("testing done")
});
Use describe to wrap test cases that uses mocked authentication so the mocked function will only stay local in that specific describe and everything outside it will use the real authentication.getAccessToken().

How do I write test case for a function which uses uuid using jest?

I'm using jest for writing test cases. One of my function uses uuid and due to which it is not a pure function. The code is something like this:
const myFunc = () => {
const a = uuid();
return a;
}
I'm writing my test case as :
test('should return a unique id value', () => {
const a = uuid();
expect(myFunc()).toEqual(a);
});
Obviously it won't work as it will generate a unique Id every time. How can I write test case for such function.
[EDIT]
I don't want test the uuid function as it will always generate a new id and it is a library function which is already being tested. I want to test another function which uses uuid, generates new object and returns that. This function is used by some other function and so on which makes me not able to test any of that because the initial object has an id which is different that the id which I'm using while testing.
I have found this works really nice.
At the top of test file:
// your regular imports here
jest.mock('uuid', () => ({ v4: () => '00000000-0000-0000-0000-000000000000' }));
// describe('test suite here', () => {
in the actual code:
import { v4 as uuidv4 } from 'uuid';
You can use jest.mock inorder to mock the import of uuid, like that:
const uuidMock = jest.fn().mockImplementation(() => {
return 'my-none-unique-uuid';
});
jest.mock('uuid', () => {
return uuidMock;
});
The only caveat of that approach is that you need to apply the mock in the test file before you are importing your real file.
Then you will even able to assert on the mock.
For more info read jest.mock.

Stubbing and restoring auth function with Sinon still results in Mocha test using stub

We are trying to stub out an authentication middleware in an express app in some but not all of our tests, and we are having trouble making stubbing work for us.
Our mocha test looks something like this:
describe('primaryDeal routes unit test', () => {
describe('Authentication allows for data fetching', () => {
let app;
let getPrimaryDealData;
let queryParams;
let isAuthenticated;
let count = 0;
beforeEach(() => {
// console.log(isAuthenticated);
if (count === 0) {
isAuthenticated = sinon.stub(authCheck, 'isAuthenticated');
isAuthenticated.callsArg(2);
}
app = require('../../lib/index.js');
});
afterEach(() => {
if (count === 0) {
isAuthenticated.restore();
}
app.close();
count++;
});
it(('should send an API request, validate input and return 200 response'), () => {
return chai.request(app)
.get('/api/contracts/')
.then((res) => {
expect(res).to.have.status(200);
});
});
it(('should respond with forbidden'), () => {
app = require('../../lib/index.js');
return chai.request(app)
.get('/api/contracts/')
.catch((res, err) => {
expect(res).to.have.status(403);
});
});
});
});
Our stub works as intended for the first it, but the stub appears to not be restored for the second it and our authentication middleware is not being run. Both tests work if the other is commented out.
We've tried separating these blocks in different files, and in different describe blocks, we've also tried switching the order of the it blocks, and we've tried giving both chai.request(app) separate servers but we are at a loss.
Why could it be that our second it statement isn't calling our Auth middleware?
I recommend you to use sandbox. It's more elegant to use. No need to restore stubs individually. Here is an sample:
let sandbox;
beforeEach(() => {
sandbox = sinon.sandbox.create();
isAuthenticated = sandbox.stub(authCheck, 'isAuthenticated');
});
afterEach(() => {
sandbox.restore();
});
Also, could you try to add replace
app = require('../../lib/index.js');
to
delete require.cache[require.resolve('../../lib/index.js')]
app = require('../../lib/index.js');
Another thought, maybe you need to use reset not restore in this particular case?
P.S.
It's also good to see index.js source as well
I had the same issue and tried this solution without success. However the delete require.cache[require.resolve('../../lib/index.js')] gave me an idea. I was able to use decache instead of delete require. This resolved the problem for me.
const decache = require('decache');
decache('../../lib/index.js');

Categories

Resources