简体   繁体   English

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

[英]How to TEST async calls made in componentDidMount that set the state of React Component

What is the best way to test that an async call within componentDidMount sets the state for a React component? 测试componentDidMount中的异步调用设置React组件状态的最佳方法是什么 For context, the libraries I'm using for testing are Mocha , Chai , Enzyme , and Sinon . 对于上下文,我用于测试的库是MochaChaiEnzymeSinon

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

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 . 正如一些人指出的那样,实现这一目标的一个好方法是将数据获取逻辑放入一个单独的容器中,并拥有一个只知道如何渲染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对Typescript进行一些修改,但你会得到这个想法)

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渲染。

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 模拟请求(fx with sinon),使其返回一些记录的承诺
  • use Enzyme's mount function 使用Enzyme的mount功能
  • Assert that the state to not have your records yet 断言状态还没有你的记录
  • Have your rest function use done callback 让你的休息功能使用done回调
  • Wait a bit (fx with setImmediate ), this will make sure your promise is resolved 稍等一下(使用setImmediate 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 调用完成的回调以通知测试已完成

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一无所知,所以在这里我假设你的代码是正确的

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. IMO,这实际上是一个常见的问题,因为promises和componentDidMount看起来更复杂:你试图测试只在另一个函数范围内定义的函数。 ie 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.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. 注意 :我单独从文档编写单元测试,因此使用shallowmountassertexpect可能不是最佳实践。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何在React Native中测试异步ComponentDidMount? - How to test a async ComponentDidMount in React Native? 如何在 componentDidMount 中存根异步操作以使用 Jest 进行反应 redux 连接组件测试 - How to stub async action in componentDidMount for react redux connected component test using Jest 如何在异步componentDidMount axios调用中测试正确的状态更新? - How to test correct state update in async componentDidMount axios call? 如何在异步调用反应后测试 state 更新和组件重新渲染 - How to test state update and component rerender after async call in react 如何使用链式承诺测试react componentDidMount中的异步提取? - How do I test async fetch in react componentDidMount with chained promises? React Enzyme - 测试`componentDidMount`异步调用 - React Enzyme - Test `componentDidMount` Async Call 使用 react-test-renderer 测试异步 componentDidMount() - Testing async componentDidMount() with react-test-renderer 如何通过异步函数设置反应父组件状态,然后将此状态作为props传递给子组件? - how to set react parent component state by a async function and then pass this state to child as props? 如何使用 componentDidMount 和 Async 在 API 调用的组件 state 中保存输入条目? - How to save input entry in a state of component for an API call using componentDidMount and Async? 在 React 中,父组件如何从响应中设置状态以及在子组件中执行的异步获取功能? - In React, how can a parent component set state from the response an async fetch function performed in a child component?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM