Sinon Spy never called but it should be - javascript

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});

Related

How to call useRouter inside useFetch's onFetchError by VueUse?

I need to create a custom fetch composable from VueUse using createFetch() and I want to check if a request returns 401 status, I'd like the route to be redirected to the login route.
export const useApiFetch = createFetch({
baseUrl: import.meta.env.VITE_API_BASE_URL,
options: {
beforeFetch({ options }) {
const { user } = useSessionStore()
if (user)
options.headers.Authorization = `Basic ${user.user_id}:${user.password}`
return { options }
},
onFetchError(response) {
const route = useRoute()
const router = useRouter()
if (route.name !== 'login' && response.status === 401)
return router.push('/login')
}
}
})
But everytime it hits the error, useRoute and useRouter are undefined, and yes.. I have checked that it runs in setup
<script setup>
const submit = async () => {
const { error, data } = await useApiFetch('/login').post(form)
}
</script>
Did I miss something or is there a better way to do this? thanks
Vue composables are primarily expected to be called in setup block. Other usages depend on their implementation and needs to be confirmed. The main restriction is that a composable is linked to specific component instance, in this case useRouter uses provide/inject to get router instance through component hierarchy; this often can be be deduced without checking the actual implementation.
It's possible to directly import router instance instead of using useRouter but this may result in module circular dependencies and this may not be work for other composables.
createFetch wasn't designed for this usage and needs to be wrapped with custom composable that guarantees that other composables will be called in time:
let useApiFetchFn;
const useApiFetch = (...args) => {
if (!useApiFetchFn) {
const sessionStore = useSessionStore()
const route = useRoute()
const router = useRouter()
useApiFetchFn = createFetch(...)
}
return useApiFetchFn(...args);
}
Whether it's correct to cache the result to useApiFetchFn depends on the implementation, in this case it's acceptable. At this point it may be more straightforward to use useFetch directly and compose the options similarly to how createFetch does that, most of its code is dedicated to TS support and variadic arguments that may not be needed in this case.

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.

Test set cookies function with Jest

Does someone knows how can I test this function in Jest? I don't have any ideas at this moment, maybe I need to mock Cookies ?
import Cookies from "js-cookie";
import { v4 as uuidv4 } from "uuid";
const setUserCookie = () => {
if (!Cookies.get("UserToken")) {
Cookies.set("UserToken", uuidv4(), { expires: 10 });
}
};
export default setUserCookie;
I tried this for now, but I don't know if this is correct, I don't think it tests the functionality of my function:
import Cookies from 'js-cookie';
import setCookie from './setCookie';
describe("setCookie", () => {
it("should set cookie", () => {
const mockSet = jest.fn();
Cookies.set = mockSet;
Cookies.set('testCookie', 'testValue');
setCookie()
expect(mockSet).toBeCalled();
});
});
Best way to test this is to utilize the actual logic, so I would change your test to the following:
it("should set cookie", () => {
// execute actual logic
setCookie();
// retrieve the result
const resultCookie = Cookies.get();
// expects here
expect(resultCookie["UserToken"]).toBeTruthy();
// expects for other values here...
});
To note, uuidv4() will generate a new value for every new test run, meaning that you cannot expect the same value for the "UserToken" property. Instead, you can use the following approach to tackle this problem:
First set up a spy for it:
import { v4 as uuidv4 } from "uuid";
jest.mock('uuid');
Then add its mock implementation with the expected result into the unit test:
const expectedUUIDV4 = 'testId';
uuidv4.mockImplementation(() => expectedUUIDV4);
// then expecting that in the result
expect(resultCookie["UserToken"]).toEqual(expectedUUIDV4);

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 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);
});

Categories

Resources