笑话:嘲笑 console.error - 测试失败

Jest: mocking console.error - tests fails

问题:

我有一个简单的 React 组件,我正在使用它来学习使用 Jest 和 Enzyme 测试组件。在处理道具时,我添加了 prop-types 模块来检查开发中的属性。 prop-types 使用 console.error 在未传递必需的 props 或 props 是错误的数据类型时发出警报。

我想模拟 console.error 来计算它在我传入 missing/mis-typed 道具时被 prop-types 调用的次数。

使用这个简化的示例组件和测试,我希望这两个测试的行为如下:

  1. 具有 0/2 个所需道具的第一个测试应该捕获模拟调用两次。
  2. 具有 1/2 所需道具的第二个测试应该捕获调用一次的模拟。

相反,我得到这个:

  1. 第一个测试运行成功。
  2. 第二次测试失败,抱怨 mock 函数被调用零次。
  3. 如果我调换测试顺序,第一个有效,第二个失败。
  4. 如果我将每个测试拆分成一个单独的文件,那么两者都有效。
  5. console.error 输出被抑制,所以很明显它对两者都进行了模拟。

我确定我遗漏了一些明显的东西,比如清除模拟错误或其他什么。

当我对导出函数的模块使用相同的结构时,调用 console.error 任意次数,一切正常。

这是我用enzyme/react测试的时候第一次测试就撞墙了

样本App.js:

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default class App extends Component {

  render(){
    return(
      <div>Hello world.</div>
    );
  }
};

App.propTypes = {
  id : PropTypes.string.isRequired,
  data : PropTypes.object.isRequired
};

样本App.test.js

import React from 'react';
import { mount } from 'enzyme';
import App from './App';

console.error = jest.fn();

beforeEach(() => {
  console.error.mockClear();
});

it('component logs two errors when no props are passed', () => {
  const wrapper = mount(<App />);
  expect(console.error).toHaveBeenCalledTimes(2);
});

it('component logs one error when only id is passed', () => {
  const wrapper = mount(<App id="stringofstuff"/>);
  expect(console.error).toHaveBeenCalledTimes(1);
});

最后说明: 是的,最好编写组件以在缺少 props 时生成一些用户友好的输出,然后对其进行测试。但是一旦我发现了这种行为,我就想弄清楚我做错了什么,以此来提高我的理解力。显然,我错过了一些东西。

你没有错过任何东西。存在一个关于丢失 error/warning 消息的已知问题 (https://github.com/facebook/react/issues/7047)。

如果你切换你的测试用例('...当只有 id 被传递时' - 第一个,'...当没有传递任何道具时' - 第二个)并添加这样的 console.log('mockedError', console.error.mock.calls); 在你的测试用例中,你可以看到,关于缺少 id 的消息在第二个测试中没有被触发。

鉴于@DLyman 解释的行为,您可以这样做:

describe('desc', () => {
    beforeAll(() => {
        jest.spyOn(console, 'error').mockImplementation(() => {});
    });

    afterAll(() => {
        console.error.mockRestore();
    });

    afterEach(() => {
        console.error.mockClear();
    });

    it('x', () => {
        // [...]
    });

    it('y', () => {
        // [...]
    });

    it('throws [...]', () => {
        shallow(<App />);
        expect(console.error).toHaveBeenCalled();
        expect(console.error.mock.calls[0][0]).toContain('The prop `id` is marked as required');
    });
});

我运行遇到了类似的问题,只是需要缓存原来的方法

const original = console.error

beforeEach(() => {
  console.error = jest.fn()
  console.error('you cant see me')
})

afterEach(() => {
  console.error('you cant see me')
  console.error = original
  console.error('now you can')
})

大佬们上面写的是对的。我遇到了类似的问题,这是我的解决方案。当你在模拟对象上做一些断言时,它也会考虑到情况:

beforeAll(() => {
    // Create a spy on console (console.log in this case) and provide some mocked implementation
    // In mocking global objects it's usually better than simple `jest.fn()`
    // because you can `unmock` it in clean way doing `mockRestore` 
    jest.spyOn(console, 'log').mockImplementation(() => {});
  });
afterAll(() => {
    // Restore mock after all tests are done, so it won't affect other test suites
    console.log.mockRestore();
  });
afterEach(() => {
    // Clear mock (all calls etc) after each test. 
    // It's needed when you're using console somewhere in the tests so you have clean mock each time
    console.log.mockClear();
  });

对于我的解决方案,我只是包装原始控制台并将所有消息组合到数组中。可能是需要的人。

const mockedMethods = ['log', 'warn', 'error']
export const { originalConsoleFuncs, consoleMessages } = mockedMethods.reduce(
  (acc: any, method: any) => {
    acc.originalConsoleFuncs[method] = console[method].bind(console)
    acc.consoleMessages[method] = []

    return acc
  },
  {
    consoleMessages: {},
    originalConsoleFuncs: {}
  }
)

export const clearConsole = () =>
  mockedMethods.forEach(method => {
    consoleMessages[method] = []
  })

export const mockConsole = (callOriginals?: boolean) => {
  const createMockConsoleFunc = (method: any) => {
    console[method] = (...args: any[]) => {
      consoleMessages[method].push(args)
      if (callOriginals) return originalConsoleFuncs[method](...args)
    }
  }

  const deleteMockConsoleFunc = (method: any) => {
    console[method] = originalConsoleFuncs[method]
    consoleMessages[method] = []
  }

  beforeEach(() => {
    mockedMethods.forEach((method: any) => {
      createMockConsoleFunc(method)
    })
  })

  afterEach(() => {
    mockedMethods.forEach((method: any) => {
      deleteMockConsoleFunc(method)
    })
  })
}