4

I'm making a GET request to my API http://localhost:3001/api/cards from the componentDidMount function of a component, so that the api request is made only after the component is rendered for the first time (As suggested by react official guide).

This API sets the state of an array data. In render function, I call data.map function to render multiple components from this array. How should I test whether the desired number of components have been rendered?

My component:

//CardGrid.js

import React from 'react';
import { Card, Col, Row } from 'antd';
import 'antd/dist/antd.css';

import { parseJSON } from './commonfunction';
import './CardGrid.css';

export default class extends React.Component {
    constructor()
    {
        super();
        this.state = {
            data: {},
        };
    }

    fetchData = async () => {
        try
        {
            const data = await parseJSON(await fetch('http://localhost:3001/api/cards'));
            console.log('data');
            console.log(data);
            this.setState({ data });
        }
        catch (e)
        {
            console.log('error is: ');
            console.log(e);
        }
    }

    componentDidMount() {
        this.fetchData();
    }

    render() {
        return (
            <div style={{ background: '#ECECEC', padding: '30px' }}>
                <Row gutter={16}>
                    {Object.keys(this.state.data).map((title) => {
                        return (<Col span="6" key={title}>
                            <Card title={title} bodyStyle={{
                                'fontSize': '6em',
                            }}>{this.state.data[title]}</Card>
                        </Col>);
                    })}
                </Row>
            </div>
        );
    }
};

Now I want to check if there are as many Card components being rendered as specified by my API.

I tried this by first mocking the fetch function to return 1 element. Then I use Full DOM Rendering of enzyme and mount the above component and expect it to contain 1 element.

Test case:

// It fails
import React from 'react';
import { Card } from 'antd';
import { mount } from 'enzyme';
import CardGrid from './CardGrid';

it('renders 1 Card element', () => {
    fetch = jest.fn().mockImplementation(() =>
        Promise.resolve(mockResponse(200, null, '{"id":"1234"}')));
    const wrapper = mount(<CardGrid />);
    expect(fetch).toBeCalled();
    expect(wrapper.find(CardGrid).length).toEqual(1);
    expect(wrapper.find(Card).length).toEqual(1);
});

All the tests are passing except that it can't find Card element. Even the fetch mock function is called. It fails until I put a setTimeout function before I try to find Card component.

//It succeeds
import React from 'react';
import { Card } from 'antd';
import { mount } from 'enzyme';
import sinon from 'sinon';
import CardGrid from './CardGrid';
it('renders 1 Card elements', async () => {
    fetch = jest.fn().mockImplementation(() =>
        Promise.resolve(mockResponse(200, null, '{"id":"1234"}')));
    const wrapper = mount(<CardGrid />);
    expect(fetch).toBeCalled();
    expect(wrapper.find(CardGrid).length).toEqual(1);
    await setTimeoutP();
    expect(wrapper.find(Card).length).toEqual(1);

});

function setTimeoutP () {
    return new Promise(function (resolve, reject) {
        setTimeout(() => {
            console.log('111111111');
            resolve();
        }, 2000);
    });
}

Is there any concept I'm failing to understand? How should I ideally test such asynchronously loading components? How can I better design them to be easily testable? Any help would be greatly appreciated. Thanks

Andreas Köberle
  • 88,409
  • 51
  • 246
  • 277
Ayush
  • 73
  • 1
  • 13
  • Do you really need `await parseJSON`? You didn't include that source but I'm guessing it's not async. The MDN docs say that "If the value is not a promise, it converts the value to resolved promise, and waits for it." so it seems like it should work, but maybe something with this approach and the testing framework? – pherris Jan 09 '17 at 16:54
  • Sorry I didn't include the source of parseJSON. It basically calls `.json()` on the Response object returned by fetch API call. I need to await it as it returns a promise. https://developer.mozilla.org/en-US/docs/Web/API/Body/json – Ayush Jan 10 '17 at 08:07
  • In that case, I think the solution is to `await` BOTH promises (for `fetch` and for `parseJSON`). – pherris Jan 10 '17 at 17:29
  • On using `await (await result).json();`, I get `TypeError: Already read`. That is because body has already been read by using .json() and can't be read again. http://stackoverflow.com/questions/34786358/what-does-this-error-mean-uncaught-typeerror-already-read – Ayush Jan 10 '17 at 18:38
  • You have two Promises here, so both need to be resolved. You are mocking out the `fetch` to return a resolved promise so thats probably OK (along with the await from Andreas below), but seems like you _also_ need to mock out the `parseJSON` method e.g. `parseJSONPromise = Promise.resolve({"id": "1234"});` `parseJSON = jest.fn().mockImplementation(() => parseJSONPromise);` and `await parseJSONPromise;` and `expect(parseJSON.).toBeCalled();` – pherris Jan 10 '17 at 20:17
  • see the updated answer from Andreas... – pherris Jan 10 '17 at 20:53

1 Answers1

6

You have to wait for the resolved promise of your fetch result and for the promise from the parseJSON. Therefor we need to mock parseJSON and let it return a resolved promise as well. Please note that the path needs to be relative to the test file.

import {parseJSON} from './commonfunction'

jest.mock('./commonfunction', () => {parseJSON: jest.fn()}) //this will replace parseJSON in the module by a spy were we can later on return a resolved promise with


it('renders 1 Card elements', async () => {
    const result = Promise.resolve(mockResponse(200, null, '{"id":"1234"}')) 
    parsedResult = Promise.resolve({"id":"1234"})
    parseJSON.mockImplementation(()=>parsedResult)
    fetch = jest.fn(() => result)
    const wrapper = mount(<CardGrid />);
    await result;
    await parsedResult;

    expect(fetch).toBeCalled();
    expect(wrapper.find(CardGrid).length).toEqual(1);
    expect(wrapper.find(Card).length).toEqual(1);
});
Andreas Köberle
  • 88,409
  • 51
  • 246
  • 277