2

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?

Canta
  • 1,480
  • 1
  • 13
  • 26
rfgamaral
  • 16,546
  • 57
  • 163
  • 275
  • Why don't you wrap it into a function? I am sure there are already some wrappers for promise expectations written. – Sulthan May 23 '17 at 17:34
  • @Sulthan I'd still like to understand what's going on and why this needs to be done this way. – rfgamaral May 23 '17 at 18:17
  • Looking at the test, it looks wrong. It tests a side-effect. Actually it seems there is something wrong in your architecture and that forces you to write tests which are too complicated. – Sulthan May 23 '17 at 18:39
  • @Sulthan How would you test it? What are architecture, it's just a request for demo purposes? Care to elaborate please? – rfgamaral May 23 '17 at 18:54

2 Answers2

2

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");
});
rfgamaral
  • 16,546
  • 57
  • 163
  • 275
1

My answer probably sucks, but I tested this kind of feature for first time recently as well with fetch promises. And this same aspect confused me.

I think you aren't duplicating the code in the implementation so much as you're having to defer your expectation until the asynchronous part of your mock/test-double promise has completed. If you put the expect outside of then, the expect will run before asynch part of stub that sets your state is done.

Basically your test double is still a promise and expect clauses in the test that depend on that double having executed need to be in then clause.

Jim Weaver
  • 983
  • 5
  • 15