Search code examples
reactjsunit-testingasynchronousjestjses6-promise

Why do I have to invoke promise.then() myself to test ES6 promises?


This is a followup question to this one (it's not necessary to read that question to answer this one).

Take the following React component as example:

class Greeting extends React.Component {
    constructor() {
        fetch("https://api.domain.com/getName")
            .then((response) => {
                return response.text();
            })
            .then((name) => {
                this.setState({
                    name: name
                });
            })
            .catch(() => {
                this.setState({
                    name: "<unknown>"
                });
            });
    }

    render() {
        return <h1>Hello, {this.state.name}</h1>;
    }
}

Using Jest, here's how we could assert that name equals to some text returned from the getName request:

test("greeting name is 'John Doe'", () => {
    const fetchPromise = Promise.resolve({
        text: () => Promise.resolve("John Doe")
    });

    global.fetch = () => fetchPromise;

    const app = shallow(<Application />);

    return fetchPromise.then((response) => response.text()).then(() => {
        expect(app.state("name")).toEqual("John Doe");
    });
});

But the following doesn't feel right:

return fetchPromise.then((response) => response.text()).then(() => {
    expect(app.state("name")).toEqual("John Doe");
});

I mean, I'm somewhat replicating the implementation in the test file.

It doesn't feel right that I have to invoke then() or catch() directly in my tests. Especially when response.text() also returns a promise and I have two chained then()s just to assert that name is equal to John Doe.

I come from Angular where one could just call $rootScope.$digest() and do the assertion afterwards.

Isn't there a similar way to achieve this? Or is there another approach for this?


Solution

  • I'll be answering my own question after discussing the subject with a colleague at work which made things clearer for me. Maybe the questions above already answered my question, but I'm giving an answer in words I can better understand.

    I'm not so much invoking then() myself from the original implementation, I'm only chaining another then() to be executed after the other ones.

    Also, a better practice is to place my fetch() call and all it's then()s and catch()s in it's own function and return the promise, like this:

    requestNameFromApi() {
        return fetch("https://api.domain.com/getName")
            .then((response) => {
                return response.text();
            })
            .then((name) => {
                this.setState({
                    name: name
                });
            })
            .catch(() => {
                this.setState({
                    name: "<unknown>"
                });
            });
    }
    

    And the test file:

    test("greeting name is 'John Doe'", () => {
        global.fetch = () => Promise.resolve({
            text: () => Promise.resolve("John Doe")
        });
    
        const app = shallow(<Application />);
    
        return app.instance().requestNameFromApi.then(() => {
            expect(app.state("name")).toEqual("John Doe");
        });
    });
    

    Which makes more sense. You have a "request function" returning a promise, you directly test the output of that function by invoking it and chain another then() to be invoked in the end so that we can safely assert what we need.

    If you interested in an alternative to returning the promise in the test, we can write the test above like this:

    test("greeting name is 'John Doe'", async () => {
        global.fetch = () => Promise.resolve({
            text: () => Promise.resolve("John Doe")
        });
    
        await shallow(<Application />).instance().requestNameFromApi();
    
        expect(app.state("name")).toEqual("John Doe");
    });