I am having a hard time understanding what I am doing wrong.
I have a JS class as such:
export default class A {
constructor(repository) {
this._repository = repository;
}
async process(date) {
// ...
this._repository.writeToTable(entry);
}
}
and I am attempting to write a test that mocks the repository using sinon.mock
This is what I have so far:
describe('A', () => {
describe('#process(date)', () => {
it('should work', async () => {
const repository = { writeToTable: () => {} };
const mock = sinon.mock(repository);
const a = new A(repository);
await a.process('2017-06-16');
mock.expects('writeToTable').once();
mock.verify();
});
});
});
but it always fails saying that
ExpectationError: Expected writeToTable([...]) once (never called)
I've checked (added a console.log) and it is calling the object I defined on the test.
I ran this locally and read the documentation on sinonjs.org and you seem to be doing everything right.
I tried re-writing your example using a spy and ended up with something like this to get a passing test:
import sinon from "sinon";
import { expect } from "chai";
import A from "./index.js";
describe("A", () => {
describe("#process(date)", () => {
it("should work", async () => {
const repository = { writeToTable: sinon.spy() };
const a = new A(repository);
await a.process("2017-06-16");
expect(repository.writeToTable.calledOnce).to.be.true;
});
});
});
Related
I have some tests in one file,
I check my reducer with some case
My code looks like this
my code
import axiosInstance from '~/utils/network';
const fetcher = axiosInstance();
const fetchMiddleware = () => {
switch (type) {
case 'LOGOUT':{
try {
await fetcher.get(API.GET.LOGOUT_OPERATION);
dispatch({ type: 'LOGOUT_SUCCESS' });
} catch (err) {
dispatch({ type: 'LOGOUT_FAIL' });
}
});
}
}
}
my test
import axiosInstance from '../../src/utils/network';
import configureStore from 'redux-mock-store';
const middlewares = [fetchMiddleware, thunk];
const mockStore = configureStore(middlewares);
const store = mockStore(getInitialReducerState());
jest.mock('../../src/utils/network', () => {
const axiosInstance = jest.fn().mockImplementation(() => {
return {
get: jest.fn().mockImplementation(() => {
return {
headers: {},
};
}),
};
}) as any;
axiosInstance.configure = jest.fn();
return axiosInstance;
});
describe('test LOGOUT', () => {
beforeEach(() => {
store.clearActions();
});
it('should test be success', async () => {
await store.dispatch({
type: 'LOGOUT',
payload: { userName: 'testUserName' },
});
expect(store.getActions()).toContainEqual({
type: 'LOGOUT_SUCCESS',
});
});
it('should test be fail', async () => {
(axiosInstance as jest.Mock).mockImplementation(() => {
return {
get: jest.fn().mockImplementation(() => {
throw new Error(' ');
}),
};
});
await store.dispatch({
type: 'LOGOUT',
payload: { userName: 'testUserName' },
});
expect(store.getActions()).toContainEqual({
type: 'LOGOUT_FAIL',
});
});
});
I want to test two scenarios: success & fail,
I mock the axiosInstance function.
But even I override the mock in the second test I get the first mock because my code loads axiosInstance only once.
what can I do?
You need to use jest.isolateModules
Let's say we have 2 files:
./lib.js - this is your ~/utils/network
./repro.js - this is your file with the code under test
./lib.js:
export default function lib() {
return () => 10;
}
./repro.js:
import lib from './lib';
const fnInstance = lib();
export const fn = () => {
return fnInstance();
};
And the ./repro.test.js:
function getRepro(libMock) {
let repro;
// Must use isolateModules because we need to require a new module everytime
jest.isolateModules(() => {
jest.mock('./lib', () => {
return {
default: libMock,
};
});
repro = require('./repro');
});
// If for some reason in the future the behavior will change and this assertion will fail
// We can do a workaround by returning a Promise and the `resolve` callback will be called with the Component in the `isolateModules` function
// Or we can also put the whole test function inside the `isolateModules` (less preferred)
expect(repro).toBeDefined();
return repro;
}
describe('', () => {
it('should return 1', () => {
const { fn } = getRepro(function lib() {
return () => 1
});
expect(fn()).toEqual(1);
});
it('should return 2', () => {
const { fn } = getRepro(function lib() {
return () => 2
});
expect(fn()).toEqual(2);
});
});
It's preferable to use existing library to mock Axios, it saves from boilerplate code and potential mistakes in mock implementation; moxios has been already suggested.
It's inconvenient to mock axiosInstance per test because it has been already called on the import of tested module, so this requires it to be re-imported per test; another answer explains how it's done with jest.isolateModules.
Since axiosInstance is evaluated only once and is supposed to return mocked object, it's convenient to mock it once per test and then change implementations:
jest.mock('~/utils/network', () => {
const axiosMock = { get: jest.fn(), ... };
return {
axiosInstance: () => axiosMock;
};
});
const axiosMock = axiosInstance();
...
(axiosMock.get axiosInstance as jest.Mock).mockImplementation(() => {
throw new Error(' ');
});
await store.dispatch(...);
This requires to use jest.restoreAllMocks in beforeEach or similar Jest configuration option to avoid test cross-contamination.
Notice that Axios doesn't throw errors but rather return rejected promises, this may affect test results, see the note regarding the benefits of libraries.
Is it possible to jest.spyOn a default export so that I can call the mockImplementation method to change what the function does before each test?
// code.js
module.exports = () => {
// some irrelevant code I want to rewrite with a mock
};
// test
const code = require('./code.js');
const mockCode = jest.spyOn(code, 'default'); // this line doesn't work with the error: "Cannot spy the default property because it is not a function; undefined given instead"
it('some test', async () => {
mockCode.mockImplementationOnce(() => { console.log('test') });
});
I've also tried to use jest.mock() unsuccessfully:
const code = require('./code');
jest.mock('./code',
() => () => {
console.log('test');
}
);
it('some test', async () => {
code.mockImplementationOnce(() => { console.log('test2') }); // error
});
I'm trying to mock a module import using Jest and I'm struggling for some reason. I've got the following code:
src/elastic.js
const getRolesFunc = elasticClient => async username => {
// Do some stuff
}
module.exports = { getRolesFunc };
src/handlerFactory.js
const { getRolesFunc } = require("../src/elastic");
const handlerFactory = elasticClient =>
async (event) => {
const getRolesAsync = getRolesFunc(elasticClient);
const roles = await getRolesAsync();
}
}
My test file currently looks like:
tests/handlerFactory.unit.test.js
const { handlerFactory } = require("../src/handlerFactory");
const { getRolesFunc } = require("../src/elastic");
jest.mock("../src/elastic", () => ({
getRolesFunc: jest.fn(),
}));
describe("handlerFactory", () => {
it("handler returns correct response", async () => {
getRolesFunc.mockImplementation(() => "foo");
// Call the handler to get our actual result
const handlerAsync = handlerFactory({});
const result = await handlerAsync(event);
});
});
At the moment however I'm getting an error in my test:
TypeError: getRolesFunc.mockImplementation is not a function
I've tried a few things none of which worked, this feels like the closest but I can't work out why the jest.mock isn't working correctly. I've looked at a few examples and still can't work out why this I can't get mocking working. Can anyone help point out what I've done wrong?
As you have module.exports = { getRolesFunc }; you need to below change in your code:
const { handlerFactory } = require("../src/handlerFactory");
const elasticObj = require("../src/elastic");
jest.mock("..src/elastic");
// in your example, now put below code:
elasticObj.getRolesFunc.mockImplementation(() => "foo");
I'm testing my Activity.vue component on Vue.js with Jest.
My Activity.vue component calls the initializeTable() method inside created() in order to initialize my tableRows property.
The problem is, when I run my tests, Jest does not call 'created()' method and, for that reason, it does not initialize my tableRows property, then the test says that my tableRows property is equal to [] (original state).
How could I call created() method when I'm testing with Jest?
I've already tried to call it explicitly inside the test (i.e. wrapper.vm.$created()), but to no avail.
Would anyone know how to solve it?
I give my code below.
Thank you in advance.
Activity.spec.js
import { shallowMount } from '#vue/test-utils'
import Activity from '#/views/Activity.vue'
import api from '#/assets/js/api'
const expectedTableRows = [...]
const $t = () => {}
api.getActivity = jest.fn(
() => Promise.resolve({
data: [...]
})
)
describe('Activity.vue', () => {
it('renders vm.tableRows in created()', () => {
const wrapper = shallowMount(Activity, {
mocks: { $t }
})
// wrapper.vm.$created()
expect(wrapper.vm.tableRows).toEqual(expectedTableRows)
})
})
Activity.vue
import api from '#/assets/js/api'
export default {
data () {
return {
tableRows: []
}
},
created () {
this.initializeTable()
},
methods: {
initializeTable () {
this.loading = true
api.getActivity().then(response => {
this.tableRows = response.data
}).catch(error => {
this.errorBackend = error
}).finally(() => {
this.loading = false
})
}
}
}
EDIT 1: Solution
As Husam Ibrahim described, the function is asynchronous, because of that, I needed to refactor the test using the following tips and now I was able to fix it.
I give the correct code below:
import flushPromises from 'flush-promises' // <--
...
describe('Activity.vue', () => {
it('renders wm.tableRows after created()', async () => { // <--
const wrapper = shallowMount(Activity, {
mocks: { $t }
})
await flushPromises() // <--
expect(wrapper.vm.tableRows).toEqual(expectedTableRows)
})
})
I am mocking a library by doing this:
let helperFn;
let mock;
beforeEach(() => {
mock = jest.fn();
require('./helperFn').default = mock;
})
If I do this in a test, does it mean that from now on within the whole test suite that default function of helperFn will be associated with that mock?
In the Jest documentations I see how to reset the mock, but I don't see how to remove the mock from a required function. I am concerned that from that test on, all the calls into helperFn.default will see that mock.
ES6 modules
Here is an ES6 example:
helperFn.js
export default () => 'original';
code.js
import helperFn from './helperFn';
export const func = () => helperFn();
code.test.js
import * as helperFnModule from './helperFn';
import { func } from './code';
describe('helperFn mocked', () => {
let mock;
beforeEach(() => {
mock = jest.spyOn(helperFnModule, 'default');
mock.mockReturnValue('mocked');
});
afterEach(() => {
mock.mockRestore();
});
test('func', () => {
expect(func()).toBe('mocked'); // Success!
});
});
describe('helperFn not mocked', () => {
test('func', () => {
expect(func()).toBe('original'); // Success!
});
});
Details
Since ES6 imports are live views of the module exports, it is easy to mock an export and then restore it afterwards.
Node.js modules
Here is a Node.js example:
helperFn.js
exports.default = () => 'original';
code.js
const helperFn = require('./helperFn').default;
exports.func = () => helperFn();
code.test.js
describe('helperFn mocked', () => {
beforeEach(() => {
const helperFnModule = require('./helperFn');
helperFnModule.default = jest.fn(() => 'mocked');
});
afterEach(() => {
jest.resetModules();
});
test('func', () => {
const { func } = require('./code');
expect(func()).toBe('mocked'); // Success!
});
});
describe('helperFn not mocked', () => {
test('func', () => {
const { func } = require('./code');
expect(func()).toBe('original'); // Success!
});
});
Details
The default export gets remembered by code.js when it runs, so changing the default export of helperFn.js doesn't affect func once code.js is required. Jest also caches modules and returns the same module for multiple require calls unless jest.resetModules is called.
So for Node.js modules it is often easiest to require code within the test itself and use jest.resetModules to reset any mocking.