Vue, Jest, Axios.create(), and Modularized API Methods. How to Spy? - vue.js

I'm testing a Vue component, that has a method() that calls a Module with Axios, that calls another.
The method inside the component looks like:
// Component.vue
methods: {
myMethod(){
return MyAPI.orders
.exportData(requestData)
.then(() =>{
this.showExportModal();
});
}
...
The MyAPI.js looks like:
export const apiClient = axios.create({
baseURL: `${baseUrl}/api/v2/`,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
const methods = {
orders: {
exportData(payload) {
return apiClient
.post('export',payload)
While this works perfectly fine, I do not find a way to test the component properly without creating an ad hoc mock for it.
I'm trying to use Jest.createMockFromModule but no luck using it this way:
// test.spec.js
const myAPI = jest.createMockFromModule('../services/myAPI');
const wrapper = shallowMount(ExportOrdersButton, {
store,
i18n,
});
[...]
it('should trigger a network request when clicked', async () => {
const button = wrapper.find('button').element;
button.click();
await Vue.nextTick()
expect(myAPI.default.orders.exportData).toHaveBeenCalled()
});
But despite the method is called, the test does not see it:
Expected number of calls: >= 1
Received number of calls: 0

jest.createMockFromModule() doesn't automatically override imports, but jest.mock() does.
Mock the target module (i.e., MyApi.js) with a factory that returns the mock object, and then require it in the test to access the mock:
jest.mock('../services/MyApi', () => ({
orders: {
exportData: jest.fn(() => Promise.resolve())
}
}))
describe('Testing component', () => {
it('click directive', async () => {
const wrapper = shallowMount(ExportOrdersButton)
await wrapper.find('button').trigger('click')
expect(require('../services/MyApi').orders.exportData).toHaveBeenCalled()
})
})

Related

Test Express.js routes which rely on a TypeORM connection

I have a very basic Express.js app which I use Jest and Supertest to test. The routes are not set up until the database is connected:
class App {
public app: express.Application;
public mainRoutes: Util = new Util();
constructor() {
this.app = express();
AppDataSource.initialize()
.then(() => {
// add routes which rely on the database
this.mainRoutes.routes(this.app);
})
.catch((error) => console.log(error));
}
}
export default new App().app;
Here is my test:
describe("Util", function () {
test("should return pong object", async () => {
const res = await request(app).get("/ping");
expect(res.statusCode).toEqual(200);
expect(res.body).toEqual({ message: "pong" });
});
});
Since I put in the promise, this has been 404ing. I can't add async to the constructor. I tried refactoring the class to separate the connection with setting up the routes, but it didn't seem to help.
This works:
test("should return pong object", async () => {
setTimeout(async () => {
const res = await request(app).get("/ping");
expect(res.statusCode).toEqual(200);
expect(res.body).toEqual({ message: "pong" });
}, 1000);
});
But obviously I don't want to add a setTimeout. How is this usually done? I am new to testing.
Just remove the setTimeout() and await the call to the application. You should be initializing the application in the beforeAll() method, which I assume you have, to get the application up and running in the testing space. You should also mock your database connection, so you can fake the data you want back, and not have to wait for the external database to actually be available.
// Create a mock for your database, and have it return whatever you need
import <your-database-class> = require('database');
jest.mock('database', () => {
...
});
describe("Util", function () {
beforeAll(async () => {
app = await <whatever you do to launch your application>
});
test('should be defined', () => {
expect(app).toBeDefined();
});
test("should return pong object", async () => {
const res = await request(app).get("/ping");
expect(res.statusCode).toEqual(200);
expect(res.body).toEqual({ message: "pong" });
});
});

How to test complex async reducers with Jest

I have such kinds of reducers that use fetch API as its base ultimately:
export const fetchRelatedFamilies = () => {
return (dispatch, getState) => {
if (isEmpty(getState().relatedFamiliesById)) {
dispatch({ type: REQUEST_RELATED_FAMILIES_BY_ID })
new HttpRequestHelper('/api/related_families',
(responseJson) => {
dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: responseJson.relatedFamiliesById })
},
e => dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, error: e.message, updates: {} }),
).get()
}
}
}
Code for HttpRequestHelper is here: https://github.com/broadinstitute/seqr/blob/master/ui/shared/utils/httpRequestHelper.js
Here is how I am trying to test it (but its not working):
import configureStore from 'redux-mock-store'
import fetchMock from 'fetch-mock'
import thunk from 'redux-thunk'
import { cloneDeep } from 'lodash'
import { fetchRelatedFamilies, REQUEST_RELATED_FAMILIES_BY_ID, RECEIVE_RELATED_FAMILIES_BY_ID } from 'redux/rootReducer'
import { STATE1 } from '/shared/components/panel/fixtures.js'
describe('fetchRelatedFamilies', () => {
const middlewares = [thunk]
const testActionsDispatch = async (currstate, expectedActions) => {
const store = configureStore(middlewares)(currstate)
store.dispatch(fetchRelatedFamilies())
// need to mimick wait for async actions to be dispatched
//await new Promise((r) => setTimeout(r, 200));
expect(store.getActions()).toEqual(expectedActions)
}
afterEach(() => {
fetchMock.reset()
fetchMock.restore()
})
it('Dispatches correct actions when data - relatedFamiliesById - is absent in state', () => {
const relatedFamiliesById = cloneDeep(STATE1.relatedFamiliesById)
fetchMock
.getOnce('/api/related_families', { body: relatedFamiliesById, headers: { 'content-type': 'application/json' } })
STATE1.relatedFamiliesById = {}
const expectedActions = [
{ type: REQUEST_RELATED_FAMILIES_BY_ID },
{ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById }
]
testActionsDispatch(STATE1, expectedActions)
})
})
I don't see { type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById } in the resulting store actions, so I tried to use the trick: await new Promise((r) => setTimeout(r, 200)); in hope that it's the issue with async fetch but what it causes is that test will pass no matter what expected actions are as if the code that is following await is completely being ignored. I can't use store.dispatch(fetchRelatedFamilies()).then(... probably because Promise is not returned, and I am getting then access of undefined error. I tried to use waitFor from the library: https://testing-library.com/docs/guide-disappearance/ but I am having really big troubles installing the library itself due to the nature of the project itself and its version, so I need to avoid it still somehow.
So, the only question that I have is how I can make the action dispatched inside the async reducer to appear, in this case - { type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById }.
The problem with the current code is that although you are awaiting for 200ms in your testActionsDispatch helper method (so that the mocked promise is resolved), you are not awaiting in the test code for that promise of 200ms to resolve.
In order to do that you have to declare your test as async and await for the execution of the testActionsDispatch code:
const testActionsDispatch = async (currstate, expectedActions) => {
const store = configureStore(middlewares)(currstate)
store.dispatch(fetchRelatedFamilies())
// need to mimick wait for async actions to be dispatched
await new Promise((r) => setTimeout(r, 200));
expect(store.getActions()).toEqual(expectedActions)
}
// Note that the test is declared as async
it('Dispatches correct actions when data - relatedFamiliesById - is absent in state', async () => {
const relatedFamiliesById = cloneDeep(STATE1.relatedFamiliesById)
fetchMock
.getOnce('/api/related_families', { body: relatedFamiliesById, headers: { 'content-type': 'application/json' } })
STATE1.relatedFamiliesById = {}
const expectedActions = [
{ type: REQUEST_RELATED_FAMILIES_BY_ID },
{ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById }
]
// Await the execution of the helper code
await testActionsDispatch(STATE1, expectedActions)
})
Now that should work, but we are adding a delay of 200ms in every test that uses this testActionsDispatch helper. That can end up adding a lot of time when you launch your test and ultimately at a logical level is not really ensuring that the promise resolves.
A better approach is to return the promise in your reducer so we can wait for it to resolve directly in the test (I'm assuming the get method from HttpRequestHelper returns the promise created by fetch and returning it):
export const fetchRelatedFamilies = () => {
return (dispatch, getState) => {
if (isEmpty(getState().relatedFamiliesById)) {
dispatch({ type: REQUEST_RELATED_FAMILIES_BY_ID })
return new HttpRequestHelper('/api/related_families',
(responseJson) => {
dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: responseJson.relatedFamiliesById })
},
e => dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, error: e.message, updates: {} }),
).get()
}
}
}
Then, in your helper you can simply await for this returned promise to resolve:
const testActionsDispatch = async (currstate, expectedActions) => {
const store = configureStore(middlewares)(currstate)
// Await for the promise instead of awaiting a random amount of time.
await store.dispatch(fetchRelatedFamilies())
expect(store.getActions()).toEqual(expectedActions)
}

TypeError: default is not a function when using vitest

I'm using vitest to do some unit tests in my vue application.
I've written some tests but they fail with the error message: 'TypeError: default is not a function'.
But I do not use a function called default() in my code.
import getInfo from './info';
vi.mock('axios', () => {
return {
default: {
get: vi.fn()
}
}
});
test('fn getInfo() should request api with axios.get url', async () => {
const spyAxios = vi.spyOn(axios, 'get');
await getInfo('1234');
expect(spyAxios).toHaveBeenCalledWith(`${process.env.VUE_APP_API_BASE_URL}`);
});
If I then execute npm run test the result is the following:
FAIL src/api/info/info.test.js > fn getInfo() should request api with axios.get url
TypeError: default is not a function
❯ src/api/info/info.test.js:61:22
59| test('fn getInfo() should request api with axios.get url', async () => {
60| const spyAxios = vi.spyOn(axios, 'get');
61| await getInfo('1234');
| ^
62| expect(spyAxios).toHaveBeenCalledWith(`${process.env.VUE_APP_API_BASE_URL}`);
63| });
The info.ts file looks like the following:
import { useLoginStore } from "../../store/LoginStore";
import axios from "axios";
// eslint-disable-next-line
export async function getInfo(param: string) : Promise<any> {
const loginStore = useLoginStore();
axios.defaults.headers.common = {'Authorization': `Bearer ${loginStore.accessToken}`};
const request = await axios.get(
process.env.VUE_APP_API_BASE_URL
);
if (request?.status == 200) {
return request.data;
}
else {
return null;
}
}
Does anyone know what's going on here?
The default attribute in your return object is not a function (default: {...}). Instead, you would return something like this:
vi.mock('axios', () => ({
default: () => ({
get: vi.fn(),
post: vi.fn(),
}),
}));

How to refactor my test to use Jest's Manual Mock feature?

I am able to get a basic test working with Jest, but when I try to refactor it to use Jest's manual mocks features, the test no longer works.
Any ideas what I could be doing wrong?
Thank you for your time 🙏
error message:
TypeError: _backendService.default.post is not a function
16 |
17 | return $axios
> 18 | .post(`${RESOURCE_PATH}/batch_upload/`, formData, {
| ^
19 | headers: {
20 | "Content-Type": "multipart/form-data",
21 | },
in tests/.../actions.spec.js:
//import $axios from "#/services/backend-service"; // could not get manual mock to work
import actions from "#/store/modules/transactions/actions";
//jest.mock("#/services/backend-service"); // could not get manual mock to work
// this bit of code works
jest.mock("#/services/backend-service", () => {
return {
post: jest.fn().mockResolvedValue(),
};
});
// this bit of code works:end
describe("store/modules/transactions/actions", () => {
it("uploads transactions succeeds", async() => {
const state = {
commit: jest.fn(),
};
await actions.uploadTransactions(
state,
{'file': 'arbitrary filename'}
)
expect(state.commit).toHaveBeenCalledWith('changeUploadStatusToSucceeded');
});
});
in src/.../__mocks__/backend-service.js:
const mock = jest.fn().mockImplementation(() => {
return {
post: jest.fn().mockResolvedValue(),
};
});
export default mock;
in src/.../backend-service.js:
import axios from "axios";
const API_BASE_URL =
`${process.env["VUE_APP_BACKEND_SCHEME"]}` +
`://` +
`${process.env["VUE_APP_BACKEND_HOST"]}` +
`:` +
`${process.env["VUE_APP_BACKEND_PORT"]}` +
`/` +
`${process.env["VUE_APP_BACKEND_PATH_PREFIX"]}`;
const $axios = axios.create({
baseURL: API_BASE_URL,
headers: {
"Content-Type": "application/vnd.api+json",
},
});
export default $axios;
in src/.../actions.js:
import $axios from "#/services/backend-service";
const RESOURCE_NAME = "transaction";
const RESOURCE_PATH = `${RESOURCE_NAME}s`;
export const actions = {
uploadTransactions(state, payload) {
let formData = new FormData();
formData.append("file", payload["file"]);
return $axios
.post(`${RESOURCE_PATH}/batch_upload/`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})
.then((response) => {
state.commit("changeUploadStatusToSucceeded");
})
.catch(function (error) {
if (error.response) {
state.commit("changeUploadStatusToFailed");
}
});
},
};
export default actions;
I've tried looking at examples from these resources, but nothing worked for me:
mocking Axios Interceptors: Mocking axios with Jest throws error “Cannot read property 'interceptors' of undefined”
overriding mock implmentations:
Mock.mockImplementation() not working
How do I change the mock implementation of a function in a mock module in Jest
How to change mock implementation on a per single test basis [Jestjs]
Jest Mock Documentation: https://jestjs.io/docs/mock-function-api#mockfnmockimplementationfn
Jest Manual Mock documentation:
https://jestjs.io/docs/es6-class-mocks
https://jestjs.io/docs/manual-mocks#examples
using 3rd party libraries: https://vhudyma-blog.eu/3-ways-to-mock-axios-in-jest/
simple actions test example: https://lmiller1990.github.io/vue-testing-handbook/vuex-actions.html#creating-the-action
outdated actions test example:
https://vuex.vuejs.org/guide/testing.html#testing-actions
In case it helps others, I ended up just using spies and did not need to use manual mocks.
These were references that helped me figure it out:
https://silvenon.com/blog/mocking-with-jest/functions
https://silvenon.com/blog/mocking-with-jest/modules/
How to mock jest.spyOn for a specific axios call
And here's the example code that ended up working for me:
in tests/.../actions.spec.js:
import $axios from "#/services/backend-service";
import actions from "#/store/modules/transactions/actions";
describe("store/modules/transactions/actions", () => {
let state;
let postSpy;
beforeEach(() => {
state = {
commit: jest.fn(),
};
postSpy = jest.spyOn($axios, 'post')
});
it("uploads transactions succeeds", async() => {
postSpy.mockImplementation(() => {
return Promise.resolve();
});
await actions.uploadTransactions(
state,
{'file': 'arbitrary filename'},
)
expect(state.commit).toHaveBeenCalledWith('changeUploadStatusToSucceeded');
});
it("uploads transactions fails", async() => {
postSpy.mockImplementation(() => {
return Promise.reject({
response: true,
});
});
await actions.uploadTransactions(
state,
{'file': 'arbitrary filename'},
)
expect(state.commit).toHaveBeenCalledWith('changeUploadStatusToFailed');
});
});
in src/.../actions.js:
import $axios from "#/services/backend-service";
const RESOURCE_NAME = "transaction";
const RESOURCE_PATH = `${RESOURCE_NAME}s`;
export const actions = {
uploadTransactions(state, payload) {
let formData = new FormData();
formData.append("account_id", 1); // change to get dynamically when ready
formData.append("file", payload["file"]);
//$axios.interceptors.request.use(function (config) {
// state.commit("changeUploadStatusToUploading");
// return config;
//});
return $axios
.post(`${RESOURCE_PATH}/batch_upload/`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})
.then((response) => {
console.log(response);
state.commit("changeUploadStatusToSucceeded");
})
.catch(function (error) {
if (error.response) {
state.commit("changeUploadStatusToFailed");
}
});
},
};
export default actions;

Jest / Vue Test Utils - Axios mock works only in test file itself

I'm trying to create my first Vue.js 2.x tests and I could use some help:
For some reason, mocking only works on functions defined in the .spec file, but not on imported ones.
For example, the following code works:
// login.spec.js file - note "login" is defined here (which uses "axios" mock)
function login(email, password) {
// I'm defined inside the spec file!
let payload = { email, password };
return axios.post(loginApiUrl, payload);
}
jest.mock('axios');
beforeEach(() => {
jest.clearAllMocks()
})
describe('Login.vue', () => {
it('sample test', async () => {
const data = { a: 1 };
axios.post.mockResolvedValueOnce(data);
let res = await login('abcd#example.com', 'password')
expect(axios.post).toHaveBeenCalledTimes(1)
// The mock works!
While this doesn't:
// login.spec.js file
import auth from '#/api/auth.js'
jest.mock('axios');
beforeEach(() => {
jest.clearAllMocks()
})
describe('Login.vue', () => {
it('sample test', async () => {
const data = { a: 1 };
axios.post.mockResolvedValueOnce(data);
let res = await auth.login('abcd#example.com', 'password')
expect(axios.post).toHaveBeenCalledTimes(1)
// This doesn't work (fetching the original URL)
// auth.js
import axios from 'axios'
import { loginApiUrl } from '../helpers/constants'
const auth = {
login(email, password) {
// params are string payloads login request
// return: axios Promise
let payload = { email, password };
console.log('login')
return axios.post(loginApiUrl, payload);
}
}
export default auth;
Note that the second code makes a call to the actual URL (without the mock)
Anyone has an idea?
Thanks in advance.