Mock axios in a Vue application for frontend testing - javascript

When testing the frontend of my Vue application, I sometimes want to skip the communication with the API backend.
I came up with the following idea:
add state in Vuex store: skipServerCommunication.
when skipServerCommunication = false: send the axios request as expected
when skipServerCommunication = true: return mocked data
The easy way would be to add an check before every call of axios.post(...) or axios.get(...), but I don't want to have to add code in every api function I wrote.
So I thought about using the interceptors of axios, but I think it's not possible to stop a request, and just return mocked data, right?
I then thought about wrapping axios, and dynamically returning the real axios instance or a mocked one, which also implements the .post(), .get() and all the other axios functions based on the Store state. I thought about something like this (I'm working with TypeScript):
import store from '#/vuex-store'
const axios = Axios.create( // real store config );
const axiosMock = {
get(path: string) {
if (path == '/test') {
return Promise.resolve( // mocked data );
}
return Promise.resolve(true)
}
}
class AxiosWrapper {
get instance() {
if (store.state.skipServerCommunication) {
return axiosMock;
}
return axios
}
}
export default new AxiosWrapper();
But this solution has some problems:
I need to replace all axios calls with axiosWrapper.instance.get(...). Can I somehow avoid this and mock axios in a way that I can still work with axios.get(...)?
VSCode isn't able to provide autocompletion anymore because the returned instance is either of type AxiosStatic or "MyMockType". So I thought about implementing the AxiosStatic interface, but I struggle to do that correctly because of the two anonymous functions in the AxiosInstance interface. Is there an alternative way to overcome this problem?

Use axios-mock-adapter instead. You can mock axios calls as needed per test, using advanced matching patterns.
Example usage:
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
describe('UserList', () => {
it('gets users on mount', async () => {
const mock = new MockAdapter(axios)
mock.onGet('/users').reply(200, {
users: [{ id: 1, name: 'John Smith' }],
})
const wrapper = shallowMount(UserList)
await wrapper.find('.getUsersBtn').trigger('click')
expect(wrapper.vm.users[0].id).toBe(1)
})
})

Related

Vue-test-utils doesn't wait for the response from the API request

I've been really struggling to do something very simple. I have to write some tests for a Vue application.
The scenario I want to test is the following:
The user fills in a form
The user submits the form
The values from the form are sent to the backend in one object
The server responds with an object containing the fields of the object it has received in the request (plus some new fields).
The Vue app stores the result in the Vuex store
I want my test to check if, after the form has been validated, the request is made and the returned values are properly stored in the store.
This is super basic, but for some reason I can't get the test to work.
I have a globally registered component that makes axios accessible by using this.api.
So in my test, I have the following (the file is simplified for this post):
...
import axios from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
import flushPromises from 'flush-promises';
// This is the axios setup used in the components
import api from '../../src/mixins/api';
// Components
import MyComponent from '../component.vue';
describe('MyComponent', () => {
// Set up the local vue
...
const wrapper = mount(MyComponent, {
localVue,
...
});
beforeEach(() => {
const instance = axios.create({
baseURL: 'some/url/',
});
wrapper.vm.api = new AxiosMockAdapter(instance);
wrapper.vm.api.onPost('url/').replyOnce(201, { data: { foo: 'bar' } });
});
it('should retrieve the data', async () => {
wrapper.find('#submit').trigger('click');
await flushPromises();
expect(wrapper.vm.$store.state.foo !== undefined).toBe(true);
});
});
But the test isn't successful. I think the request is made properly, but by the time the test reaches its end the response hasn't be received yet. And this is despite using flushPromises(). I also tried to use setTimeout but with no success neither.
I'm new to unit testing (especially on front end apps) and I have no idea what else to try. Nothng works... It's very frustrating because what I'm tryng to do is pretty straight forward and basic.
Anyone has an idea what to do ?

Redux state in regualar javascript function

I am working on a ReactJS application, which uses many API calls to get data from the server. These API calls are organized into multiple classes and all of them use another class which issues a HTTP request using Axios.
Example markup and flow: (this is hand typed to keep it simple, please ignore any minor mistake or omissions)
class MyComponent extends Component{
componentDidMount(){
this.props.getData(//parameters//); //getData is an action bound via react-redux connect.
}
}
export const getData = (//parameters//) => (
dispatch: any
) => {
return new Promise((resolve, reject) => {
//Some business logic
let axiosService = new axiosService();
axiosService .get(//parameters//).then((data: any) => {
dispatch({
type: "RECEIVE_DATA",
payload: data
});
resolve();
});
});
};
export default class AxiosService {
config: AxiosRequestConfig;
url : string;
constructor() {
this.url = "http://localhost:8080/api/";
this.config = {
headers: {
//I want to inject Authorization token here
},
responseType: "json"
};
}
get = () => {
return new Promise((resolve, reject) => {
resolve(axios.get(this.url, this.config));
});
};
}
Now I want to inject auth token as Authorization header when AxiosService makes an API call. This token is available in redux store. How can I access redux store in my service?
I can pass the token as a parameter to AxiosService get call, but that is messy. Based on my reading custom thunk middleware might help me here, but not sure how to update my service call. I have many actions, that need to use AxiosService.
There are many ways. For e.g. you can call Axios from the reducer or from the action builder. The reducer have the state, and redux-thunk is a popular library for that, that gives you the store to the Action Builder.
You can also use redux-saga, to listen to actions and perform http requests in response. redux-saga also gives you the store.
If you don't want all the libraries you can do a workaround. Where you created the store, save it to the global window:
window.store=redux.createStore(...)
Then, your http library can access the store:
window.store.getState()
It's not recommended way, but it's working.

Vuex and Axios in asyncData

I want to access my Vuex data in asyncData but I can't. Also I can't seem to be able to use axios module in asyncData.
I tried a lot.
pages/index.vue
export default {
asyncData() {
//in my project there's a lot more code here but i think this is enough
let id = this.$store.state.id //i know i should prob use getter but no
//here i want to do an axios req with this var "id" but axios doesnt work
return axios.get(`url${id}`)
.then(...) //not gonna write it out here
}
}
store/index.js
export const state = () => ({
id: "#5nkI12_fSDAol/_Qa?f"
})
I expect it to get the ID from Vuex and then do a Axios req. Whole app doesn't work.
You can't use this in asyncData. The function asyncData gets passed a context object as param which basically represents the this keyword.
First you need to specify what do you want retrieve from the context object and then you can use it.
In your case do something like this:
export default {
asyncData({ $axios, store }) { //here we get Vuex and Axios
let id = store.state.id
return $axios.get(`url${id}`)
.then(...)
}
}

Pattern for redirecting on unauthorized vuex actions

Navigation guards are perfect for redirecting unauthorized users to a login page, but what does one do to redirect unauthorized vuex actions to a login page?
I can do this easily enough in the vue method where I'm calling the action like so:
if (!this.isLoggedIn) {
this.$router.push({ name: 'login', query: { comeBack: true } })
$(`.modal`).modal('hide')
return
}
But then I'm inserting these 5 lines of code for every component method that requires authorization.
All the solutions I can think of sound hacky, so I'm wondering what the vuex way is:
In order to reject it at the vuex action level, I have to pass up the $router instance, and I'm still reusing the 5 lines for each action that requires auth.
I can handle this in a utility file, but then I'm handling $router instance in that file.
I can use a global vue mixin and call it (a) before making a call and then again (b) when getting a 401 back from the server.
All those seem odd. What vuex way am I missing here?
This sounds like a job for middleware. Unfortunately, Vuex doesn't have an official way to do middleware.
There is a subscribeAction() but that runs after the commit, so does not allow mods to the action. There is also a proposal Middleware processing between actions and mutation.
As I see it, we want middleware to be able to do two generic things
cancel the action
allow alternative actions to be called
The second is difficult to do without patching store.dispatch() or messing with the private property _actions after store has been created.
However, to guard an action as you describe, we only need to be able to cancel it.
Here is a poor-man's middleware for the modules pattern for Vuex store which I prefer.
store construction from modules
export const store = new Vuex.Store({
modules: {
config,
pages: applyMiddleware(pages),
measures,
user,
loadStatus,
search
}
})
applyMiddleware
const applyMiddleware = function(module) {
if(module.middlewares) {
Object.values(module.middlewares).forEach(middlewareFn => {
Object.keys(module.actions).forEach(actionName => {
const actionFn = module.actions[actionName]
module.actions[actionName] = addMiddleware(actionName, actionFn, middlewareFn)
});
})
}
return module;
}
addMiddleware
const addMiddleware = function(actionName, actionFn, middlewareFn) {
return function(context, payload) {
const resultFn = middlewareFn(actionFn)
if(resultFn) {
resultFn(context, payload)
}
}
}
defining middleware in the module
const actions = {
myAction: (context, payload) => {
...
context.commit('THE_ACTION', payload)
...
},
}
const middlewares = {
checkAuthMiddleware: (action) => {
return this.isLoggedIn
? action // if logged-in run this action
: null; // otherwise cancel it
}
}
export default {
state,
getters,
mutations,
actions,
middlewares
}
This implementation has module-specific middleware functions, but you could also define them globally and apply to as many modules as applicable.

Relay Modern - How to use fetchQuery without fragmentContainer

so I am working on relay for react application with server side rendering. I'm trying to use relay to get data from graphql endpoint.
I was kinda following this, using fetchQuery to get data by making request from compiled graphql by relay-compiler.
e.g.
import { graphql, fetchQuery } from 'react-relay'
const query = graphql`
query SomeQuery($id: ID) {
author(id: $id) {
name
...SomeFragment_authorData
}
}
`
const variables = { "id" : "1f" };
fetchQuery(env, query, variables)
.then((jsonData) => { console.log(jsonData); })
when it finishes running, it gives me some sort of this object:
{
author: {
__fragment: { ... }
...
}
}
Which I assume will be used by children components wrapped with createFragmentContainer() to get the real data.
Since I'm not using createFragmentContainer(), I'm not sure how to get the data properly, is there any way to transform above response into the real data?
any help would be appreciated!
Note:
At the moment this is what I do to get the data: env._network.fetch(query(), variables). It is working, but it doesn't seem right that I need to dig into private variable, in order to get the fetch that I provided to Network object.
You need to add a relay directive to your fragment spread:
...SomeFragment_authorData #relay(mask: false)
you can use relay environment and relay-runtime utils.
import {
createOperationDescriptor,
getRequest,
} from 'relay-runtime';
const request = getRequest(query);
const operation = createOperationDescriptor(request, variables);
environment.execute({ operation }).toPromise();

Categories