【问题标题】:Promise not being executed in Jest test承诺未在 Jest 测试中执行
【发布时间】:2018-10-16 10:51:15
【问题描述】:

我目前正在为我们的一个项目创建一个新的 React 组件,我几乎无法为它编写一个合适的测试。我已经阅读了很多文档和博客文章等等,但我似乎无法让它运行。

TL;DR

对我来说,Promise 似乎没有被执行。当我使用调试器运行测试时,它不会在 Promise 的函数中停止,也不会在 then() 函数中停止。但是,它会在测试本身的 then/catch 函数中停止。

代码

因此,该组件实际上相当简单。目前它应该通过 API 搜索位置。它的测试如下所示:

import axios from 'axios';
import React from 'react';
import {shallowWithIntl} from "../../../Helpers/react-intl-helper";
import Foo from "../../../../src/Components/Foo/Foo";
import {mount} from "enzyme";

const queryTerm = 'exampleQueryTerm';
const locationAgs = 'exampleLocationKey';

const fakeLocationObject = {
  search: '?for=' + queryTerm + '&in=' + locationAgs
};

jest.mock('axios', () => {
  const exampleLocations = [{
    data: {"id": "expected-location-id"}
  }];

  return {
    get: jest.fn().mockReturnValue(() => {
      return Promise.resolve(exampleLocations)
    })
  };
});

let fooWrapper, instance;

beforeEach(() => {
  global.settings = {
    "some-setting-key": "some-setting-value"
  };

  global.URLSearchParams = jest.fn().mockImplementation(() => {
    return {
      get: function(param) {
        if (param === 'for') return queryTerm;
        else if (param === 'in') return locationAgs;
        return '';
      }
    }
  });

  fooWrapper = shallowWithIntl(<Foo location={fakeLocationObject} settings={ global.settings } />).dive();
  instance = fooWrapper.instance();
});

it('loads location and starts result search', function() {
  expect.assertions(1);

  return instance
    .searchLocation()
    .then((data) => {
      expect(axios.get).toHaveBeenCalled();
      expect(fooWrapper.state('location')).not.toBeNull();
    })
    .catch((error) => {
      expect(fooWrapper.state('location')).toBe(error);
    });
});

因此,如您所见,测试应该在 Foo 组件实例上调用 searchLocation,该实例返回一个 Promise 对象,正如您(几乎)可以在其实现中看到的那样。

import React, { Component } from 'react';
import { injectIntl } from "react-intl";
import {searchLocationByKey} from "../../Services/Vsm";

class Foo extends Component {

  constructor(props) {
    super(props);

    this.state = {
      location: null,
      searchingLocation: false,
      searchParams: new URLSearchParams(this.props.location.search)
    };
  }

  componentDidUpdate(prevProps) {
    if (!prevProps.settings && this.props.settings) {
      this.searchLocation();
    }
  }

  searchLocation() {
    this.setState({
      searchingLocation: true
    });

    const key = this.state.searchParams.get('in');

    return searchLocationByKey(key)
      .then(locations => {
        this.setState({ location: locations[0], searchingLocation: false })
      })
      .catch(error => console.error(error));
  }

  render() {
    // Renders something
  };

}

export default injectIntl(Foo);

输入searchLocationByKey

function requestLocation(url, resolve, reject) {
  axios.get(url).then(response => {
    let locations = response.data.map(
      location => ({
        id: location.collectionKey || location.value,
        rs: location.rs,
        label: location.label,
        searchable: location.isSearchable,
        rawData: location
      })
    );

    resolve(locations);
  }).catch(error => reject(error));
}

export const searchLocationByKey = function(key) {
  return new Promise((resolve, reject) => {
    let url = someGlobalBaseUrl + '?regional_key=' + encodeURIComponent(key);
    requestLocation(url, resolve, reject);
  });
};

问题

这是测试的输出:

Error: expect(received).toBe(expected)

Expected value to be (using ===):
  [Error: expect(received).not.toBeNull()

Expected value not to be null, instead received
  null]
Received:
  null

我不得不承认我对 Promises、React 和 JavaScript 测试还很陌生,所以我可能混淆了几件事。正如我上面所写的,Promise 似乎没有正确执行。调试时不会停在Foo.searchLocation中定义的then()函数中。相反,显然,测试中定义的 then() 和 catch() 函数都被执行了。

我已经在这个问题上花费了太多时间,我不知道如何继续。我做错了什么?

更新 1:done() 函数

正如 El Aoutar Hamza 在下面的答案中指出的那样,可以将函数(通常称为“完成”)传递给测试函数。我就是这样做的:

it('loads location and starts result search', function(done) {
  expect.assertions(1);

  return instance
    .searchLocation()
    .then((data) => {
      expect(fooWrapper.state('location')).not.toBeNull();
      done();
    })
    .catch((error) => {
      expect(fooWrapper.state('location')).toBe(error);
    });
});

但我最终得到了这个错误:

Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.

【问题讨论】:

    标签: javascript reactjs testing jestjs es6-promise


    【解决方案1】:

    就像我在 El Aoutar Hamza 的回答下的最新评论中提到的那样,感谢一位能够帮助我的同事,我找到了解决方案。

    似乎不可能将 Promise 从 Foo.searchLocation 返回到测试中。我们需要做的是将获取和处理来自searchLocationByKey 的 Promise 的代码包装到另一个 Promise 中,如下所示:

    import React, { Component } from 'react';
    import { injectIntl } from "react-intl";
    import {searchLocationByKey} from "../../Services/Vsm";
    
    class Foo extends Component {
    
      constructor(props) {
        super(props);
    
        this.state = {
          location: null,
          searchingLocation: false,
          searchParams: new URLSearchParams(this.props.location.search)
        };
      }
    
      componentDidUpdate(prevProps) {
        if (!prevProps.settings && this.props.settings) {
          this.searchLocation();
        }
      }
    
      searchLocation() {
        this.setState({
          searchingLocation: true
        });
    
        const key = this.state.searchParams.get('in');
    
        return new Promise((resolve, reject) => {
          searchLocationByKey(key)
            .then(locations => {
              this.setState({ location: locations[0], searchingLocation: false });
              resolve();
            })
            .catch(error => {
              console.error(error));
              reject();
            }
        });
      }
    
      render() {
        // Renders something
      };
    
    }
    
    export default injectIntl(Foo);

    直到那时,Jest 才能够正确地融入承诺,并且一切都按照我最初的预期工作。

    我仍然不明白为什么不能简单地返回承诺并且需要将其包装在另一个 Promise 中。因此,如果有人对此有解释,将不胜感激。

    【讨论】:

      【解决方案2】:

      requestLocation 内部,您正尝试访问response.data,而在模拟axios.get 时,您将返回一个用array 解析的Promise!你应该返回一个 Promise 解决的 objectdata 属性(包含数组)。

      jest.mock('axios', () => ({
        get: jest.fn(() => Promise.resolve({
          data: [{ "id": "expected-location-id" }]
         }))
      }));
      

      另外一点是,在测试异步代码时,测试甚至会在调用回调之前完成,这就是为什么你应该考虑为你的测试提供一个名为done 的参数,这样,jest 将等到调用 done 回调。

      describe('Foo', () => {
          it('loads location and starts result search', done => {
            expect.assertions(1);
      
            return instance
              .searchLocation()
              .then((data) => {
                expect(fooWrapper.state('location')).not.toBeNull();
                done();
              })
              .catch((error) => {
                expect(fooWrapper.state('location')).toBe(error);
                done();
              });
          });
      });
      

      【讨论】:

      • 您好,感谢您的回复。确实,返回值是错误的。我也尝试过 done 功能,但并没有真正奏效,但导致了这个错误:Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
      • 当您将参数 (done) 传递给 it 时,您需要在测试中调用它以让 Jasmine 知道您的测试已完成,即您需要在您的 Promise 处理程序中调用它
      • 我就是这么做的。我已经用结果更新了我的帖子。
      • 您也没有在catch 中调用它,而且您似乎没有解决我所说的嘲笑axios.get (我编辑了我的答案)
      • 我也尝试在两个分支中调用它,但结果是一样的。我已经适应了嘲笑,但没有将其复制到帖子中,对不起。 :) 与此同时,我能够与一位同事一起工作。将发布答案。
      猜你喜欢
      • 2016-07-23
      • 2017-09-14
      • 1970-01-01
      • 2019-03-11
      • 2020-07-06
      • 2017-03-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多