在Jest中如何使用async/await测试Promise的catch()方法行为?

9

假设我有这个简单的React组件:

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

根据以下答案和更多的研究,我得出了一个最终方案来测试resolve()情况:

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

目前代码的运行很好。我的问题是如何测试catch()语句。以下代码并没有按照我预期地运行:

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

断言失败,name为空:
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)

我错过了什么?
4个回答

10

这条线

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

在两个测试中都不正确。这意味着shallow在返回一个promise,但它实际上不是这样。因此,在构造函数中等待promise链解析的方式并不正确。首先,将fetch请求移动到componentDidMount中,React文档建议在该方法中触发网络请求,像这样:

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

现在,我们可以直接调用componentDidMount来进行测试。由于ComponentDidMount返回了Promise,因此await将等待承诺链解决。

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>')
})

你不是第一个建议我将请求移动到 componentDidMount 的人。React文档推荐使用这个生命周期回调来实例化网络请求。但除了测试和我的特定问题之外,是否在构造函数中进行网络请求是一种不好的做法?如果是,为什么? - rfgamaral
刚刚测试了你的解决方案,它完美地运行了,我现在更好地理解了这些事情,所以非常感谢你。我只是对我的先前问题很好奇... - rfgamaral
1
使用构造函数与生命周期的一个问题是可能会不必要地调用它来加载数据。您可以实例化组件,但从未安装它,因此该网络调用从未被使用。一般而言,我认为两种方法都可以正常工作,但在某些边缘情况下,它并不理想,因此建议始终使用生命周期。可能存在其他缺点;不确定。 - TLadd

0

最近,我遇到了相同的问题,并最终通过以下方式解决了它(以您的代码为例)

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

0
从这段代码片段的外观来看
        .then((response) => {
            return response.text();
        })
        .then((name) => {
            this.setState({
                name: name
            });
        })

看起来这个函数会返回一个字符串,然后在下一个 'then' 块中作为 name 参数出现。或者它本身就返回一个 Promise?

你有没有研究过 jest 的 spyOn 特性?这将帮助你模拟不仅是 fetch 部分,还可以断言 setState 方法被调用的次数和预期值。

最后,我认为 React 不鼓励在 constructor 中进行副作用操作。构造函数应该用于设置初始状态和其他变量。使用 componentWillMount 应该是更好的选择 :)


response.text() 返回一个 promise (https://developer.mozilla.org/zh-CN/docs/Web/API/Body/text)。但是问题在于如何测试 catch - rfgamaral

-1

如果您不想调用done,则可以返回下一个 promise 状态给 jest。根据断言(expect)测试用例的结果,测试将会失败或者通过。

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

});

它没有起作用。首先,我必须调用global.fetch().catch()。其次,在更改后,断言仍然失败,name为空。但我也很难理解为什么当我使用reject()拒绝承诺时,我必须自己调用catch - rfgamaral

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接