9

Say I have this simple React component:

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

Given the answers below and bit more of research on the subject, I've come up with this final solution to test the resolve() case:

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

    global.fetch = () => fetchPromise;

    const app = await shallow(<Application />);

    expect(app.state("name")).toEqual("John Doe");
});

Which is working fine. My problem is now testing the catch() case. The following didn't work as I expected it to work:

test.only("greeting name is 'John Doe'", async () => {
    const fetchPromise = Promise.reject(undefined);

    global.fetch = () => fetchPromise;

    const app = await shallow(<Application />);

    expect(app.state("name")).toEqual("<unknown>");
});

The assertion fails, name is empty:

expect(received).toEqual(expected)

Expected value to equal:
    "<unknown>"
Received:
    ""

    at tests/components/Application.spec.tsx:51:53
    at process._tickCallback (internal/process/next_tick.js:103:7)

What am I missing?

Canta
  • 1,480
  • 1
  • 13
  • 26
rfgamaral
  • 16,546
  • 57
  • 163
  • 275

4 Answers4

10

The line

const app = await shallow(<Application />);

is not correct in both tests. This would imply that shallow is returning a promise, which it does not. Thus, you are not really waiting for the promise chain in your constructor to resolve as you desire. First, move the fetch request to componentDidMount, where the React docs recommend triggering network requests, like so:

import React from 'react'

class Greeting extends React.Component {
  constructor() {
    super()
    this.state = {
      name: '',
    }
  }

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

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

export default Greeting

Now we can test it by calling componentDidMount directly. Since ComponentDidMount is returning the promise, await will wait for the promise chain to resolve.

import Greeting from '../greeting'
import React from 'react'
import { shallow } from 'enzyme'

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

  global.fetch = () => fetchPromise

  const app = shallow(<Greeting />)
  await app.instance().componentDidMount()

  expect(app.state('name')).toEqual('John Doe')
})

test("greeting name is '<unknown>'", async () => {
  const fetchPromise = Promise.reject(undefined)

  global.fetch = () => fetchPromise

  const app = shallow(<Greeting />)
  await app.instance().componentDidMount()

  expect(app.state('name')).toEqual('<unknown>')
})
TLadd
  • 6,488
  • 2
  • 32
  • 40
  • You are not the first person to recommend I move the request to `componentDidMount`. React docs recommend this lifecycle callback to instantiate the network request. But besides testing and my particular question, is it a bad practice to make the network request in the constructor? If so, why? – rfgamaral May 26 '17 at 16:14
  • Just tested your solution and it worked just fine and I better understand these things now, so thank you for that. I'm just curious about my previous question... – rfgamaral May 26 '17 at 16:41
  • 1
    One issue with using the constructor vs the lifecycle is that you might call it to load data unnecessarily. You may instantiate the component but then never mount it, so that network call was never used. In general, I think it would probably work fine either way, but there are certain edge cases where it is less ideal, so just always using the lifecycles is encouraged. There may be other drawbacks; not sure. – TLadd May 27 '17 at 01:17
0

By the looks of this snippet

        .then((response) => {
            return response.text();
        })
        .then((name) => {
            this.setState({
                name: name
            });
        })

it seems that text would return a string, which then would appear as the name argument on the next 'then' block. Or does it return a promise itself?

Have you looked into jest's spyOn feature? That would help you to mock not only the fetch part but also assert that the setState method was called the correct amount of times and with the expected values.

Finally, I think React discourages making side effects inside constructor. The constructor should be used to set initial state and other variables perhaps. componentWillMount should be the way to go :)

A. Capelo
  • 73
  • 1
  • 8
  • `response.text()` returns a promise (https://developer.mozilla.org/en-US/docs/Web/API/Body/text). But the issue on this question is testing the `catch`. – rfgamaral May 23 '17 at 09:52
0

Recently, I have faced the same issue and ended up resolving it by following way (taking your code as an example)

test.only("greeting name is 'John Doe'", async () => {

const fetchPromise = Promise.resolve(undefined);

jest.spyOn(global, 'fetch').mockRejectedValueOnce(fetchPromise)

const app = await shallow(<Application />);

await fetchPromise;

expect(app.state("name")).toEqual("<unknown>");}); 
Nishan shetty
  • 179
  • 3
  • 16
-1

Another way if you don't want to call done then return the next promise state to jest. Based on result of assertion( expect ) test case will fail or pass.

e.g

describe("Greeting", () => {

    test("greeting name is unknown", () => {
        global.fetch = () => {
            return new Promise((resolve, reject) => {
                process.nextTick(() => reject());
            });
        };
        let app = shallow(<Application />);
        return global.fetch.catch(() => {
           console.log(app.state());
           expect(app.state('name')).toBe('<unknown>');
        })
    });

});
WitVault
  • 23,445
  • 19
  • 103
  • 133
  • It didn't work. First I had to invoke `global.fetch().catch()` instead. Second, after that change, the assertion still failed with `name` being empty. But I'm also having a hard time understanding why I have to call `catch` myself when that is supposed to be called automatically when I reject the promise with `reject()`. – rfgamaral May 20 '17 at 22:07