Как тестировать асинхронные вызовы, сделанные в компонентеDidMount, которые задают состояние компонента React

Каков наилучший способ проверки того, что асинхронный вызов в componentDidMount устанавливает состояние для компонента React? В контексте библиотеки, которые я использую для тестирования, Mocha, Chai, Enzyme и Sinon.

Вот пример кода:

/* 
 * assume a record looks like this:
 * { id: number, name: string, utility: number }
 */

// asyncComponent.js
class AsyncComponent extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            records: []
        };
    }

    componentDidMount() {
        // assume that I'm using a library like `superagent` to make ajax calls that returns Promises

        request.get('/some/url/that/returns/my/data').then((data) => {
            this.setState({
                records: data.records
            });
        });
    }

    render() {
        return (
            <div className="async_component">
                { this._renderList() }
            </div>
        );
    }

    _renderList() {
        return this.state.records.map((record) => {
            return (
                <div className="record">
                    <p>{ record.name }</p>
                    <p>{ record.utility }</p>
                </div>
            );
        });
    }
}


// asyncComponentTests.js
describe("Async Component Tests", () => {
    it("should render correctly after setState in componentDidMount executes", () => {
        // I'm thinking of using a library like `nock` to mock the http request

       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 />);

       // NOW WHAT? This is where I'm stuck.
    });
});

Ответ 1

Игнорируя разумный совет, чтобы снова подумать о структуре, один из способов сделать это можно:

  • Отметьте запрос (fx с sinon), чтобы он дал обещание для некоторых записей
  • использовать функцию Enzyme mount
  • Предположим, что состояние еще не имеет ваших записей
  • Используйте функцию отдыха done
  • Подождите немного (fx с setImmediate), это гарантирует, что ваше обещание будет разрешено.
  • Повторить подтверждение на смонтированном компоненте, на этот раз проверяя, что состояние установлено
  • Вызов завершенного обратного вызова для уведомления о завершении теста

Итак, короче:

// asyncComponentTests.js
describe("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();
        });
    });
});

Примечание:

У меня нет подсказки о ноке, поэтому здесь я предполагаю, что ваш код правильный

Ответ 2

IMO, на самом деле это обычная проблема, которая выглядит более сложной из-за promises и componentDidMount: Вы пытаетесь проверить функции, которые определяются только в рамках другой функции. т.е. вы должны разделить свои функции и проверить их индивидуально.

Компонент

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.js
describe("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);
        });
    });
});

Примечание. Я написал модульные тесты только из документации, поэтому использование shallow, mount, assert и expect может быть не лучшим образом.