I have a utils class that uses axios to perform simple REST calls. When writing a jest unit test, although the tests pass, i seem to get an advisory error:
C:/home/dev/node_modules/axios/lib/core/createError.js:16
var error = new Error(message);
Error: Network Error
at createErrorC:/home/dev/node_modules/axios/lib/core/createError.js:16
config: {
// ....
header: { Accept: 'application/json, text/plain, */*' },
withCredentials: true,
responseType: 'json',
method: 'get',
url: 'http://mock.rest.server.com:1234/rest/user/data/adam',
data: undefined
},
request: XMLHttpRequest {},
response: undefined,
isAxiosError: true,
toJSON: [Function: toJSON]
utility.ts
import axios, { AxiosResponse } from 'axios'
axios.defaults.withCredentials = true;
axios.defaults.responseType = 'json';
export class UserUtils {
public getUserConfig(userName: string): Promise<AxiosResponse> {
if(!userName) {
return;
}
return axios.get('http://mock.rest.server.com:1234/rest/user/data/' + userName);
}
}
utility.test.ts
import axios from 'axios';
import { UserUtils } from '../../utility';
describe("Utility test", () => {
const utils = new UserUtils();
jest.mock('axios', () => {
return {
post: jest.fn(),
get: jest.fn()
}
}
// clear all mocks
beforEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
test("get user data",() => {
jest.spyOn(axios, 'get');
utils.getUserConfig('adam')
.then(repsonse => {
expect(axios.get).toHaveBeenCalledWith('http://mock.rest.server.com:1234/rest/user/data/adam');
});
});
});
Maybe this thread can help you: https://stackoverflow.com/a/51654713/20293448
Personally, I like using jest manual mocks (docs)
In my project, I have:
// src/__mocks__/axios.ts
import axios from 'axios';
const mockAxios = jest.genMockFromModule<typeof axios>('axios');
// this is the key to fix the axios.create() undefined error!
mockAxios.create = jest.fn(() => mockAxios);
// eslint-disable-next-line import/no-default-export
export default mockAxios;
// src/.../test.ts
import mockAxios from 'axios';
const mockedPost = mockAxios.post as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
});
...
expect(mockedPost).toHaveBeenCalledWith(route, payload);
Hope this helps!
Related
I'm using vue3+typescript+pinia.
I am trying to follow the docs to crete tests but no success, got errors.
I want to test a store action which uses function that returns a promise.
EDITED:
The store pinia action
actions: {
async createContact(contact: Contact) {
console.log('this', this);
this.isLoading = true
ContactDataService.createContact(contact)
.then(response => {
this.sucess = true
console.log(response)
})
.catch(error => {
this.hasError = true
console.log(error);
})
this.isLoading = false
},
},
The exported class instance:
import Contact from "#/types/ContactType";
import http from "../http-commons";
class ContactDataService {
createContact(contact: Contact): Promise<any> {
const headers = {
"Content-Type": "application/json",
"accept": "*/*",
"Access-Control-Allow-Origin": "*"
}
return http.post("/contact", contact, { headers });
}
}
export default new ContactDataService();
The test:
import { setActivePinia, createPinia } from 'pinia'
import { describe, it, expect, beforeEach, vi } from "vitest";
import { useContactStore } from '#/stores/ContactStore'
import ContactDataService from "../../services/ContactDataService"
import Contact from '#/types/ContactType';
vi.mock('../../services/ContactDataService', () => {
const ContactDataService = vi.fn()
ContactDataService.prototype.createContact = vi.fn()
return { ContactDataService }
})
const contactExample: Contact = {
firstName: 'string',
lastName: 'string',
emailAddress: 'string',
}
describe('ContactStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('createContact', async () => {
const contactStore = useContactStore()
// expect(contactStore.sucess).toBeFalsy()
contactStore.createContact(contactExample)
// expect(contactStore.sucess).toBeTruthy()
})
})
When I run test I cant figure out how to mock the ContactDataService.createContact(contact) inside the action createContact.
Error: [vitest] No "default" export is defined on the "mock:/src/services/ContactDataService.ts"
I am using apollo client and server and from the server the image uploads to Cloudinary no problem. but In the app whoever I tried to send the file to the server, it says createReadStream is not a function, code: INTERNAL_SERVER_ERROR. I am using image picker dependency of react-native which gives me a file path and other file specs and base64 whether I send the whole file or only the path or base64 it does not work.
the file is not going through correctly
my apolloConfig.js is:
import { ApolloClient, ApolloLink } from "#apollo/client";
import { Observable } from "apollo-link";
import { withClientState } from "apollo-link-state";
import { InMemoryCache } from "apollo-cache-inmemory";
import { onError } from "#apollo/client/link/error";
import { createUploadLink } from "apollo-upload-client";
import { buildAxiosFetch } from "#lifeomic/axios-fetch";
import { Config } from "App/Config";
import axios from "axios";
import AsyncStorage from "#react-native-community/async-storage";
const SERVER_URL = Config.API_URL;
const cache = new InMemoryCache({});
const httpLink = createUploadLink({
uri: SERVER_URL,
credentials: "same-origin",
fetch: buildAxiosFetch(axios, (config, input, init) => ({
...config,
onUploadProgress: init.onUploadProgress,
})),
});
const request = async (operation) => {
const token = await AsyncStorage.getItem("token");
console.log(token);
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : "",
},
});
};
export const requestLink = new ApolloLink(
(operation, forward) =>
new Observable((observer) => {
let handle;
Promise.resolve(operation)
.then((oper) => request(oper))
.then(() => {
handle = forward(operation).subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
});
})
.catch(observer.error.bind(observer));
return () => {
if (handle) handle.unsubscribe();
};
})
);
const link = ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
console.log("[graphQLErrors]", graphQLErrors);
graphQLErrors.map(({ message, extensions }) => {
console.log(
`[GraphQL error]: Message: ${message}, code: ${extensions.code}`
);
if (extensions.code === "UNAUTHENTICATED") {
AsyncStorage.clear();
}
});
}
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
}),
requestLink,
httpLink,
]);
const client = new ApolloClient({ link, cache });
export { client };
my uploadAvatar.js
try {
const { data } = await uploadImage({
variables: { file: image },
});
console.log(data);
} catch (error) {
console.log(error);
Toast.show({
type: "error",
position: "bottom",
text1: "Authentication Failed",
text2: "Something went wrong while uploading photo!",
});
}
});
};
You have to convert your file to a ReactNativeFile as the docs of apollo-upload-client mentioned
import { ReactNativeFile } from 'apollo-upload-client';
const file = new ReactNativeFile({
uri: uriFromCameraRoll,
name: 'a.jpg',
type: 'image/jpeg',
});
more info : https://github.com/jaydenseric/apollo-upload-client
Since I want to setup Axios interceptors with React Context, the only solution that seems viable is creating an Interceptor component in order to use the useContext hook to access Context state and dispatch.
The problem is, this creates a closure and returns old data to the interceptor when it's being called.
I am using JWT authentication using React/Node and I'm storing access tokens using Context API.
This is how my Interceptor component looks like right now:
import React, { useEffect, useContext } from 'react';
import { Context } from '../../components/Store/Store';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
const ax = axios.create();
const Interceptor = ({ children }) => {
const [store, dispatch] = useContext(Context);
const history = useHistory();
const getRefreshToken = async () => {
try {
if (!store.user.token) {
dispatch({
type: 'setMain',
loading: false,
error: false,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
});
const { data } = await axios.post('/api/auth/refresh_token', {
headers: {
credentials: 'include',
},
});
if (data.user) {
dispatch({
type: 'setStore',
loading: false,
error: false,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
authenticated: true,
token: data.accessToken,
id: data.user.id,
name: data.user.name,
email: data.user.email,
photo: data.user.photo,
stripeId: data.user.stripeId,
country: data.user.country,
messages: {
items: [],
count: data.user.messages,
},
notifications:
store.user.notifications.items.length !== data.user.notifications
? {
...store.user.notifications,
items: [],
count: data.user.notifications,
hasMore: true,
cursor: 0,
ceiling: 10,
}
: {
...store.user.notifications,
count: data.user.notifications,
},
saved: data.user.saved.reduce(function (object, item) {
object[item] = true;
return object;
}, {}),
cart: {
items: data.user.cart.reduce(function (object, item) {
object[item.artwork] = true;
return object;
}, {}),
count: Object.keys(data.user.cart).length,
},
});
} else {
dispatch({
type: 'setMain',
loading: false,
error: false,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
});
}
}
} catch (err) {
dispatch({
type: 'setMain',
loading: false,
error: true,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
});
}
};
const interceptTraffic = () => {
ax.interceptors.request.use(
(request) => {
request.headers.Authorization = store.user.token
? `Bearer ${store.user.token}`
: '';
return request;
},
(error) => {
return Promise.reject(error);
}
);
ax.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
console.log(error);
if (error.response.status !== 401) {
return new Promise((resolve, reject) => {
reject(error);
});
}
if (
error.config.url === '/api/auth/refresh_token' ||
error.response.message === 'Forbidden'
) {
const { data } = await ax.post('/api/auth/logout', {
headers: {
credentials: 'include',
},
});
dispatch({
type: 'resetUser',
});
history.push('/login');
return new Promise((resolve, reject) => {
reject(error);
});
}
const { data } = await axios.post(`/api/auth/refresh_token`, {
headers: {
credentials: 'include',
},
});
dispatch({
type: 'updateUser',
token: data.accessToken,
email: data.user.email,
photo: data.user.photo,
stripeId: data.user.stripeId,
country: data.user.country,
messages: { items: [], count: data.user.messages },
notifications:
store.user.notifications.items.length !== data.user.notifications
? {
...store.user.notifications,
items: [],
count: data.user.notifications,
hasMore: true,
cursor: 0,
ceiling: 10,
}
: {
...store.user.notifications,
count: data.user.notifications,
},
saved: data.user.saved,
cart: { items: {}, count: data.user.cart },
});
const config = error.config;
config.headers['Authorization'] = `Bearer ${data.accessToken}`;
return new Promise((resolve, reject) => {
axios
.request(config)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(error);
});
});
}
);
};
useEffect(() => {
getRefreshToken();
if (!store.main.loading) interceptTraffic();
}, []);
return store.main.loading ? 'Loading...' : children;
}
export { ax };
export default Interceptor;
The getRefreshToken function is called every time a user refreshes the website to retrieve an access token if there is a refresh token in the cookie.
The interceptTraffic function is where the issue persists.
It consists of a request interceptor which appends a header with the access token to every request and a response interceptor which is used to handle access token expiration in order to fetch a new one using a refresh token.
You will notice that I am exporting ax (an instance of Axios where I added interceptors) but when it's being called outside this component, it references old store data due to closure.
This is obviously not a good solution, but that's why I need help organizing interceptors while still being able to access Context data.
Note that I created this component as a wrapper since it renders children that are provided to it, which is the main App component.
Any help is appreciated, thanks.
Common Approach (localStorage)
It is a common practice to store the JWT in the localStorage with
localStorage.setItem('token', 'your_jwt_eykdfjkdf...');
on login or page refresh, and make a module that exports an Axios instance with the token attached. We will get the token from localStorage
custom-axios.js
import axios from 'axios';
// axios instance for making requests
const axiosInstance = axios.create();
// request interceptor for adding token
axiosInstance.interceptors.request.use((config) => {
// add token to request headers
config.headers['Authorization'] = localStorage.getItem('token');
return config;
});
export default axiosInstance;
And then, just import the Axios instance we just created and make requests.
import axios from './custom-axios';
axios.get('/url');
axios.post('/url', { message: 'hello' });
Another approach (when you've token stored in the state)
If you have your JWT stored in the state or you can grab a fresh token from the state, make a module that exports a function that takes the token as an argument and returns an axios instance with the token attached like this:
custom-axios.js
import axios from 'axios';
const customAxios = (token) => {
// axios instance for making requests
const axiosInstance = axios.create();
// request interceptor for adding token
axiosInstance.interceptors.request.use((config) => {
// add token to request headers
config.headers['Authorization'] = token;
return config;
});
return axiosInstance;
};
export default customAxios;
And then import the function we just created, grab the token from state, and make requests:
import axios from './custom-axios';
// logic to get token from state (it may vary from your approach but the idea is same)
const token = useSelector(token => token);
axios(token).get('/url');
axios(token).post('/url', { message: 'hello' });
I have a template that works in a system with millions of access every day.
This solved my problems with refresh token and reattemp the request without crashing
First I have a "api.js" with axios, configurations, addresses, headers.
In this file there are two methods, one with auth and another without.
In this same file I configured my interceptor:
import axios from "axios";
import { ResetTokenAndReattemptRequest } from "domain/auth/AuthService";
export const api = axios.create({
baseURL: process.env.REACT_APP_API_URL,
headers: {
"Content-Type": "application/json",
},
});
export const apiSecure = axios.create({
baseURL: process.env.REACT_APP_API_URL,
headers: {
Authorization: "Bearer " + localStorage.getItem("Token"),
"Content-Type": "application/json",
},
export default api;
apiSecure.interceptors.response.use(
function (response) {
return response;
},
function (error) {
const access_token = localStorage.getItem("Token");
if (error.response.status === 401 && access_token) {
return ResetTokenAndReattemptRequest(error);
} else {
console.error(error);
}
return Promise.reject(error);
}
);
Then the ResetTokenAndReattemptRequest method. I placed it in another file, but you can place it wherever you want:
import api from "../api";
import axios from "axios";
let isAlreadyFetchingAccessToken = false;
let subscribers = [];
export async function ResetTokenAndReattemptRequest(error) {
try {
const { response: errorResponse } = error;
const retryOriginalRequest = new Promise((resolve) => {
addSubscriber((access_token) => {
errorResponse.config.headers.Authorization = "Bearer " + access_token;
resolve(axios(errorResponse.config));
});
});
if (!isAlreadyFetchingAccessToken) {
isAlreadyFetchingAccessToken = true;
await api
.post("/Auth/refresh", {
Token: localStorage.getItem("RefreshToken"),
LoginProvider: "Web",
})
.then(function (response) {
localStorage.setItem("Token", response.data.accessToken);
localStorage.setItem("RefreshToken", response.data.refreshToken);
localStorage.setItem("ExpiresAt", response.data.expiresAt);
})
.catch(function (error) {
return Promise.reject(error);
});
isAlreadyFetchingAccessToken = false;
onAccessTokenFetched(localStorage.getItem("Token"));
}
return retryOriginalRequest;
} catch (err) {
return Promise.reject(err);
}
}
function onAccessTokenFetched(access_token) {
subscribers.forEach((callback) => callback(access_token));
subscribers = [];
}
function addSubscriber(callback) {
subscribers.push(callback);
}
I have a module called authProvider.js which I want to mock when I'm testing one of my functions in api.js.
I have set "automock": true in my jest config.
This is my structure
src
|-auth
| |-__mocks__
| | |-authProvider.js
| |-authProvider.js
|-utils
|-api.js
|-api.test.js
This is what I have tried so far but only having success with the first test case. I'm not sure how to set up the mocking in the second test case...
api.test.js
import { mockAuthProvider } from "../auth/__mocks__/authProvider";
import { getDefaultHeaders, getValueIndexByColumnName } from './api';
describe('API utils', () => {
describe('getDefaultHeaders', () => {
it('Not authenticated', async () => {
expect(await getDefaultHeaders()).toEqual({
'Content-Type': 'application/json'
});
});
it('Authenticated', async () => {
mockAuthProvider.getAccount.mockImplementationOnce(() =>
Promise.resolve({user: 'test'})
);
const headers = await getDefaultHeaders();
expect(mockAuthProvider.getAccount).toBeCalled();
expect(headers).toEqual({
Authorization: 'Bearer abc123',
'Content-Type': 'application/json'
});
});
});
});
api.js
import { authProvider } from '../auth/authProvider';
import settings from '../settings';
export async function getDefaultHeaders() {
const account = await authProvider.getAccount();
const authenticationParameters = {
scopes: ['api://' + settings.AD_CLIENT_ID + '/login'],
redirectUri: window.location.origin + '/auth.html'
};
let token;
if (account) {
try {
token = await authProvider.acquireTokenSilent(authenticationParameters);
} catch (error) {
token = await authProvider.acquireTokenPopup(authenticationParameters);
}
}
if (token) {
return {
Authorization: `Bearer ${token.accessToken}`,
'Content-Type': 'application/json'
}
}
return {
'Content-Type': 'application/json'
}
}
__ mocks __/authProvider.js
const mockAuthProvider = {
getAccount: jest.fn(),
acquireTokenSilent: jest.fn(),
acquireTokenPopup: jest.fn()
};
module.exports = {
mockAuthProvider
};
Error message
Expected number of calls: >= 1
Received number of calls: 0
18 | const headers = await getDefaultHeaders();
19 |
> 20 | expect(mockAuthProvider.getAccount).toBeCalled();
| ^
UPDATE
I added a file to mock the whole module that exports the auth provider, but still not the best way to solve it I think. I'm having difficulties using it in multiple test cases since I need to specify the return values in a specific order.
Is there a better way to solve this issue?
__ mocks __/react-aad-msal.js
import React from 'react';
const errorObj = {
message: 'Some error'
};
export const mockGetAccount = jest.fn()
.mockReturnValueOnce(null) // Not authenticated
.mockReturnValueOnce({user: 'test'}) // Authenticated silent
.mockReturnValueOnce({user: 'test'}); // Authenticated popup
export const mockAcquireTokenSilent = jest.fn()
.mockReturnValueOnce({accessToken: 'abc123'}) // Authenticated silent
.mockRejectedValueOnce(errorObj); // Authenticated popup
export const mockAcquireTokenPopup = jest.fn()
.mockReturnValueOnce({accessToken: 'abc123'}); // Authenticated popup
export const MsalAuthProvider = jest.fn(() => ({
getAccount: mockGetAccount,
acquireTokenSilent: mockAcquireTokenSilent,
acquireTokenPopup: mockAcquireTokenPopup
}));
export const AuthenticationState = {
Authenticated: 'Authenticated',
Unauthenticated: 'Unauthenticated'
};
export const LoginType = {
Popup: 'popup'
};
export const AuthenticationActions = {
Initializing: 'Initializing',
Initialized: 'Initialized',
AcquiredIdTokenSuccess: 'AcquiredIdTokenSuccess',
AcquiredAccessTokenSuccess: 'AcquiredAccessTokenSuccess',
AcquiredAccessTokenError: 'AcquiredAccessTokenError',
LoginSuccess: 'LoginSuccess',
LoginError: 'LoginError',
AcquiredIdTokenError: 'AcquiredIdTokenError',
LogoutSucc: 'LogoutSucc',
AuthenticatedStateChanged: 'AuthenticatedStateChanged'
};
export const AzureAD = ({children}) => <div>{children}</div>;
The new api.test.js looks like this, note that the order of the tests now is important since the return values from the mock are in a fixed order.
import { getDefaultHeaders, axiosCreate, getValueIndexByColumnName } from './api';
describe('API utils', () => {
describe('getDefaultHeaders', () => {
it('Not authenticated', async () => {
const headers = await getDefaultHeaders();
expect(headers).toEqual({
'Content-Type': 'application/json'
});
});
it('Authenticated silent', async () => {
const headers = await getDefaultHeaders();
expect(headers).toEqual({
Authorization: 'Bearer abc123',
'Content-Type': 'application/json'
});
});
it('Authenticated popup', async () => {
const headers = await getDefaultHeaders();
expect(headers).toEqual({
Authorization: 'Bearer abc123',
'Content-Type': 'application/json'
});
});
});
describe('axiosCreate', () => {
it('Create axios API base', () => {
expect(axiosCreate()).toBeTruthy();
});
});
describe('getValueIndexByColumnName', () => {
it('Invalid input data', () => {
expect(getValueIndexByColumnName([], null)).toEqual(null);
expect(getValueIndexByColumnName(['column1'], null)).toEqual(-1);
});
it('Valid input data', () => {
expect(getValueIndexByColumnName(['column1'], 'column')).toEqual(-1);
expect(getValueIndexByColumnName(['column1'], 'column1')).toEqual(0);
expect(getValueIndexByColumnName(['column1', 'column2', 'column3'], 'column2')).toEqual(1);
});
});
});
I'm currently having a lot of trouble running tests on my redux actions. The test passes but I get the following error each time it is ran:
ReferenceError: localStorage is not defined
I also got an error before which was:
ReferenceError: fetch is not defined
I fixed this by using isomorphic-fetch. Anyway I am unsure on how I should configure Mocha to run these front end tests. Any help would be much appreciated.
Mocha test command:
mocha -w test/test_helper.js test/*.spec.js
test_helper.js:
require('babel-register')();
var jsdom = require('jsdom').jsdom;
var exposedProperties = ['window', 'navigator', 'document'];
global.document = jsdom('');
global.window = document.defaultView;
Object.keys(document.defaultView).forEach((property) => {
if (typeof global[property] === 'undefined') {
exposedProperties.push(property);
global[property] = document.defaultView[property];
}
});
global.navigator = {
userAgent: 'node.js'
};
documentRef = document;
auth.actions.spec.js
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import * as actions from '../client/app/actions/auth'
import * as types from '../client/app/constants/ActionTypes'
import nock from 'nock'
import chai from 'chai'
import sinon from 'sinon'
var expect = chai.expect
import { SERVER_API } from './config'
const middlewares = [ thunk ]
const mockStore = configureMockStore(middlewares)
describe('auth actions', () => {
afterEach(() => {
nock.cleanAll()
})
it('creates LOGIN_REQUEST and LOGINSUCCESS when correct username and password provided', () => {
nock(SERVER_API)
.post('/login', {
username: 'test',
password: 'password'
})
.reply(200, {
token: 'TOKEN'
});
const expectedActions = [
{
type: types.LOGIN_REQUEST,
isFetching: true,
isAuthenticated: false,
creds: {
username: 'test',
password: 'password'
}
},
{
type: types.LOGIN_SUCCESS,
isFetching: false,
isAuthenticated: true,
token: 'TOKEN'
}
]
const INITAL_STATE = {
isFetching: false,
isAuthenticated: false
}
const store = mockStore(INITAL_STATE)
return store.dispatch(actions.loginUser({username:'test',password:'password'}))
.then(() => {
expect(store.getActions()).to.deep.equal(expectedActions)
})
})
})
auth.js
import { push } from 'react-router-redux'
import 'es6-promise'
import fetch from 'isomorphic-fetch'
import {
LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAILURE
} from '../constants/ActionTypes.js'
import { SERVER_PORT } from '../constants/config'
function requestLogin(creds) {
return {
type: LOGIN_REQUEST,
isFetching: true,
isAuthenticated: false,
creds
}
}
function receiveLogin(user) {
return {
type: LOGIN_SUCCESS,
isFetching: false,
isAuthenticated: true,
token: user.token
}
}
function loginError(message) {
return {
type: LOGIN_FAILURE,
isFetching: false,
isAuthenticated: false,
message
}
}
export function loginUser(creds) {
let config = {
method: 'POST',
headers: { 'Content-Type':'application/x-www-form-urlencoded' },
body: `username=${creds.username}&password=${creds.password}`
}
return dispatch => {
dispatch(requestLogin(creds))
return fetch('http://localhost:'+SERVER_PORT+'/api/login', config)
.then(response =>
response.json()
.then(user => ({ user, response }))
).then(({ user, response }) => {
if (!response.ok) {
dispatch(loginError(user.message))
return Promise.reject(user)
}
else {
dispatch(receiveLogin(user))
localStorage.setItem('token', user.token) //offending line
dispatch(push('foo'))
}
}).catch(err => console.log("Error: ", err))
}
}
Thanks.
Pretty straightforward error, you can't use localStorage on your mocha tests because window.localStorage isn't defined. There are two ways to fix this. The more "canon" way would be to move that localStorage call from your action because this is a side-effect which is an anti-pattern in redux actions. Instead you should have middleware that catches this action and sets the localStorage.
By doing that, you already eliminate the issue of testing this action.
If, however, you don't know how to do that and don't think it wise, then you can "fake" localStorage by making a global variable at the top of your mocha test file which creates a fake localStorage. I recommend against this one but it's definitely a solution that could work in your case.