How can I mock axios API calls? with using jest - javascript

Hi I'm testing my vuex action async function which is calling api via axios, but I have some problem that it show error like this "
TypeError: Cannot destructure property data of 'undefined' or 'null'.
35 | commit('storeSearchValue', name);
36 | const url = process.env.VUE_APP_URL_API_News + '/news' + '?q=' + name;
> 37 | const { data } = await axios.get(url);"
my vue js code is
async updateSearchValue({ commit }, name) {
commit('storeSearchValue', name);
const url = process.env.VUE_APP_URL_API_News + '/news' + '?q=' + name;
const { data } = await axios.get(url);
commit('storeNewsData', data.result);
},
and this is test file,
import actions from '#/store/modules/data/data-actions.js'
import VueRouter from 'vue-router';
import axios from 'axios';
import {
createLocalVue
} from '#vue/test-utils';
const localVue = createLocalVue();
localVue.use(VueRouter);
jest.mock('axios');
describe('', () => {
test('updateSearchValue', async () => {
const commit = jest.fn()
const name = jest.fn()
await actions.updateSearchValue({
commit,
name
})
expect(commit).toHaveBeenCalledWith('updateSearchValue', name)
})
})

I'm working with jest and TS and trying to do:
axios.get.mockReturnValue...
or:
axios.get.mockImplementationOnce...
returned the following error:
TypeError: mockedAxios.get.mockImplementationOnce is not a function
The thing that finally did the trick for me was:
import axios from 'axios';
jest.mock('axios');
axios.get = jest.fn()
.mockImplementationOnce(() => Promise.resolve({ data: 'mock data' }));

You have used jest.mock('axios') which is automatically generating mock for module and it will create jest.fn() for axios.get, but it will return undefined unless you tell it otherwise
Since you're expecting it to return a resolved promise with object with data property you can use:
axios.get.mockReturnValue(Promise.resolve({
data: 'mock data'
});
or the short-hand:
axios.get.mockResolvedValue({ data: 'mock data' });
Also check this answer

Related

Cannot call store inside an API with Pinia

I'm using Vue 3.2 <script setup>, If I try to acess Pinia's store inside an API Service It throws the following error;
Uncaught ReferenceError: Cannot access 'store' before initialization at api.js?:9:1 (anonymous) # api.js?9:9
src/services/api.js:
import axios from 'axios';
import store from '../stores/index';
// eslint-disable-next-line no-undef
const api = axios.create({ baseURL: import.meta.env.VITE_APP_API_URL });
if (store) {
const { token } = store;
if (token) {
api.defaults.headers.Authorization = `Bearer ${token}`;
}
}
console.log(api);
export default api;
src/stores/index.ts:
import { defineStore } from 'pinia'
import Project from '../models/Project';
import { grantAuthSshd, revokeAuth, parseJwt } from '../services/auth';
const initialUser = JSON.parse(sessionStorage.getItem('Orcamento:token') || '{}');
const useProject = defineStore('project-store', {
state: () => ({
loading: false as boolean,
}),
actions: {
loadingDataTable(status: ((status: boolean) => void) & boolean) {
this.loadingDataTable = status;
},
}
});
I tried to use Pinia's interceptors but the error persists:
import axios from 'axios';
import useProject from '../stores/index';
const api = axios.create({ baseURL: import.meta.env.VITE_APP_API_URL });
// use interceptors
api.interceptors.request.use(
(config) => {
const { token } = store;
if ({token}) {
api.config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
const store = useProject();
export default api;
The problem is that there is indirect circular dependency between services/api.js and stores/index.ts modules, to the point they cannot be evaluated correctly.
useProject() returns a singleton, one of reasons why a store is wrapped with a function is that this prevents it from being accessed too soon. Pinia stores are supposed to be accessed only after Pinia is initialized, otherwise this would require to evaluate the modules that depend on it in a specific order that isn't easy to achieve.
In this case useProject is supposed to be used in-place, not on module evaluation:
api.interceptors.request.use(
(config) => {
const store = useProject();
const { token } = store;
...
Due to how ES modules work, this allows to resolve circular dependency.
A way to avoid circular dependency is to move this code from services/api.js to another module that stores/index.ts doesn't depend on, e.g. entry point.

Trying to stub a function results in Descriptor for property is non-configurable and non-writable

I'm trying to write out a unit test that stubs the getSignedUrl function from the #aws-sdk/s3-request-presigner package, however when I try stub out the function with sinon, I receive the error:
TypeError: Descriptor for property getSignedUrl is non-configurable and non-writable
const s3RequestSigner = require("#aws-sdk/s3-request-presigner");
const expect = require('chai').expect;
const sinon = require('sinon')
....
it('should throw an error when getSignedUrl rejects', async function() {
const sandbox = sinon.createSandbox();
sandbox.stub(s3RequestSigner, "getSignedUrl").rejects("fakeUrl");
sandbox.restore();
})
I'm using node.js 16 and writing javascript rather than typescript. Is there a way to mock out my function, i'm struggling to write my tests otherwise?
I came up with the following workaround for ES6 modules. You can wrap getSignedUrl in your own module and mock that module instead. This approach should work for any modules where sinon is unable to mock a "non-configurable and non-writable" method.
For example:
my-s3-client-internals.js - Your custom wrapper module
// You'll need to import the original method, assign it to
// a new const, then export that const
import { getSignedUrl as getSignedUrl_orig } from '#aws-sdk/s3-request-presigner';
export const getSignedUrl = getSignedUrl_orig;
my-s3-client.js - Consumer of getSignedUrl
// Import the method instead from your custom file
import { getSignedUrl } from './my-s3-client-internals';
// Call it however you normally would, for example:
export const getUrl(bucket, key) {
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
return getSignedUrl(client, command, { expiresIn: 300 });
}
my-s3-client.spec.js - Unit tests for the consumer module
import { getUrl } from './my-s3-client';
import * as clientInternals from './my-s3-client-internals';
import sinon from 'sinon';
it('does something', () => {
// Mock the method exported from your wrapper module
sinon.stub(clientInternals, 'getSignedUrl')
.callsFake(async (client, command, options) => {
return 'fake-url';
});
// Then call your consumer method to test
const url = await getUrl('test-bucket', 'test-key');
expect(url).to.equal('fake-url');
});
So I won't make this the official answer, unless there are no better solutions, but this is what my research has brought about a solution.
The issue is related to this: https://github.com/sinonjs/sinon/issues/2377
Where sinon will throw an error when the Object.descriptor is non-configurable.
There is no obvious way around that currently, that I can find. The way to solve it is to use proxyquire:
const sinon = require('sinon')
const proxyquire = require('proxyquire')
...
it('should throw an error when getSignedUrl rejects', async function() {
const fakeurl = 'hello world'
const fakeURL = sinon.stub().resolves(fakeurl)
const handler = proxyquire(
'../../handlers/presigned_url',
{
'#aws-sdk/s3-request-presigner': {
'getSignedUrl': async () => {
return fakeURL()
}
}
}
)
This will then resolve with whatever you want fakeurl to be.
Another possible solution is to use mockery. E.g. to mock uuid
import { expect } from 'chai';
import mockery from 'mockery';
import sinon from 'sinon';
describe('domain/books', () => {
let createBook;
let uuidStub;
before(async () => {
mockery.enable({
warnOnReplace: false,
warnOnUnregistered: false,
});
uuidStub = sinon.stub();
mockery.registerMock('uuid', { v4: uuidStub });
({ createBook } = await import('../domain/books.js'));
});
afterEach(() => {
sinon.resetHistory();
});
after(() => {
sinon.restore();
mockery.disable();
mockery.deregisterAll();
});
describe('createBook', () => {
it('should save a book and return the id', () => {
const id = 'abc123';
uuidStub.returns(id);
const { id: bookId } = createBook({
title: 'My Book',
author: 'Jane Doe',
});
expect(bookId).to.equal(id);
});
});
});
The mockery setup is a bit tedious, but the library saved me a number of times.

Can not get a response from an API with axios using authorization

I am trying to pull a recipe list from a server that is expecting an auth token from local storage. I created a local axiosWithAuth file because I am going to be hitting multiple endpoints from the API. My auth file looks like this...
import axios from 'axios';
const axiosWithAuth = () => {
const token = localStorage.getItem('token');
return axios.create({
baseURL: 'https://secret-fam-recipes.herokuapp.com/api',
headers: { Authorization: token }
});
};
export default axiosWithAuth
I am then using this axios call with Redux actions so that the data that is returned updates the store for my project. I have tried calling the API two different ways. One with async await which looks like this...
export const listRecipes = () => async (dispatch) => {
dispatch({type: actions.FETCH_RECIPES_REQUEST});
try {
const {recipes} = await axiosWithAuth.get("/recipes")
console.log("recipes", recipes)
dispatch({type: actions.FETCH_RECIPES_SUCCESS, payload: recipes})
} catch(error) {
dispatch({type: actions.FETCH_RECIPES_FAILURE,
payload:
error.resposne && error.response.message
? error.response.message
: error.message
})
}
}
and another without async await which looks like this...
export const listRecipes = () => (dispatch) => {
dispatch({type: actions.FETCH_RECIPES_REQUEST});
axiosWithAuth.get("/recipes")
.then(data => {
return dispatch({type: actions.FETCH_RECIPES_SUCCESS, payload: data})
})
.catch(error => {
dispatch({type: actions.FETCH_RECIPES_FAILURE,
payload:
error.resposne && error.response.message
? error.response.message
: error.message
})
})
}
I also imported my dependencies like so...
import axiosWithAuth from "../utils/axiosWithAuth";
import * as actions from "../constants/recipeConstants";
When I try the async await action I get a 500 error back from the server. When I use the non async await action I get a type error when trying to load the page that says the following..
TypeError: _utils_axiosWithAuth__WEBPACK_IMPORTED_MODULE_0__.default.get is not a function
so obviously there is something wrong with my actions or my axiosWithAuth format. I have tried changing my axiosWithAuth function to use the auth keyword instead of headers but it does not work still. I am not sure if my action is written improperly or if there is something else I should look at. Any help would be greatly appreciated. Thank you!
I was just working with someone and we figured it out. axiosWithAuth is a function so I should have called the function in my action. The correct line is
const {recipes} = await axiosWithAuth().get("/recipes")
More short approach is not to return a function from your custom axios but return the actual axios instance. Since you're just setting a header, you can do this:
import axios from 'axios';
const axiosInstance = axios.create({
baseURL: 'https://secret-fam-recipes.herokuapp.com/api'
});
axiosInstance.interceptors.request.use((config) => {
config.headers['Authorization'] = localStorage.getItem('token');
return config;
});
export default axiosInstance;
And then, just import and use the axios instance:
import axios from './custom-axios';
axios.get('/url-here');

How to configure axios base URL?

I am trying to configure my axios base URL. I found the code below from the following StackOverflow question:
How do I create configuration for axios for default request headers in every http call?
However, I am getting 'Unhandled Rejection (TypeError): Cannot read property 'data' of undefined
(anonymous function)' error. This post is 2 years only and uses a class, but in my code, I am using a function.
The axios call works fine when I do it as normal (not change the base URL). But when I add the axiosConfig and change the base URL I get the error.
If anybody could shine some light on this problem I would be grateful.
axiosConfig.js
import axios from "axios";
const baseURL = process.env.REACT_APP_BASE_URL;
const instance = axios.create({
// .. congigure axios baseURL
baseURL: `${baseURL}`
});
export default instance;
The file where the axios call is made
import axiosConfig from "../axios/axiosConfig";
export const getPosts = () => {
const posts= (dispatch) => {
return axiosConfig
.get('/posts')
.then((response) => {
dispatch({
type: GET_POSTS,
payload: response.data,
});
})
.catch((error) => {
dispatch({
type: POSTS_ERROR,
payload: error.response.data.message,
});
});
};
return posts;
};
It works in the production and development for me.
import axios from "axios";
const api = axios.create({
baseURL: process.env.REACT_APP_BASE_URL || "http://localhost:3030",
});
export default api;
to use I do something like
import api from "../services/api";
const response = await api.post("/sessions", { email, password });
in App.js define
import Axios from "axios";
Axios.defaults.baseURL = "http://127.0.0.1:8000/api/";
in any other components use
insted of http://127.0.0.1:8000/api/user use only user
axios.get('user')
.then((res)=> {
console.log(res)
})
Your axiosConfig.js file it's ok.
You are accessing to 'data' property of an object with value = undefined.
I'm assuming it's response.data
So check https://github.com/axios/axios/issues/883, here you can find more information about why your getting this response
My advise is
Debugg and check the value of response
add .catch(error => {...) at the end of .then(...)

Dispatch within the mock store throws an error in Jest test

In my React and Redux app, trying to run my first test using Jest & TypeScript and I am unable to run the test successfully. For clarification sake, the app itself is running without errors & works well. It appears, I am setting up the Axios mock response incorrectly. When I run the Jest tests, getting following error:
Actions may not have an undefined "type" property. Have you misspelled
a constant?...
Action creator:
export const fetchBoats = (
userAuthToken: string,
filterValues: BoatSearchFilterParams = {},
page: number = 1
) => (dispatch: any) => {
const filterParams: string = setFilterParamsAsString(filterValues);
const url = `${GET_BOATS}?${filterParams}&page=${page}`;
return dispatch({
types: [FETCH_REQUEST, FETCH_SUCCESS, FETCH_FAILURE],
promise: axios({
method: "get",
url: url,
headers: {
Authorization: `Token ${userAuthToken}`
}
})
});
};
My test file in its current state, without assertions, is:
import configureStore from "redux-mock-store";
import thunk from "redux-thunk";
import { fetchResults } from "./boats";
import axios from "axios";
const mockedAxios = axios as jest.Mocked<typeof axios>;
const mockStore = configureStore([thunk]);
const store = mockStore();
jest.mock("axios");
const respObj = {
results : ["a", "b", "c", "d"]
};
describe("Test the 'Boats' action", () => {
beforeEach(() => {
store.clearActions();
});
it("should have data structure matching the expected structure", () => {
const respObj = { data: resp };
mockedAxios.get.mockResolvedValue(respObj);
const ac: any = store.dispatch(fetchBoats("abcabcabc"));
// expect(ac).toContain(); commented out.
});
});
How can I get the action creator to return the respObj response & then assert on it?
EDIT:
There appears some confusion on how the action creator works. I have a custom middleware which intercepts an action with a signature as this action creator above. This action is valid & working very well :)

Categories