how to create instance of class with jest - javascript

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.

Related

How to stub a module with sinon?

I need to stub hubspot module in order to test my apis.
I need to test this code:
const createCompany = async company => {
const hubspotClient = new hubspot.Client({
apiKey: HUBSPOT_KEY
});
//stuff
const companyObj = {
properties: {
//my properties
}
};
return await hubspotClient.crm.companies.basicApi.create(companyObj);
};
Here They show how to stub a class and access a method through its instance, but in my case I have multiple properties like .crm.companies.basicApi.create().
I tried doing:
getDataStub = sinon
.stub(hubspot.Client.prototype, 'crm.companies.BasicApi.create')
.resolves(fakeResponse);
But it doesn't work and it says TypeError: Cannot stub non-existent own property crm.companies.basicApi.create.
Do you have any hint on how to fix that?
You can create a HubspotClient instance outside of createCompany. Then export that instance so in the test file you could stub methods of it.
const hubspotClient = new hubspot.Client({
apiKey: HUBSPOT_KEY
});
// ...
const createCompany = async company => {
// ...
return await hubspotClient.crm.companies.basicApi.create(companyObj);
}
// ...
stub(hubspotClient.crm.companies.basicApi, 'create');

Mocking node_modules which return a function with Jest?

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.

Sinon Spy never called but it should be

Problem
I'm testing a custom redux middleware using Jest and SinonJS and more precisely I want to test if some functions are called on special conditions inside the middleware.
I use SinonJS for creating the spies and I run my tests with Jest. I initialised the spies for the specific functions I want to track and when I check if the spies has been called, the spies has not been even if it should be (manually tested).
Code
Here is the middleware I want to test :
import { Cookies } from 'react-cookie';
import setAuthorizationToken from './setAuthorizationToken';
let cookies = new Cookies();
export const bindTokenWithApp = (store) => (next) => (action) => {
// Select the token before action
const previousToken = getToken(store.getState());
// Dispatch action
const result = next(action);
// Select the token after dispatched action
const nextToken = getToken(store.getState());
if (previousToken !== nextToken) {
if (nextToken === '') {
setAuthorizationToken(false);
cookies.remove(SESSION_COOKIE_NAME, COOKIE_OPTIONS);
} else {
cookies.set(SESSION_COOKIE_NAME, nextToken, COOKIE_OPTIONS);
setAuthorizationToken(nextToken);
}
}
return result;
};
Here is my actual test
import { bindTokenWithApp } from './middleware';
import { Cookies } from 'react-cookie';
import sinon, { assert } from 'sinon';
import setAuthorizationToken from './setAuthorizationToken';
describe('bindTokenWithApp', () => {
const next = jest.fn();
const action = jest.fn();
let cookies = new Cookies();
it('removes cookies when there is no token', () => {
// My actual not working spies
const cookieSpy = sinon.spy(cookies.remove);
const authSpy = sinon.spy(setAuthorizationToken);
// Stub for the specific case. This code works,
// I console.logged in the middleware and I'm getting the below values
const getState = sinon.stub();
getState.onFirstCall().returns({ auth: { token: 'a token' } });
getState.onSecondCall().returns({ auth: { token: '' } });
const store = { getState: getState };
bindTokenWithApp(store)(next)(action);
assert.calledOnce(cookieSpy);
assert.calledOnce(authSpy);
// Output : AssertError: expected remove to be called once but was called 0 times
// AssertError: expected setAuthorizationToken to be called once but was called 0 times
cookieSpy.restore(); // <= This one works
authSpy.restore(); // TypeError: authSpy.restore is not a function
});
});
I've read SinonJS doc and a few StackOverFlow posts but without solutions. I also can't call authSpy.restore();. I think I do not initialise spies the right way and I'm misunderstanding a concept in SinonJS but I can't find which one !
The setAuthorizationToken signature is
(alias) const setAuthorizationToken: (token: any) => void
import setAuthorizationToken
I think it's a classical module so I can't figure out why I struggle with authSpy.restore();
The two spies you have actually have two different fixes, both with the same underlying problem. sinon.spy(someFunction) doesn't actually wrap someFunction itself, it returns a spy for it but doesn't perform any replacement.
For the first spy, there exists a shorthand to automatically wrap an object method: sinon.spy(cookie, 'remove') should do what you need.
For the second spy, it is more complicated as you need to wrap the spy around the default export of setAuthorizationToken. For that you will need something like proxyquire. Proxyquire is a specialized require mechanism that allows you to replace imports with your desired test methods. Here's a brief of what you'll need to do:
const authSpy = sinon.spy(setAuthorizationToken);
bindTokenWithApp = proxyquire('./middleware', { './setAuthorizationToken': authSpy});

How to connect to mongoDB and test for drop collection?

This is how I connect to a mongoDB using monk(). I'll store it in state.
Assume we want to drop some collections, we call dropDB.
db.js
var state = {
db: null
}
export function connection () {
if (state.db) return
state.db = monk('mongdb://localhost:27017/db')
return state.db
}
export async function dropDB () {
var db = state.db
if (!db) throw Error('Missing database connection')
const Users = db.get('users')
const Content = db.get('content')
await Users.remove({})
await Content.remove({})
}
I'm not quite sure if it is a good approach to use state variable. Maybe someone can comment on that or show an improvement.
Now I want to write a unit test for this function using JestJS:
db.test.js
import monk from 'monk'
import { connection, dropDB } from './db'
jest.mock('monk')
describe('dropDB()', () => {
test('should throw error if db connection is missing', async () => {
expect.assertions(1)
await expect(dropDB()).rejects.toEqual(Error('Missing database connection'))
})
})
This part is easy, but the next part gives me two problems:
How do I mock the remove() methods?
test('should call remove() methods', async () => {
connection() // should set `state.db`, but doesn't work
const remove = jest.fn(() => Promise.resolve({ n: 1, nRemoved: 1, ok: 1 }))
// How do I use this mocked remove()?
expect(remove).toHaveBeenCalledTimes(2)
})
And before that? How do I setup state.db?
Update
As explained by poke the global variable makes the problem. So I switched to a class:
db.js
export class Db {
constructor() {
this.connection = monk('mongdb://localhost:27017/db');
}
async dropDB() {
const Users = this.connection.get('users');
const Content = this.connection.get('content');
await Users.remove({});
await Content.remove({});
}
}
which results in this test file:
db.test.js
import { Db } from './db'
jest.mock('./db')
let db
let remove
describe('DB class', () => {
beforeAll(() => {
const remove = jest.fn(() => Promise.resolve({ n: 1, nRemoved: 1, ok: 1 }))
Db.mockImplementation(() => {
return { dropDB: () => {
// Define this.connection.get() and use remove as a result of it
} }
})
})
describe('dropDB()', () => {
test('should call remove method', () => {
db = new Db()
db.dropDB()
expect(remove).toHaveBeenCalledTimes(2)
})
})
})
How do I mock out any this elements? In this case I need to mock this.connection.get()
Having a global state is definitely the source of your problem here. I would suggest to look for a solution that does not involve global variables at all. As per Global Variables Are Bad, global variables cause tight coupling and make things difficult to test (as you have noticed yourself).
A better solution would be to either pass the database connection explicitly to the dropDB function, so it has the connection as an explicit dependency, or to introduce some stateful object that holds onto the connection and offers the dropDB as a method.
The first option would look like this:
export function openConnection() {
return monk('mongdb://localhost:27017/db');
}
export async function dropDB(connection) {
if (!connection) {
throw Error('Missing database connection');
}
const Users = connection.get('users');
const Content = connection.get('content');
await Users.remove({});
await Content.remove({});
}
This would also make it very easy to test dropDB as you can now just pass a mocked object for it directly.
The other option could look like this:
export class Connection() {
constructor() {
this.connection = monk('mongdb://localhost:27017/db');
}
async dropDB() {
const Users = this.connection.get('users');
const Content = this.connection.get('content');
await Users.remove({});
await Content.remove({});
}
}
A test for the first option could look like this:
test('should call remove() methods', async () => {
const usersRemove = jest.fn().mockReturnValue(Promise.resolve(null));
const contentRemove = jest.fn().mockReturnValue(Promise.resolve(null));
const dbMock = {
get(type) {
if (type === 'users') {
return { remove: usersRemove };
}
else if (type === 'content') {
return { remove: contentRemove };
}
}
};
await dropDB(dbMock);
expect(usersRemove).toHaveBeenCalledTimes(1);
expect(contentRemove).toHaveBeenCalledTimes(1);
});
Basically, the dropDB function expects an object that has a get method which when called returns an object that has a remove method. So you just need to pass something that looks like that, so the function can call those remove methods.
For the class, this is a bit more complicated since the constructor has a dependency on the monk module. One way would be to make that dependency explicit again (just like in the first solution), and pass monk or some other factory there. But we can also use Jest’s manual mocks to simply mock the whole monk module.
Note that we do not want to mock the module containing our Connection type. We want to test that, so we need it in its un-mocked state.
To mock monk, we need to create a mock module of it at __mocks__/monk.js. The manual points out that this __mocks__ folder should be adjacent to the node_modules folder.
In that file, we simply export our custom monk function. This is pretty much the same we already used in the first example, since we only care about getting those remove methods in place:
export default function mockedMonk (url) {
return {
get(type) {
if (type === 'users') {
return { remove: mockedMonk.usersRemove };
}
else if (type === 'content') {
return { remove: mockedMonk.contentRemove };
}
}
};
};
Note that this refers to the functions as mockedMonk.usersRemove and mockedMonk.contentRemove. We’ll use this in the test to configure those function explicitly during the test execution.
Now, in the test function, we need to call jest.mock('monk') to enable Jest to mock the monk module with our mocked module. Then, we can just import it too and set our functions within the test. Basically, just like above:
import { Connection } from './db';
import monk from 'monk';
// enable mock
jest.mock('./monk');
test('should call remove() methods', async () => {
monk.usersRemove = jest.fn().mockReturnValue(Promise.resolve(null));
monk.contentRemove = jest.fn().mockReturnValue(Promise.resolve(null));
const connection = new Connection();
await connection.dropDB();
expect(monk.usersRemove).toHaveBeenCalledTimes(1);
expect(monk.contentRemove).toHaveBeenCalledTimes(1);
});

Mock a dependency's constructor Jest

I'm a newbie to Jest. I've managed to mock my own stuff, but seem to be stuck mocking a module. Specifically constructors.
usage.js
const AWS = require("aws-sdk")
cw = new AWS.CloudWatch({apiVersion: "2010-08-01"})
...
function myMetrics(params) {
cw.putMetricData(params, function(err, data){})
}
I'd like to do something like this in the tests.
const AWS = jest.mock("aws-sdk")
class FakeMetrics {
constructor() {}
putMetricData(foo,callback) {
callback(null, "yay!")
}
}
AWS.CloudWatch = jest.fn( (props) => new FakeMetrics())
However when I come to use it in usage.js the cw is a mockConstructor not a FakeMetrics
I realise that my approach might be 'less than idiomatic' so I'd be greatful for any pointers.
This is a minimal example https://github.com/ollyjshaw/jest_constructor_so
npm install -g jest
jest
Above answer works. However, after some time working with jest I would just use the mockImplementation functionality which is useful for mocking constructors.
Below code could be an example:
import * as AWS from 'aws-sdk';
jest.mock('aws-sdk', ()=> {
return {
CloudWatch : jest.fn().mockImplementation(() => { return {} })
}
});
test('AWS.CloudWatch is called', () => {
new AWS.CloudWatch();
expect(AWS.CloudWatch).toHaveBeenCalledTimes(1);
});
Note that in the example the new CloudWatch() just returns an empty object.
The problem is how a module is being mocked. As the reference states,
Mocks a module with an auto-mocked version when it is being required.
<...>
Returns the jest object for chaining.
AWS is not module object but jest object, and assigning AWS.CloudFormation will affect nothing.
Also, it's CloudWatch in one place and CloudFormation in another.
Testing framework doesn't require to reinvent mock functions, they are already there. It should be something like:
const AWS = require("aws-sdk");
const fakePutMetricData = jest.fn()
const FakeCloudWatch = jest.fn(() => ({
putMetricData: fakePutMetricData
}));
AWS.CloudWatch = FakeCloudWatch;
And asserted like:
expect(fakePutMetricData).toHaveBeenCalledTimes(1);
According to the documentation mockImplementation can also be used to mock class constructors:
// SomeClass.js
module.exports = class SomeClass {
method(a, b) {}
};
// OtherModule.test.js
jest.mock('./SomeClass'); // this happens automatically with automocking
const SomeClass = require('./SomeClass');
const mockMethod= jest.fn();
SomeClass.mockImplementation(() => {
return {
method: mockMethod,
};
});
const some = new SomeClass();
some.method('a', 'b');
console.log('Calls to method: ', mockMethod.mock.calls);
If your class constructor has parameters, you could pass jest.fn() as an argument (eg. const some = new SomeClass(jest.fn(), jest.fn());

Categories

Resources