I feel like I am missing something when it comes to testing React components with async fetch operations.
I have a following component like this...
export default function Page() {
const [result, setResult] = useState();
async function initialize() {
const response = api.fetchData();
setResult(response);
}
useEffect(() => {
initialize();
}, []);
return (isLoading ? (
<div>Fetching Data...</div>
) : (
<div className="page-result-name">{result.name}</div>
)
}
I want to create the following test.
test('Page rendering test.', async () => {
jest.spyOn(api, 'fetchData').mockResolvedValue({ name: 'User 1'});
const pageRendering = renderer.create(<Page />);
// Wait? How to wait?
// I have tried the following... as well as other method.
// await waitFor(() => {
// const element = document.getElementByClassName('page-result-name')[0] as
// HTMLElement;
// expect(element.innerHTML.includes('User 1')).toBeTruthy();
// });
expect(pageRendering.toJSON()).toMatchSnapshot();
});
The issue I am running into is that the data has not been returned by the time the snapshot is taken.
Regardless of the approach I run into issues with warnings like the following
Warning: It looks like you're using the wrong act() around your test interactions
Or it always displays Fetching Data...
I have used render from #testing-library/react and the waitFor and things work well. I don't know how to generate a snapshot for that.
Any help here would be appreciated!!
Cheers!!
For async operations the tricky is waiting for loading component to be removed from the screen, this way you assure that the your component is completed loaded and then you can expect the snapshot:
test('Page rendering test.', async () => {
jest.spyOn(api, 'fetchData').mockResolvedValue({ name: 'User 1'});
const pageRendering = renderer.create(<Page />);
// Wait? How to wait?
// wait for your data to be loaded before you make your assertion.
await waitForElementToBeRemoved(screen.getByText('Fetching Data...'));
expect(pageRendering.toJSON()).toMatchSnapshot();
});
Related
I would like to be able to mock hooks on Sequelize objects but nothing seems to work for me.
In this particular instance, I want to mock afterSave
// models/user.js
class User extends Model {
...
{
hooks: {
afterSave: async (user) => {
return notifyAdmin(message, user)
}
}
}
}
// service.js
module.exports.create = async data => {
await user.create(data)
return 'yay'
}
// user.test.js
const user = require('models/user')
it('test', async () => {
const mockSave = jest.spyOn(user.prototype, 'save')
const mockHook = jest.spyOn(user.prototype, 'afterSave')
await service.create({ first: 'han', last: 'solo' })
expect(u.id).toBeDefined()
expect(mockHook).toBeCalled()
})
This is a very contrived example but it does showcase what I want to do. That is to mock the afterSave hook; this doesn't work and throws an exception.
Reason for trying to do this is because most of our unit tests don't care if the notif functions are called (that gets tested somewhere else) but get bogged down by a hook.
One test I have goes from 20ms to 4s when the hook is enabled.
Hopefully, this question made some sense and someone can let me know what I'm doing wrong.
I am using jest & react-testing-library.
In my component on didMount I add an event listener to the window reacting on resize events. Based on resize events, another function is called. In my test, that function is mocked.
Now I have the problem, that I am not able to trigger these resize events.
Is there any way to get that done?
window.resizeTo(500, 500);
fireEvent.resize(window);
fireEvent(window, new Event("resize"));
I tried to achieve the triggering of the event listener on different ways, but nothing worked.
Thanks for your help in advance :)
Here's an example of how to spy on window.addEventListener to make sure that it's been invoked (and your mock function is registered) before you dispatch the resize event:
The example component is a functional one and uses the effect hook, but the test should be the same for a class component.
TS Playground
example.test.tsx:
import {useEffect} from 'react';
import {fireEvent, render, waitFor} from '#testing-library/react';
type ComponentProps = { callback: () => unknown };
function Component ({callback}: ComponentProps) {
useEffect(() => {
const handleResize = () => {
// You described some extra logic here,
// but this is where the callback is invoked:
callback();
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [callback]);
return null;
}
test('callback is invoked after window resize event', async () => {
let aResizeEventListenerWasAddedToWindow = false;
const originalMethod = window.addEventListener;
const spy = jest.spyOn(window, 'addEventListener');
spy.mockImplementation((...args) => {
// Because this custom implementation is created for the spy,
// Jest will no longer automatically invoke the original.
// It needs to be done manually:
originalMethod(...args);
const [eventType] = args;
if (eventType === 'resize') aResizeEventListenerWasAddedToWindow = true;
});
const callback = jest.fn();
render(<Component {...{callback}} />);
// Wait for the resize handler in the component to be registered (useEffect callback is async)
await waitFor(() => expect(aResizeEventListenerWasAddedToWindow).toBeTruthy());
fireEvent.resize(window);
expect(callback).toHaveBeenCalled();
// Restore the original method to window.addEventListener
spy.mockRestore();
});
I'm trying to test a component that loads data asynchronously when mounted. The component works as expected, it's just the test that's giving me issues. The component's async loadData() function hangs at await axios.get() while jest test runner is in the component.vm.$nextTick(). As a result, the checks in the $nextTick loop never pass.
Immediately after the $nextTick loop times out, the component's await statement completes and the component renders itself. axios is mocked, so it should resolve really fast. If I remove the await and just fill in a constant instead, the entire thing executes as expected.
I'm guessing that $nextTick loop is not asynchronous and it's consuming the thread, even though this is the recommended way of testing asynchronous stuff. The problem is, I don't have an onclick async handler to await: this method is called from onMount.
Unfortunately, I don't know how to make a jsFiddle of this one, so I hope this will be enough:
my component (the relevant parts)
export default {
data() { return { content: '' }; },
mounted() { this.loadDoc() }
methods: {
async loadDoc() {
const res = await axios.get('some url'); // <-- this is the line that hangs until timeout
// const res = { data: 'test data'}; // this would test just fine
this.content = res.data;
}
}
}
and my component.spec.js:
jest.mock('axios', () => ({
get: async (url) => {
return { data: 'test data' };
}
};
describe('my super test', () => {
it('renders', (done) => {
const doc = shallowMount(myComponent);
doc.vm.$nextTick(() => {
expect(doc.html()).toContain('test data'); // <-- this never matches
done();
});
});
});
I would delete, but I just spent quite some hours for something that was suggested in the docs, but not explained that it's the only way... I'm hoping somebody else finds this useful.
Using flush-promises package instead of $nextTick loop immediately "fixed" the problem
Code sample (rework of above):
describe('my super test', () => {
it('renders', async() => {
const doc = shallowMount(myComponent);
await flushPromises();
expect(doc.html()).toContain('test data'); // <-- now it works
});
});
I've got a very basic component that uses axios.all to make 3 calls to a jokes api and then stores the values in state. I then map those values on the page. Very basic stuff.
I'm trying to write a test that mocks the axios.all that I can pass some hard coded responses to. I want to prove that the data binding is happening correctly after the call has resolved.
I'm having a very hard time doing this, and I was wondering if anyone had any insights.
Link to CodeSandbox
Thanks in advance for any and all help!
The problem with your test is that you are not mocking the axios methods. Simply calling it axiosMock when you import the library is not how it works.
You have to actually mock the methods you use:
describe("<TestScreen />", () => {
afterEach(() => {
cleanup();
jest.clearAllMocks();
});
beforeEach(() => {
// Mock all the calls to axios.get, each one returning a response.
axios.get = jest
.fn()
.mockResolvedValueOnce(RESPONSES[0])
.mockResolvedValueOnce(RESPONSES[1])
.mockResolvedValueOnce(RESPONSES[2]);
// Spy on spread method so that we can wait for it to be called.
jest.spyOn(axios, "spread");
});
test("placeholder", async () => {
const { queryAllByTestId } = render(<App />);
await waitFor(() => expect(axios.spread).toHaveBeenCalledTimes(1));
const setups = queryAllByTestId("setup");
const punchlines = queryAllByTestId("punchline");
expect(setups.length).toBe(3);
expect(punchlines.length).toBe(3);
});
});
I use Supertest to test my Express apps, but I'm running into a challenge when I want my handlers to do asynchronous processing after a request is sent. Take this code, for example:
const request = require('supertest');
const express = require('express');
const app = express();
app.get('/user', async (req, res) => {
res.status(200).json({ success: true });
await someAsyncTaskThatHappensAfterTheResponse();
});
describe('A Simple Test', () => {
it('should get a valid response', () => {
return request(app)
.get('/user')
.expect(200)
.then(response => {
// Test stuff here.
});
});
});
If the someAsyncTaskThatHappensAfterTheResponse() call throws an error, then the test here is subject to a race condition where it may or may not failed based on that error. Even aside from error handling, it's also difficult to check for side effects if they happen after the response is set. Imagine that you wanted to trigger database updates after sending a response. You wouldn't be able to tell from your test when you should expect that the updates have completely. Is there any way to use Supertest to wait until the handler function has finished executing?
This can not be done easily because supertest acts like a client and you do not have access to the actual req/res objects in express (see https://stackoverflow.com/a/26811414/387094).
As a complete hacky workaround, here is what worked for me.
Create a file which house a callback/promise. For instance, my file test-hack.js looks like so:
let callback = null
export const callbackPromise = () => new Promise((resolve) => {
callback = resolve
})
export default function callWhenComplete () {
if (callback) callback('hack complete')
}
When all processing is complete, call the callback callWhenComplete function. For instance, my middleware looks like so.
import callWhenComplete from './test-hack'
export default function middlewareIpnMyo () {
return async function route (req, res, next) {
res.status(200)
res.send()
// async logic logic
callWhenComplete()
}
}
And finally in your test, await for the callbackPromise like so:
import { callbackPromise } from 'test-hack'
describe('POST /someHack', () => {
it.only('should handle a post request', async () => {
const response = await request
.post('/someHack')
.send({soMuch: 'hackery'})
.expect(200)
const result = await callbackPromise()
// anything below this is executed after callWhenComplete() is
// executed from the route
})
})
Inspired by #travis-stevens, here is a slightly different solution that uses setInterval so you can be sure the promise is set up before you make your supertest call. This also allows tracking requests by id in case you want to use the library for many tests without collisions.
const backgroundResult = {};
export function backgroundListener(id, ms = 1000) {
backgroundResult[id] = false;
return new Promise(resolve => {
// set up interval
const interval = setInterval(isComplete, ms);
// completion logic
function isComplete() {
if (false !== backgroundResult[id]) {
resolve(backgroundResult[id]);
delete backgroundResult[id];
clearInterval(interval);
}
}
});
}
export function backgroundComplete(id, result = true) {
if (id in backgroundResult) {
backgroundResult[id] = result;
}
}
Make a call to get the listener promise BEFORE your supertest.request() call (in this case, using agent).
it('should respond with a 200 but background error for failed async', async function() {
const agent = supertest.agent(app);
const trackingId = 'jds934894d34kdkd';
const bgListener = background.backgroundListener(trackingId);
// post something but include tracking id
await agent
.post('/v1/user')
.field('testTrackingId', trackingId)
.field('name', 'Bob Smith')
.expect(200);
// execute the promise which waits for the completion function to run
const backgroundError = await bgListener;
// should have received an error
assert.equal(backgroundError instanceof Error, true);
});
Your controller should expect the tracking id and pass it to the complete function at the end of controller backgrounded processing. Passing an error as the second value is one way to check the result later, but you can just pass false or whatever you like.
// if background task(s) were successful, promise in test will return true
backgroundComplete(testTrackingId);
// if not successful, promise in test will return this error object
backgroundComplete(testTrackingId, new Error('Failed'));
If anyone has any comments or improvements, that would be appreciated :)