如何测试在componentDidMount中设置React组件状态的异步调用

时间:2022-12-24 20:10:22

What is the best way to test that an async call within componentDidMount sets the state for a React component? For context, the libraries I'm using for testing are Mocha, Chai, Enzyme, and Sinon.

在componentDidMount中测试异步调用设置response组件的状态的最佳方法是什么?我用来测试的文库有Mocha, Chai,酵素和Sinon。

Here's an example code:

这里有一个例子代码:

/* 
 * 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.
    });
});

3 个解决方案

#1


1  

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)

这里是如何做到这一点的:(我必须用Tslint对它进行一些修改,但您会明白的)

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 loading
class 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.

现在,您可以通过将模拟数据作为道具来测试MyComponent呈现。

#2


0  

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
  • 模拟请求(使用sinon进行fx),使其返回一些记录的承诺
  • use Enzyme's mount function
  • 使用酶的功能
  • Assert that the state to not have your records yet
  • 断言该州尚未拥有您的记录
  • Have your rest function use done callback
  • 你的rest函数使用done回调吗
  • Wait a bit (fx with setImmediate), this will make sure your promise is resolved
  • 稍等一下(使用setimmediation进行fx),这将确保您的承诺得到解决
  • 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
  • 调用done回调以通知测试已经完成

So, in short:

所以,简而言之:

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

Note:

注意:

I have no clue about nock, so here I assume your code is correct

我对nock一无所知,所以我假设你的代码是正确的

#3


0  

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.

IMO,这实际上是一个常见的问题,因为承诺和componentDidMount而显得更加复杂:您试图测试一个仅在另一个函数的范围内定义的函数。也就是说,你应该把你的函数分解出来,然后分别测试它们。

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.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);
        });
    });
});

Note: I've written the unit tests from documentation alone, so the use of shallow, mount, assert, and expect might not be best practices.

注意:我只从文档中编写了单元测试,因此使用shallow、mount、assert和expect可能不是最佳实践。

#1


1  

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)

这里是如何做到这一点的:(我必须用Tslint对它进行一些修改,但您会明白的)

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 loading
class 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.

现在,您可以通过将模拟数据作为道具来测试MyComponent呈现。

#2


0  

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
  • 模拟请求(使用sinon进行fx),使其返回一些记录的承诺
  • use Enzyme's mount function
  • 使用酶的功能
  • Assert that the state to not have your records yet
  • 断言该州尚未拥有您的记录
  • Have your rest function use done callback
  • 你的rest函数使用done回调吗
  • Wait a bit (fx with setImmediate), this will make sure your promise is resolved
  • 稍等一下(使用setimmediation进行fx),这将确保您的承诺得到解决
  • 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
  • 调用done回调以通知测试已经完成

So, in short:

所以,简而言之:

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

Note:

注意:

I have no clue about nock, so here I assume your code is correct

我对nock一无所知,所以我假设你的代码是正确的

#3


0  

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.

IMO,这实际上是一个常见的问题,因为承诺和componentDidMount而显得更加复杂:您试图测试一个仅在另一个函数的范围内定义的函数。也就是说,你应该把你的函数分解出来,然后分别测试它们。

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.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);
        });
    });
});

Note: I've written the unit tests from documentation alone, so the use of shallow, mount, assert, and expect might not be best practices.

注意:我只从文档中编写了单元测试,因此使用shallow、mount、assert和expect可能不是最佳实践。