How to TEST async calls made in componentDidMount that set the state of React Component
So, what you are really trying to test is that based on some mock data it "should render correctly ...".
As some people pointed out, a good way to achieve that is by placing the data fetching logic into a separate container and have a "dumb" presentation component that only knows how to render props
.
Here is how to do that:(I had to modify it a bit for Typescript with Tslint, but you'll get the idea)
export interface Props { // tslint:disable-next-line:no-any records: Array<any>;}// "dumb" Component that converts props into presentation class MyComponent extends React.Component<Props> { // tslint:disable-next-line:no-any constructor(props: Props) { super(props); } render() { return ( <div className="async_component"> {this._renderList()} </div> ); } _renderList() { // tslint:disable-next-line:no-any return this.props.records.map((record: any) => { return ( <div className="record" key={record.name}> <p>{record.name}</p> <p>{record.utility}</p> </div> ); }); }}// Container class with the async data loadingclass MyAsyncContainer extends React.Component<{}, Props> { constructor(props: Props) { super(props); this.state = { records: [] }; } componentDidMount() { fetch('/some/url/that/returns/my/data') .then((response) => response.json()) .then((data) => { this.setState({ records: data.records }); }); } // render the "dumb" component and set its props render() { return (<MyComponent records={this.state.records}/>); }}
Now you can test MyComponent
rendering by giving your mock data as props.
Ignoring the, sane, advice to think again about the structure, one way to go about this could be:
- Mock the request (fx with sinon), to make it return a promise for some records
- use Enzyme's
mount
function - Assert that the state to not have your records yet
- Have your rest function use
done
callback - Wait a bit (fx with
setImmediate
), this will make sure your promise is resolved - Assert on the mounted component again, this time checking that the state was set
- Call your done callback to notify that the test has completed
So, in short:
// asyncComponentTests.jsdescribe("Async Component Tests", () => { it("should render correctly after setState in componentDidMount executes", (done) => { nock("http://some.url.com") .get("/some/url/that/returns/my/data") .reply(200, { data: [ { id: 1, name: "willson", utility: 88 }, { id: 2, name: "jeffrey", utility: 102 } ] }); const wrapper = mount(<AsyncComponent />); // make sure state isn't there yet expect(wrapper.state).to.deep.equal({}); // wait one tick for the promise to resolve setImmediate(() => { expect(wrapper.state).do.deep.equal({ .. the expected state }); done(); }); });});
Note:
I have no clue about nock, so here I assume your code is correct
IMO, this is actually a common issue which appears more complicated because of promises and componentDidMount
:You're trying to test a functions which are only defined within the scope of another function. i.e. You should split your functions out and test them individually.
Component
class AsyncComponent extends React.Component { constructor(props) { super(props); this.state = { records: [] }; } componentDidMount() { request.get('/some/url/that/returns/my/data') .then(this._populateState); } render() { return ( <div className="async_component"> { this._renderList() } </div> ); } _populateState(data) { this.setState({ records: data.records }); } _renderList() { return this.state.records.map((record) => { return ( <div className="record"> <p>{ record.name }</p> <p>{ record.utility }</p> </div> ); }); }}
Unit Test
// asyncComponentTests.jsdescribe("Async Component Tests", () => { describe("componentDidMount()", () => { it("should GET the user data on componentDidMount", () => { const data = { records: [ { id: 1, name: "willson", utility: 88 }, { id: 2, name: "jeffrey", utility: 102 } ] }; const requestStub = sinon.stub(request, 'get').resolves(data); sinon.spy(AsyncComponent.prototype, "_populateState"); mount(<AsyncComponent />); assert(requestStub.calledOnce); assert(AsyncComponent.prototype._populateState.calledWith(data)); }); }); describe("_populateState()", () => { it("should populate the state with user data returned from the GET", () => { const data = [ { id: 1, name: "willson", utility: 88 }, { id: 2, name: "jeffrey", utility: 102 } ]; const wrapper = shallow(<AsyncComponent />); wrapper._populateState(data); expect(wrapper.state).to.deep.equal(data); }); });});
Note: I've written the unit tests from documentation alone, so the use of shallow
, mount
, assert
, and expect
might not be best practices.