I am developing an application in react and for the first time I decided to implement tests. I started with the simpler components, which I still had trouble with, but now I would like to test more complex components, especially with API calls via axios.
My app looks like this:
import * as React from "react";
import Axios from "axios";
import { AnyData } from "../../interface/AnyDataInterface";
interface IProps {
}
interface IState {
loading: boolean | undefined,
myData: AnyData | undefined
}
export default class Home extends React.Component <IProps, IState> {
constructor (props: IProps){
super(props);
this.state = {
loading: true,
myData: {
field: undefined,
...
}
};
}
fetchData = ():void => {
this.setState({ loading: true });
Axios.get<AnyData>('my-url')
.then(res => {
if (res.status === 200 && res != null) {
this.setState({ myData:{... res.data} });
this.setState({ loading: false });
} else {
console.log('problem when fetching the logs from backend');
}
})
.catch(error => {
console.log(error);
});
}
render():JSX.Element {
return(
<div className="container">
<div>
<button role="button" onClick={this.fetchData}>Search</button>
</div>
{this.state.loading == true ? <p>Wait</p>:
<div role="composite">
{ Some child component that will render data }
</div>
}
</div>
);
}
}
My test looks like this:
import axios from 'axios';
import React from 'react';
import { render, screen, fireEvent, waitFor } from '#testing-library/react';
import Home from './Home';
jest.mock('axios');
describe('Home component', () => {
test('it can be clicked', async () => {
const fakeData = [{
some_field: some_data,
...
}];
axios.get = jest.fn().mockResolvedValue(() =>
Promise.resolve({data: fakeData}));
render(<Home />);
fireEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(axios.get).toBeCalledWith("my-url");
});
await waitFor(() => {
//Both expect should pass, if the title is there the data should also appear
expect(screen.getByText('some title text')).toBeInTheDocument(); //Pass without error
expect(screen.getByText('the data from fakeData that should be there')).toBeInTheDocument(); //Send me an error because jest is unable to find some_data
});
});
});
When I wrote this test it failed every time but the problem was not in my test, the problem was in my data fetch function. In the .then I had a condition which was the following: if (res.status === 200 && res != null) { and the res.status === 200 was a condition never satisfied.
So I removed this condition and the call goes through normally. But I had to change a part of my code that is normally not problematic, is there a way to mock the status of the response too ? So that I can put this condition back in my function.
I have another problem that appears now, in my test my component displays well the information "after click" but each field that appears has its corresponding data empty, the data seems not to pass although the .then passes and does not send me to the .catch.
What seems strange to me is that by removing the condition res.status === 200 in my data fetch function and leaving the condition res != null, the test still passes (with the same empty data problem). Finally, by adding a console.log("data: ", res); in my .then in my fetch data function of my component I get, when the test is executed, the following log: data: [Function (anonymous)].
I don't know if this could explain this behavior but my test is in javascript while my classes and functions are in typescript. But the data I fill in my test has the right form (the same as the type requested in my class) so typescript should see that the type matches and have no particular problem
If anyone knows how to answer any of these questions it would really help me, I've been stalling on this test for a while and it's starting to drive me crazy.
EDIT:
I just realized that in fetchData() in my component I am trying to access res.data but the fake data I am sending in my test is in the form fakeData = [{...}]. Could this be the reason why my object is empty?
I tried to change this fake data in my test to: fakeData = [{ data: { ...}}] but I still have the same problem of empty response when calling the get method of axios.
EDIT:
I've try to change test file to a .tsx file and change my "fakeData" in my test to:
jest.mock('axios');
describe('Home component', () => {
test('it can be clicked', async () => {
const fakeData: AnyData = {
"some_field": some_data,
...
};
const fakeConfig: AxiosRequestConfig<any> = {
"maxBodyLength": -1
};
const fakeHeaders: AxiosResponseHeaders= {
"access-control-allow-credentials": "true",
"access-control-allow-headers": "*",
"access-control-allow-methods": "*",
"access-control-allow-origin": "some-url",
"access-control-expose-headers": "scrollId",=
"content-type": "application/json; charset=utf-8",
....
"x-powered-by": "Express"
}
const res: AxiosResponse<AnyData, any> = {
config: fakeConfig,
data: fakeData,
headers: fakeHeaders,
status: 200,
statusText: "OK",
};
axios.get = jest.fn().mockResolvedValue(() =>
Promise.resolve({res: res}));
render(<Home />);
fireEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(axios.get).toBeCalledWith("my-url");
});
...
});
});
But even with this method it still doesn't work, yet I really have the impression that the problem comes from the formatting of the data I am sending and I don't see how my data differs from the one requested in fetchData(). As for the fields in my data, I'm sure they're good, so I guess the problem is with the reading of res.data.
When I make the new test by putting a console.log of myData in fetchData() I get the following data: { some_field: undefined, ... }
Thanks in advance if you take the time to help me.
I finally realized my mistake on my test. It was in the rewriting of the axios.get method.
The code that works is as follows:
jest.mock('axios');
describe('Home component', () => {
test('it can be clicked', async () => {
const fakeData: AnyData = {
"some_field": some_data,
...
};
const fakeConfig: AxiosRequestConfig<any> = {
"maxBodyLength": -1
};
const fakeHeaders: AxiosResponseHeaders= {
"access-control-allow-credentials": "true",
"access-control-allow-headers": "*",
"access-control-allow-methods": "*",
"access-control-allow-origin": "some-url",
"access-control-expose-headers": "scrollId",=
"content-type": "application/json; charset=utf-8",
....
"x-powered-by": "Express"
}
const res: AxiosResponse<AnyData, any> = {
config: fakeConfig,
data: fakeData,
headers: fakeHeaders,
status: 200,
statusText: "OK",
};
(axios.get as jest.Mock).mockResolvedValue(res); //THE LINE THAT CHANGE EVERYTHING
render(<Home />);
fireEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(axios.get).toBeCalledWith("my-url");
});
...
});
});
Related
I want to know if there is a way to create a kind of middleware in React?
What i want is to have an alert component show if there is a failing result for an http request.
Right now, i am making http request on login,registration,etc and i am importing my alert component in every page and setting the Alert component props like type, message, visibility everywhere i need the component, but i think maybe there is a better way of doing this.
Here is my code:
...imports
export const RegisterPage = () => {
const [alertConfig, setAlertConfig] = useState({
type: "",
message: "",
show: false,
});
...code
const onSubmitHandler = async (e) => {
e.preventDefault();
if (!isFormValid()) return;
const formData = new FormData();
formData.append("password", formValues.password);
if (formValues.provider.startsWith("8")) {
formData.append("contact", formValues.provider);
} else {
formData.append("email", formValues.provider);
}
setIsLoading(true);
try {
const response = await fetch(
`${process.env.REACT_APP_API_URL}/auth/register`,
{
method: "POST",
body: formData,
}
);
const data = await response.json();
if (data.status === "success") {
const { token, user } = data.data;
dispatch(setCurrentUser(user, token));
navigate("/choose-actor");
} else {
setAlertConfig({
type: "warning",
message: data.message,
show: true,
});
}
} catch (error) {
console.log(error);
setAlertConfig({
type: "danger",
message: "Ocorreu algum erro",
show: true,
});
} finally {
setIsLoading(false);
}
};
return
...html
{alertConfig.show && <Alert {...alertConfig} />}
...more html
As you can see, i am changing the configuration for the alert inside inside the function that executes the http request, and i have to do the save for every page that performs this action.
I looking for a design patter where i dont have to repeat myself.
Hope my question is clear.
I've seen some similar posts about mocking axios but I have spend some hours and I didn't manage to solve my problem and make my test work. I've tried solutions that I have found but they didn't work.
I'm writing small app using React, Typescript, react-query, axios. I write tests with React Testing Library, Jest, Mock Service Worker.
To test delete element functionality I wanted just to mock axios delete function and check if it was called with correct parameter.
Here is the PROBLEM:
I'm using axios instance:
//api.ts
const axiosInstance = axios.create({
baseURL: url,
timeout: 1000,
headers: {
Authorization: `Bearer ${process.env.REACT_APP_AIRTABLE_API_KEY}`,
},
//api.ts
export const deleteRecipe = async (
recipeId: string
): Promise<ApiDeleteRecipeReturnValue> => {
try {
const res = await axiosInstance.delete(`/recipes/${recipeId}`);
return res.data;
} catch (err) {
throw new Error(err.message);
}
};
});
//RecipeItem.test.tsx
import axios, { AxiosInstance } from 'axios';
jest.mock('axios', () => {
const mockAxios = jest.createMockFromModule<AxiosInstance>('axios');
return {
...jest.requireActual('axios'),
create: jest.fn(() => mockAxios),
delete: jest.fn(),
};
});
test('delete card after clicking delete button ', async () => {
jest
.spyOn(axios, 'delete')
.mockImplementation(
jest.fn(() =>
Promise.resolve({ data: { deleted: 'true', id: `${recipeData.id}` } })
)
);
render(
<WrappedRecipeItem recipe={recipeData.fields} recipeId={recipeData.id} />
);
const deleteBtn = screen.getByRole('button', { name: /delete/i });
user.click(deleteBtn);
await waitFor(() => {
expect(axios.delete).toBeCalledWith(getUrl(`/recipes/${recipeData.id}`));
});
});
In test I get error "Error: Cannot read property 'data' of undefined"
However if I would not use axios instance and have code like below, the test would work.
//api.ts
const res = await axios.delete(`/recipes/${recipeId}`);
I'm pretty lost and stuck. I've tried a lot of things and some answers on similar problem that I've found on stackoverflow, but they didn't work for me. Anybody can help?
I don't want to mock axios module in mocks, only in specific test file.
I don't have also experience in Typescript and testing. This project I'm writing is to learn.
I found some workaround and at least it's working. I moved axiosInstance declaration to a separate module and then I mocked this module and delete function.
//RecipeItem.test.tsx
jest.mock('axiosInstance', () => ({
delete: jest.fn(),
}));
test('delete card after clicking delete button and user confirmation', async () => {
jest
.spyOn(axiosInstance, 'delete')
.mockImplementation(
jest.fn(() =>
Promise.resolve({ data: { deleted: 'true', id: `${recipeData.id}` } })
)
);
render(
<WrappedRecipeItem recipe={recipeData.fields} recipeId={recipeData.id} />
);
const deleteBtn = screen.getByRole('button', { name: /delete/i });
user.click(deleteBtn);
await waitFor(() => {
expect(axiosInstance.delete).toBeCalledWith(`/recipes/${recipeData.id}`);
});
});
If you have a better solution I would like to see it.
When I leave this code as is, I will get the correct console.log (commented with "these appear correct") that I'm looking for. However when I replace the api_url with http://localhost:9000/ipdata/${this.state.inputValue} the console.log is blank. This is why I think I'm either passing the input value wrong or I'm adding it to the state wrong.
I would assume I'm adding it to the state wrong as the spans that I'm trying to render in order to output the data on the client aren't displaying anything either.
Heres my code ...
import React from 'react';
import './App.css';
class App extends React.Component {
constructor(props) {
super(props);
this.state = { apiResponse: '', inputValue: '', result: {} };
}
async callAPI() {
try {
console.log('called API...');
const api_url = `http://localhost:9000/ipdata/8.8.8.8`;
const res = await fetch(api_url, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
const result = await res.json();
// these appear correct
console.log(result.city);
console.log(result.region_code);
console.log(result.zip);
this.setState({ result });
} catch (error) {
// handle errors
}
}
render() {
return (
<div className="App">
<h1>IP Search</h1>
<input
type="text"
value={this.state.inputValue}
onChange={(e) => this.setState({ inputValue: e.target.value })}
/>
<button onClick={this.callAPI}>Search IP</button>
<p>
<span>{this.state.result.city}</span>
<span>{this.state.result.region_code}</span>
<span>{this.state.result.zip}</span>
</p>
</div>
);
}
}
export default App;
API call on the Node server...
const fetch = require('node-fetch');
app.get('/ipdata/:ipaddress', async (req, res, next) => {
console.log(req.params);
const ipaddress = req.params.ipaddress;
console.log(ipaddress);
const api_url = `http://api.ipstack.com/${ipaddress}?access_key=API_KEY`;
const response = await fetch(api_url);
const json = await response.json();
res.json(json);
});
The problem is not the way you set state, but the way you access it, because callAPI doesn't have access to this, so you get an error thrown inside the function and as you don't handle errors, it gets swollen. To make it work you either bind the function
onClick={this.callAPI.bind(this)}
or use arrow function instead
callAPI = async ()=> {
I get the following error when running my tests : TypeError: Cannot read property 'title' of null. However it should not be null but an empty string.
If I start the application I do not get this error. Only when running the tests written in jest typescript and react testing library.
The application:
When clicked on the button, it will call the following api : https://jsonplaceholder.typicode.com/posts/1. Then it shows the title in the react app. That's it.
MultipleFetches.tsx
import * as React from 'react';
import {CallAPI} from '../API/useAPI';
interface State{
postResponse :{
data:{
userId: number,
id: number,
title: string,
body:string
},
success : boolean,
error: null
}
}
interface PostResponse{
}
class MultipleFetches extends React.Component{
state : State = {
postResponse:{
data: {
userId:0,
id:0,
title : "",
body : ""
},
success: false,
error : null
}
}
componentDidMount(){
}
handleOnClick =() =>{
CallAPI("https://jsonplaceholder.typicode.com/posts/1").then(res =>{
this.setState({
postResponse : res
})
})
}
render() : React.ReactNode{
return(
<div>
<h1>Fetch API Multiple Times</h1>
<button data-testid="post-click" onClick={this.handleOnClick}>Click to fetch data</button>
<p><b>Post Title</b></p>
<p data-testid="post-title">{this.state.postResponse.data.title}</p>
</div>
)
}
}
export default MultipleFetches;
MultipleFetches.test.tsx
import React from 'react';
import { render, fireEvent, waitForElement, getByTestId, wait, waitFor } from "#testing-library/react";
import MultipleFetches from "../components/MultipleFetches";
describe("<MultipleFetches/>", () =>{
test("post shows correct title after button click", async () =>{
const utils = render(<MultipleFetches/>)
const data ={
userId: 1,
id: 1,
title: "Mock Title",
body: "Mocked Body"
}
// I don't even know what this does??
global.fetch = jest.fn().mockImplementation(() => data);
jest.spyOn(global,'fetch')
.mockImplementation(() => Promise.resolve({
json: () => Promise.resolve(data)
} as Response))
const button = utils.getByTestId("post-click");
fireEvent.click(button);
const newPostTitle = await waitFor(() => utils.getByTestId("post-title"))
expect(newPostTitle.textContent).toBe("Mock Title");
})
})
useAPI.ts
const returnResponse ={
data: null,
success: false,
error: null
}
export async function CallAPI(url : string){
try{
const response = await fetch(url);
if ( response.status < 200 || response.status >= 300) throw new Error("Failed to fetch");
if(response.status === 200){
const json = await response.json();
returnResponse.data = json;
returnResponse.success = true
}
}
catch(e){
returnResponse.error = e.message
}
return returnResponse;
}
Looks like you missed to set status as part of response while your api code is now checking that. The right mock is supposed to be:
jest.spyOn(global,'fetch')
.mockImplementation(() => Promise.resolve({
status: 200, // add your status here
json: () => Promise.resolve(data)
} as Response))
After some changes on an existing component, I am having trouble in the jest tests.
Basically, I added a call on a componentDidMount function to a function that does a "fetch" internally and now I am getting an error when running jest tests
fetch is being called in the utils/index.ts and this one is being called from the MyComponent.tsx
componentDidMount() {
this.props.actions.requestLoadA();
this.props.actions.requestLoadB();
// Problematic call HERE
this.getXPTOStatuses("something");
}
getXPTOStatuses = (something: string) => {
HttpUtility.get(`/api/getXPTOStatuses?param=${something}`)
.then(response => handleFetchErrors(response))
.then(data => {
// ....
})
.catch(error => {
// show the error message to the user -> `Could not load participant statuses. Error: '${error.message}'`
});
}
and the get(...)
public static get = (url: string) => fetch(url, { method: "GET", headers: { Accept: "application/json" }, credentials: "same-origin" });
and the jest test in the cause:
MyContainer.test.tsx
describe("Risk Import Container Tests", () => {
let props: MyContainerProps;
beforeEach(() => {
props = initialProps;
});
it("Matches the snapshot", () => {
const props = initialProps;
const tree = shallow(<MyContainer {...props} />);
expect(tree).toMatchSnapshot();
});
});
By default, Enzymes's shallow function calls componentDidMount. So, this would naturally call through to where fetch is used.
You have 2 options, mock fetch somehow.
Or use the shallow option disableLifecycleMethods to disable the call to componentDidMount.