Reputation: 1075
I have a component which behaves like the following.
The code is like this:
import React from 'react';
class IpAddress extends React.Component {
state = {
ipAddress: null
};
constructor(props) {
super(props);
this.fetchData();
}
fetchData() {
return fetch(`https://jsonip.com`)
.then((response) => response.json())
.then((json) => {
this.setState({ ipAddress: json.ip });
});
}
render() {
if (!this.state.ipAddress) return <p class="Loading">Loading...</p>;
return <p>Pretty fly IP address you have there.</p>
}
}
export default IpAddress;
This works fine. The Jest test is a struggle though. Using jest-fetch-mock works well.
import React from 'react';
import ReactDOM from 'react-dom';
import { mount } from 'enzyme';
import IpAddress from './IpAddress';
it ('updates the text when an ip address has loaded', async () => {
fetch.mockResponse('{ "ip": "some-ip" }');
const address = mount(<IpAddress />);
await address.instance().fetchData();
address.update();
expect(address.text()).toEqual("Pretty fly IP address you have there.");
});
It's a bit sad that I have to call await address.instance().fetchData()
, just to make sure that the update has happened. Without this, the promise from fetch
or the async nature of setState
(I'm not sure which) don't run until after my expect
; the text is left as "Loading".
Is this the sane way to test code like this? Would you write this code completely differently?
My problem has since escalated. I'm using a high order component, which means I can no longer do .instance()
and use the methods on it - I'm not sure how to get back to my unwrapped IpAddress. Using IpAddress.wrappedComponent
doesn't give me back the original IpAddress, like I expected.
This fails with the following error message, which I unfortunately don't understand.
Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
Check the render method of `WrapperComponent`.
Upvotes: 0
Views: 1759
Reputation: 208
You could use react-testing-library's waitForElement
to avoid having to explicitly await
on your fetch
call and simplify things a little:
import React from "react";
import IpAddress from "./IpAddress";
import { render, cleanup, waitForElement } from "react-testing-library";
// So we can use `toHaveTextContent` in our expectations.
import "jest-dom/extend-expect";
describe("IpAddress", () => {
beforeEach(() => {
fetch.resetMocks();
});
afterEach(cleanup);
it("updates the text when an IP address has loaded", async () => {
fetch.mockResponseOnce(JSON.stringify({ ip: "1.2.3.4" }));
const { getByTestId } = render(<IpAddress />);
// If you add data-testid="ip" to your <p> in the component.
const ipNode = await waitForElement(() => getByTestId("ip"));
expect(ipNode).toHaveTextContent("Pretty fly IP address you have there.");
});
});
This will automatically wait for your element to appear and fail if it doesn't appear by some timeout. You still have to await
but hopefully this is a little closer to what you originally wanted.
Upvotes: 1
Reputation: 11
I must admit haven't used really jest-fetch-mock before, but from the docs and my little experiments, it looks like it replaces the global fetch
with a mocked version. Notice how in this example isn't awaiting any promises: https://github.com/jefflau/jest-fetch-mock#simple-mock-and-assert. It's merely checking that fetch
was called with the right arguments. So therefore I think you can remove the async/await and assert there is a call to jsonip.com.
What I think is tripping you up is actually the React lifecycle. Essentially it boils down to where you put the fetch
call. The React team discourages you from putting "side-effects" (like fetch
) in the constructor
. Here's the official React docs description: https://reactjs.org/docs/react-component.html#constructor. Unfortunately I've not really found good documentation on why. I believe it's because React may call the constructor
at odd times during the lifecycle. I think it is also the reason that you're having to manually call the fetchData
function in your test.
The best practice for putting side-effects is in componentDidMount
. Here's an ok explaination of why: https://daveceddia.com/where-fetch-data-componentwillmount-vs-componentdidmount/ (although it's worth noting that componentWillMount
is now deprecated in React 16.2). componentDidMount
is called exactly once, only after the component is rendered into the DOM.
It's also worth noting that this is all going to change soon with upcoming versions of React. This blog post/conference video goes into a lot more details: https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html
This approach means it will render initially in the loading state, but once the request has resolved you can trigger a re-render by setting state. Because you're using mount
from Enzyme in your test, this will call all of the necessary lifecycle methods, including componentDidMount
and so you should see the mocked fetch
being called.
As for the higher order component, there's a trick that I sometimes use that is maybe not the best practice, but I think is a pretty useful hack. ES6 modules have a single default
export, as well as as many "regular" exports as you like. I leverage this to export the component multiple times.
The React convention is to use the default
export when importing components (i.e. import MyComponent from './my-component'
). This means you can still export other things from the file.
My trick is to export default
the HOC-wrapped component, so that you can use it in your source files as you would with any other component, but also export the unwrapped component as a "regular" component. That would look something like:
export class MyComponent extends React.Component {
...
}
export default myHOCFunction()(MyComponent)
Then you can import the wrapped component with:
import MyComponent from './my-component'
And the unwrapped component (i.e. for use in tests) with:
import { MyComponent } from './my-component'
It's not the most explicit pattern in the world, but it is quite ergonomic imo. If you wanted explicitness, you could do something like:
export const WrappedMyComponent = myHOCFunction()(MyComponent)
export const UnwrappedMyComponent = MyComponent
Upvotes: 1