Mocking an axios post using React Testing Library / Jest - reactjs

I am struggling with figuring out how to correctly mock this Axios post in my React test. It seems like the documentation and tutorials out there are all over the place in terms of how they do it, no strong consensus or best practice.
So here is my Test:
BatchList.test.js
import React from 'react';
import { setupWorker, rest } from 'msw';
import {
render,
screen,
within,
fireEvent,
waitFor,
} from '#testing-library/react';
import '#testing-library/jest-dom';
import { Provider as AlertProvider } from 'react-alert';
import AlertMUITemplate from 'react-alert-template-mui';
import BatchList from './BatchList';
import mockedAxios from 'axios';
// ... other tests that succeed here ...
// only test that fails here
test('clicking yes and passing back only 2 valid batch numbers should show 2 valid, 2 invalid batch numbers in list', async () => {
// renderBatchListWithNumbers();
const batchList = render(
<AlertProvider template={AlertMUITemplate}>
<BatchList
formulaID={''}
orderID={''}
itemID={''}
formulaResults={[]}
batchNumbers={[
{ BATCH_ID: '987654', ID: '78' },
{ BATCH_ID: '261010', ID: '79' },
{ BATCH_ID: '301967', ID: '80' },
{ BATCH_ID: '445566', ID: '81' },
]}
setBatchNumbers={mockedEmptyFn}
batchNumber={'5'}
setBatchNumber={mockedEmptyFn}
/>
</AlertProvider>
);
const completeButton = batchList.getByText('Complete');
fireEvent.click(completeButton);
const yesButton = batchList.getByText('Yes');
expect(yesButton).toBeInTheDocument();
fireEvent.click(yesButton);
// now we need to figure out mocking the API calls!!!
const data = {
msg: 'fail',
invalidBatchNumbers: ['987654', '445566'],
};
mockedAxios.post.mockResolvedValueOnce({
data: data,
});
await waitFor(() => {
expect(batchList.getByText('987654')).toHaveClass('invalid');
expect(batchList.getByText('261010')).toHaveClass('valid');
});
});
And here is my axios.js inside my __mocks__ folder:
export default {
post: jest.fn().mockResolvedValue(),
};
So the error I am getting in my test is this: thrown: "Unable to complete all batches - TypeError: (0 , _axios.default) is not a function"
And that error message string is coming from my client side API call here:
export const completeAllBatches = async (
orderID,
itemID,
batchNumbers
) => {
const completeAllBatchesURL =
serverURL + `:${port}/completeAllBatches`;
try {
return await axios({
method: 'post',
url: completeAllBatchesURL,
timeout: shortTimeout,
data: {
orderID,
itemID,
batchNumbers,
},
})
.then((res) => {
return res;
})
.catch((e) => {
return Promise.reject(
'1: Unable to complete all batches - ' + e
);
});
} catch (e) {
return Promise.reject('2: Unable to complete all batches - ' + e);
}
};

I solved a similar problem, perhaps this is useful?
I was following this tutorial.
Why this break?
I think the problem is that typescript doesn't know how to handle some of the complexities of an axios.post.
Instead of importing default as mocked, I imported as normal
import axios from 'axios'
jest.mock('axios');
Then I am a bit more explicit with the mock
const mockedAxios = axios as jest.Mocked<typeof axios>;
let payload:object = {}
const mockedPost = mockedAxios.post.mockReturnValueOnce(payload);
//I highly recommend separating out api client like in the tutorial, then call that api function...
const data = await postBatch();
expect(axios.post).toHaveBeenCalled();
LMK if this works for you, I'm still playing with my React testing patterns :)

Related

Stripe AxiosError: Request failed with status code 500

I am trying for implement Stripe checkout in NextJS following this example.
But I always get this error:
checkout.js:
import React from "react";
import Header from "../components/Header";
import Image from "next/image";
import { selectItems, selectTotal } from "../slices/basketSlice";
import { useSelector } from "react-redux";
import CheckoutProduct from "../components/CheckoutProduct";
import Currency from "react-currency-formatter";
import { useSession } from "next-auth/react";
import {loadStripe} from '#stripe/stripe-js';
import axios from 'axios';
const stripePromise = loadStripe(process.env.stripe_public_key)
function Checkout() {
const items = useSelector(selectItems);
const total = useSelector(selectTotal);
const { data: session } = useSession();
const CreateCheckoutSession = async () => {
const stripe = await stripePromise
// create checkout session
const checkoutSession = await axios.post("/api/create-checkout-session", {
items : items,
email: session.user.email,
})
//redirect user to Stripe checkout
const result = await stripe.redirectToCheckout({
sessionId: checkoutSession.data.id
})
if (result.error) {
alert(result.error.message)
}
}
create-checkout-session.js (tried to do it the way on doc page but get same relult):
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY)
export default async (req, res) => {
const {
items,
email
} = req.body
// console.log(items)
//console.log(email)
// console.log(process.env.STRIPE_SECRET_KEY)
const transformedItems = items.map(item => ({
description: item.description,
quantity: 1,
price_data: {
currency: 'usd',
unit_amount: item.price * 100,
product_data: {
name: item.title,
images: [item.image]
},
},
}))
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'payment',
shipping_rates: ['shr_1MFhHmL1z8EG1fYglFH9EBP7'],
shipping_address_collection: {
allowed_countries: ['GB', 'US', 'CA']
},
line_items: transformedItems,
success_url: `${pocess.env.HOST}/success`,
cancel_url: `${process.env.HOST}/checkout`,
metadata: {
email,
images: JSON.stringify(items.map(item => item.image))
}
})
}
res.status(err.statusCode || 500).json(err.message);
res.status(200).json({
id: session.id
})
I seem to me following ths Stipe formating but just keep on getting this unhelpfull error. Does anybody have a clue what it means?
The error seems to be coming from your own server and not Stripe's API. In your create-checkout-session.js file, the scope of the async function seem to end prior to you running res.status(xxx).json(...).
In this case, session variable is out of the scope of your res.status(...).json(...) function. Additionally, I suspect this code is sending the 500 res.status(err.statusCode || 500).json(err.message); as it doesn't know what err variable is.
Can you try fixing the scope as well as defining the err variable and see if that makes a difference?
In case this helps someone.
It turns out the Stripe api version I was using had different formatting requirements, so that description needs to be inside product_data array.
I find Stripe documentation just so hard to navigate. I don't get why not just use a flat array for all items and why keep nesting - renesting the items? :(
The stripe-session-checkout.js images
Remove the shipping-rates and the payment-method-types
It worked for me

CyPress unable to mock API call

We are working on a react + typescript project & trying to implement unit testing using cypress.
I have a component called GettingStartedForm where two APIs are called on componentDidMount
Create API
Get API - is called once the creation is successful.
We are trying to mock both API calls but the Get API call is not getting mocked.
Here is the component,
componentDidMount() {
this.createMethod();
}
/* Create Method */
createMethod() {
const contentType = 'application/json';
const pathAPICreate = '/v1/create/';
postData(pathAPICreate, contentType, '')
.then((response:any)=> {
this.getData();
}).catch((error:any)=>{
...
});
}
/* Get step data */
async getData() {
const contentType = 'application/json';
const pathData = '/v1/data/';
try {
const dataResponse = await getCreatedData(
pathData,
contentType,
);
...
}
Here is my test,
import GettingStartedForm from '../GettingStartedForm';
import React from 'react';
import {mount} from '#cypress/react';
describe('Onboarding getting started form - step 1', () => {
beforeEach(() => {
cy.intercept('POST', '**/v1/create', {data: 'success'}).as('createConsultant');
cy.intercept('GET', '**/v1/data/', {fixture: 'gettingStartedStepResponse.json'}).as('stepData');
});
it('Should render all elements', () => {
mount(<GettingStartedForm
nextFormHandler={(e, formStatus) => false}
previousFormHandler={(e, formStatus) => false}
></GettingStartedForm>);
...
});
});
and console message here
Is there something we are missing here? Any help is highly appreciated.

How to mock axios in React with using axios.create function

I'm working on React project where I'm using axios for http requests. I have a separate file with axios configuration like below:
import axios from 'axios'
export default axios.create({
baseURL: " http://localhost:3001",
params: {
}
})
I'm using this in action thunk creators like below:
import streams from "../apis/streams";
export const fetchStreams = () => {
return async(dispatch: ThunkDispatch<void, State, Action>) => {
const response: AxiosResponse<Stream[]> = await streams.get<Stream[]>('/streams');
dispatch({type: ActionType.FETCH_STREAMS, payload: response.data});
}
}
First I created "src/__mocks__/axios.ts" file like:
const mockedAxios: any = jest.createMockFromModule('axios');
mockedAxios.create = jest.fn(() => mockedAxios);
export default mockedAxios;
then I wrote test like below:
import mockedAxios, {AxiosResponse} from "axios";
import streamsApi from '../apis/streams'
import expectedStreams from "../mocks/expectedStreams";
jest.mock('axios')
describe('fetchStreams action', () => {
it('Store is updated correctly', async () => {
const mockedResponse: AxiosResponse = {
data: expectedStreams,
status: 200,
statusText: 'OK',
headers: {},
config: {}
}
mockedAxios.get.mockImplementationOnce(() => {
Promise.resolve(mockedResponse);
})
const results = await streamsApi.get('/streams');
expect(results.data).toBe(mockedResponse.data);
});
});
Unfortunately I've received an error like this:
Why is that? How can I correctly create facke API response in this case?
I would be grateful for help.
Ok, I know what was wrong. I forget to add return before Promise like so:
mockedAxios.get.mockImplementationOnce(() => {
return Promise.resolve(mockedResponse);
})

How to reset or clear the useQuery mocks of ApolloProvider before each single test?

I am having in one describe two test functions. They both use a component with apollo mocks as this component uses useQuery. But the response from mocks for each test should be different. Now I am unable to reset or clear mocks which I propagate to the ApolloProvider. Has anyone had this kind of issue? How did you solve it?
import React from 'react'
import { render, createMocks, screen, cleanup } from 'test-utils'
import GET_DEVICES from 'src/screens/dashboard/queries/GET_DEVICES'
import Section from './Section'
describe('DashboardScreen', () => {
const result = {
data: {
numDevices: 1
}
}
const mocks = createMocks([{
query: GET_DEVICES,
result: result,
}])
afterEach(() => {
jest.clearAllMocks() <------ THIS IS NOT SOLVING THE ISSUE
})
it('should load Section with expected badges', async () => {
render(<Section />, { mocks })
const badge0 = screen.getByTestId('Badge-0')
expect(badge0).toBeInTheDocument()
const badge1 = screen.getByTestId('Badge-1')
expect(badge1).toBeInTheDocument()
})
it('should display the exclamation icon and 7 devices require attention', async () => {
const result_local = {
data: {
numDevices: 4,
}
}
const mocks_local = createMocks([
{
query: GET_DEVICES,
result: result_local,
}
])
render(<Section } />, { mocks: mocks_local })
const devicesWithActiveIssuesIcon = await screen.findByTestId('ExclamationIcon')
expect(devicesWithActiveIssuesIcon).toBeInTheDocument()
})
})
The createMocks is an import and looks like this:
const createMocks = mocks =>
mocks.reduce(
(requests, mock) => [
...requests,
{
request: {
query: mock.query,
variables: mock.variables
},
result: mock.result
}
],
[]
)
It will return an object as follows:
const mocks = [
{
request: {
query: GET_DEVICES,
variables: {}
},
result: {
data: {
numDevices: 1
},
},
},
]
the issue might be from that the ApolloProviders used to render this two components are sharing the same InMemoryCache instance. see the following link for details.
https://github.com/benawad/apollo-mocked-provider/issues/1#issuecomment-511593398
if this is the case you just need to create new instance of InMemoryCache for every test.

Typescript,testing Api calls in Redux actions, mocking class in Enzyme, Jest

I have an issue where I need to be mocking a class Api that is called within my redux actions, this class calls axios get, post etc... which need to be mocked. I have been following this tutorial explaining how to mock axios and this tutorial about how to mock a class but neither approaches appear to be working.
Now for some code... here's an example of the type of action I need to test.
export const getAlldata = (id: string) => {
return (dispatch: any) => {
dispatch(beginAjaxRequest(id, types.BEGIN_GET_DATA_AJAX));
return Api.get("/data/data").then((response: any) => {
dispatch(getDataSuccess(response.data, id))
}).catch((error) => {
dispatch(handleAjaxError(id, new Alert({ id: id, title: "Error getting data", message: error.toString(), timestamp: Date.now(), error: true })));
});
}
}
and the parts of Api this calls.
import axios from 'axios';
class Api {
static get(path: string) {
return axios({
method: 'get',
url: (global as any).apiDomain + path,
headers: {
Authorization: "Bearer " + (global as any).authentication.getToken(),
"Content-Type": "application/json"
}
});
}
}
export default Api;
Which I have tried to mock in src/mocks/Api (two underscores following and preceding mocks)
import * as Promise from 'bluebird';
import { getTestData } from '../models/_tests/TestData';
class Api {
static get(path: string) {
switch (path) {
case "/data/data":
return Promise.resolve({
data: getTestData(3)
});
default:
return {};
}
}
}
export default Api;
and setup in my setupTests.
import * as Enzyme from 'enzyme';
import Api from './__mocks__/Api';
const Adapter = require("enzyme-adapter-react-16");
(global as any).Api = Api;
Enzyme.configure({ adapter: new Adapter() });
and called in my actual test...
describe('thunk actions', () => {
var middleware = [thunk];
var mockStore = configureMockStore(middleware);
afterAll(() => {
cleanAll();
});
test('getAllData gets all data', (done: any) => {
var store = mockStore({});
jest.mock('../../api/Api'); // path to real Api
var id = generateGuid();
store.dispatch<any>((getAllData(id))).then(() => {
done();
});
});
});
So obviously this doesn't actually test anything, I'm just trying to get this working but I keep getting errors within the real Api instead of the mock. I have also tried mocking axios but I get the same error (can't getToken of undefined) so this doesn't seem to be replacing either axios or Api, can anyone see where I'm going wrong?
You know you're screwed when you post a question to stackoverflow and get 0 answers and 0 responses in over a week... Not ideal but I've found a workaround to override the Api class in my thunk actions, instead of importing the Api class into all my action files and calling it directly, I now only import it into the root of my project (App.tsx) and make it global as below (stripped down to it's bare minimum).
import * as React from 'react';
import Api from './api/Api';
export interface State {
}
export interface Props {
}
export class App extends React.Component<Props, State> {
state = {
};
componentWillMount = () => {
(global as any).Api = Api;
};
public render() {
return (
<div>
</div>
);
}
}
export default App;
...and then call Api on my thunk actions as below
export const getAlldata = (id: string) => {
return (dispatch: any) => {
dispatch(beginAjaxRequest(id, types.BEGIN_GET_DATA_AJAX));
return (global as any).Api.get("/data/data").then((response: any) => {
dispatch(getDataSuccess(response.data, id))
}).catch((error) => {
dispatch(handleAjaxError(id, new Alert({ id: id, title: "Error getting data", message: error.toString(), timestamp: Date.now(), error: true })));
});
}
}
Then simply override this is setupTests.ts
import * as Enzyme from 'enzyme';
import Api from './__mocks__/Api';
const Adapter = require("enzyme-adapter-react-16");
(global as any).Api = Api;
Enzyme.configure({ adapter: new Adapter() });
...and then there's no need for jest mocks, simply call the actions in your tests and test.
This method would also work outside of Node by replacing global with window. This does the job but is not ideal as I prefer not to use the global namespace, so if anyone knows a better way post away.

Resources