Change API response in the middle of a test - reactjs

I want to test a component that shows a loading screen and then when the API sends the data, it shows the data accordingly.
I know how to do this in different test suites, but I want to do it in the same test — the API might change additional data over time and I want to test the component going from one state to the other.
For context, I'm using useSWR, but I mock the entire hook that calls it (as seen below).
The file below explains it as well:
First, I import and mock the hook
Then, I render the component (with the default mocked value) and assert that the loading is there.
I change the response of this mock hook to something different
The second assertion group should pass but it doesn't. The mock isn't even called again (I console'd it).
import { useCustomHook } from "./useCustomHook"
jest.mock(
"useCustomHook",
() => ({
useCustomHook: jest.fn(() => ({
isLoading: true,
response: undefined,
})),
}),
)
const mockuseCustomHook = useCustomHook as jest.Mock
describe("Component", () => {
it('Should render properly', () => {
render(<Component />)
expect(screen.getByText('Loading')).toBeInTheDocument()
mockuseCustomHook.mockImplementationOnce(() => ({
isLoading: false,
response: { data: 'This is the data' },
}))
expect(screen.queryByText('Loading')).not.toBeInTheDocument()
expect(screen.getByText('This is the data')).toBeInTheDocument()
})
})

Related

RTL: Mock a custom React hook only for selected unit tests

I have a custom hook called useInitialSetup that returns a key-value dictionary object.
It's being consumed in App.tsx.
In App.test.tsx, I have a suite of unit tests written using Jest and React Testing Library.
Is it possible to mock useInitialSetup in only a selection of unit tests, but not all?
Here's what I have tried in App.test.tsx:
jest.mock('../../hooks/useInitialSetup', () => {
return jest.fn(() => ({
loading: mockLoading,
suspendedTrial: mockSuspendedTrial,
maxTrials: mockMaxTrials,
reachedMaxTrials: mockReachedMaxTrials,
}));
});
it('Unit test that uses a mocked version of useInitialSetup', () => {
render(<App/>)
...
})
it('Unit test that uses the real implementation of useInitialSetup', () => {
jest.dontMock('../../hooks/useInitialSetup')
render(<App/>)
...
})
Unit test #2 still uses the mocked version of useInitialSetup.
Here's what I ended up doing. This lets me mock the output of my useInitialSetup custom hook for this one selected unit test. Any subsequent unit tests will use the default implementation of this hook.
import * as useInitialSetup from '../../hooks/useInitialSetup';
it('Test', () => {
const spy = jest.spyOn(useInitialSetup, 'default');
spy.mockReturnValue({
loading: false,
reachedMaxTrials: true,
errorCheckingCanCreateInstance: false,
errorFetchingUser: false,
// #ts-ignore
suspendedTrial: mockSuspendedTrial,
resetChecks: () => {},
maxTrials: 1,
});
render(setupComponent());
expect(true).toBe(true) // or whatever assertion you want
spy.mockRestore();
})

Using Jest Mock API Calls to SetState

I have an API call which runs whenever a certain component mounts. If this API call is successful the response data is used to update the state of one of my React Hooks.
The issue I am having is either related to asynchronicity or a poorly formatted mock API call, but no matter what I try I cannot get this test to work.
Here is a simplified version of the API:
const getOrg =() => {
axios.get(URL, config)
.then(response => response.data)
.then(data => {
setOrgTitle(data.name)
}
}
Basically the API is triggered and my setOrgTitle hook is updated from the response.
const [orgTitle, setOrgTitle] = useState("");
Now in my return statement I am displaying the value of orgTitle:
<h1 className={styles.titleText} id="document-folders-h1">
{orgTitle} Folders
</h1>
Alright, so the component is pretty simple. When I am trying to test things my two ideas were to either set the initial orgTitle hook state in my test or to mock the API. After some research I decided mocking the API was the way to go.
So I have a mockAxios component:
const mockAxios = {
get: jest.fn(() => Promise.resolve({ data: {} }))
};
module.exports = mockAxios;
And my test file:
import mockAxios from "../../mockTests/DocumentFoldersMock";
it("fetches results for getAdminOrg", () => {
axios.get.mockImplementation(() =>
Promise.resolve({ data: { name: "GETtest" } })
);
const wrapper = mount(
<AppProviders>
<DocumentFolders />
</AppProviders>
);
const orgTitle = wrapper.find("#document-folders-h1");
expect(orgTitle.text()).toContain("GETtest Folders");
});
I am mocking the response data, however I am not sure how to run the setOrgTitle function which is called in the .then of my actual axios call. How can I do this from my mock axios call using my mock response?
The result of the Jest test says expected("GETtest Folders") received(" Folders") so I am pretty sure that I am either having an issue with asynchronicity or an issue calling the hook update.

Is there a way to test a mocked function then the actual in the same test file on Jest?

In the same test file, I need to test the actual redux action creator and then, later, if the same action creator was called from another function.
If I jest.mock I'm unable to test the actual. If I don't, I can't mock it later.
I've also tried jest.spyOn with mockImplementation but it doesn't seem to mock it.
// actionsOne.js
export const myActionOne = ({
type: 'MY_ACTION_ONE'
})
// test.js
import { dispatch } from './reduxStore.js'
import { myActionOne } from './actionsOne.js'
import { myActionTwo } from './actionsTwo.js'
jest.mock('./actionsOne.js', () => ({
myActionOne: jest.fn()
}))
test('expectation 1', () => {
// would pass if no mocking is in place. fail otherwise.
expect(dispatch(myActionOne())).toEqual({
type: 'MY_ACTION_ONE'
})
})
test('expectation 2', () => {
dispatch(myActionTwo()) // say this would also dispatch myActionOne somewhere
// would pass if mocking is in place. fail otherwise.
expect(myActionOne).toHaveBeenCalled()
})
Because myActionOne does a lot of stuff when dispatched, I wanted to test it once and for myActionTwo just check if it was called.
Any recommendation? Thanks!
Ok, I got it to work using jest.requireActual():
test('expectation 1', () => {
const { myActionOne: myActionOneActual } = jest.requireActual('./actionsOne.js')
expect(dispatch(myActionOneActual())).toEqual({
type: 'MY_ACTION_ONE'
})
})
expectation 1 passes because we are asserting with the actual function.
No changes to expectation 2, since it asserts with the mocked function.

Wrapping async moxios call in act callback

I am trying to test a react functional component using hooks. The useEffect hook makes a call to a third part API which then calls setState on return.
I have the test working but keep getting a warning that an update to the component was not wrapped in act.
The problem I have is that the expectation is inside a moxios.wait promise and therefore I cannot wrap that in an act function and then assert on the result of that.
The test passes but I know not wrapping code that updates state in an act function could lead to false positives or uncovered bugs. I'm just wondering how I should be testing this.
I've tried using the new async await act function in the react 16.9.0 alpha release as well as numerous suggestions I've found in many github issues like jest setTimers and none seem to solve the issue.
The component
const Benefits = props => {
const [benefits, setBenefits] = useState([])
const [editing, setEditing] = useState(false)
const [editingBenefit, setEditingBenefit] = useState({id: null, name: '', category: ''})
useEffect(() => {
axios.get('#someurl')
.then(response => {
setBenefits(response.data)
})
}, [])
}
The test
describe('Benefits', () => {
it('fetches the list of benefits from an api and populates the benefits table', (done) => {
const { rerender } = render(<Benefits />)
moxios.wait(() => {
const request = moxios.requests.mostRecent()
request.respondWith({
status: 200,
response: benefits
}).then(() => {
expect(document.querySelectorAll('tbody > tr').length).toBe(2)
done()
})
})
})
})
The test passes but I get the following warning
Warning: An update to Benefits inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser.
in Benefits (at benefits.spec.js:28)
from react 16.9.0 you can use async/await act
Your code should look like this
describe('Benefits', () => {
it('fetches the list of benefits from an api and populates the benefits table', async() => {
const { rerender } = render(<Benefits />);
await moxios.wait(jest.fn);
await act(async() => {
const request = moxios.requests.mostRecent()
await request.respondWith({
status: 200,
response: benefits
});
});
expect(document.querySelectorAll('tbody > tr').length).toBe(2)
})
I use jest.fn in moxios.wait because it needs callback function

Return value of a mocked function does not have `then` property

I have the following async call in one of my React components:
onSubmit = (data) => {
this.props.startAddPost(data)
.then(() => {
this.props.history.push('/');
});
};
The goal here is to redirect the user to the index page only once the post has been persisted in Redux (startAddPost is an async action generator that sends the data to an external API using axios and dispatches another action that will save the new post in Redux store; the whole thing is returned, so that I can chain a then call to it in the component itself). It works in the app just fine, but I'm having trouble testing it.
import React from 'react';
import { shallow } from 'enzyme';
import { AddPost } from '../../components/AddPost';
import posts from '../fixtures/posts';
let startAddPost, history, wrapper;
beforeEach(() => {
startAddPost = jest.fn();
history = { push: jest.fn() };
wrapper = shallow(<AddPost startAddPost={startAddPost} history={history} />);
});
test('handles the onSubmit call correctly', () => {
wrapper.find('PostForm').prop('onSubmit')(posts[0]);
expect(startAddPost).toHaveBeenLastCalledWith(posts[0]);
expect(history.push).toHaveBeenLastCalledWith('/');
});
So I obviously need this test to pass, but it fails with the following output:
● handles the onSubmit call correctly
TypeError: Cannot read property 'then' of undefined
at AddPost._this.onSubmit (src/components/AddPost.js:9:37)
at Object.<anonymous> (src/tests/components/AddPost.test.js:25:46)
at process._tickCallback (internal/process/next_tick.js:109:7)
So how can I fix this? I suspect this is a problem with the test itself because everything works well in the actual app. Thank you!
Your code is not testable in the first place. You pass in a callback to the action and execute it after saving the data to the database like so,
export function createPost(values, callback) {
const request = axios.post('http://localhost:8080/api/posts', values)
.then(() => callback());
return {
type: CREATE_POST,
payload: request
};
}
The callback should be responsible for the above redirection in this case. The client code which uses the action should be like this.
onSubmit(values) {
this.props.createPost(values, () => {
this.props.history.push('/');
});
}
This makes your action much more flexible and reusable too.
Then when you test it, you can pass a stub to the action, and verify whether it is called once. Writing a quality, testable code is an art though.
The problem with your code is that the startAddPost function is a mock function which does not return a Promise, but your actual this.props.startAddPost function does return a Promise.
That's why your code works but fails when you try to test it, leading to the cannot read property.... error.
To fix this make your mocked function return a Promise like so -
beforeEach(() => {
startAddPost = jest.fn().mockReturnValueOnce(Promise.resolve())
...
});
Read more about mockReturnValueOnce here.

Resources