开玩笑:模拟 console.error - 测试失败

Posted

技术标签:

【中文标题】开玩笑:模拟 console.error - 测试失败【英文标题】:Jest: mocking console.error - tests fails 【发布时间】:2017-11-19 16:24:34 【问题描述】:

问题:

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

我想模拟console.error 来计算prop-types 在我传递缺失/错误类型的道具时调用它的次数。

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

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

相反,我得到了这个:

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

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

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

当我用酶/反应测试时,我在第一次测试后碰到了这堵墙。

示例 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);
);

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

【问题讨论】:

【参考方案1】:

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

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

【讨论】:

您不需要缓存原始方法。你可以做console.error.mockRestore() 有意思,下次遇到这个我试试 mockRestore 在这种情况下不起作用。来自 jest 24 文档:“请注意,mockFn.mockRestore 仅在使用 jest.spyOn 创建模拟时才有效。因此,在手动分配 jest.fn() 时,您必须自己处理恢复。” 经典! . . . .【参考方案2】:

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

describe('desc', () => 
    let spy = spyConsole();

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

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

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

function spyConsole() 
    // https://github.com/facebook/react/issues/7047
    let spy = ;

    beforeAll(() => 
        spy.console = jest.spyOn(console, 'error').mockImplementation(() => );
    );

    afterAll(() => 
        spy.console.mockRestore();
    );

    return spy;

【讨论】:

【参考方案3】:

上面写的都是对的。我遇到了类似的问题,这是我的解决方案。当您对模拟对象进行一些断言时,它还会考虑情况:

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();
  );

【讨论】:

我发现调用 mockImplementationOnce 很有用,可以避免恢复模拟。【参考方案4】:

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

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

【讨论】:

【参考方案5】:

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

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



【讨论】:

以上是关于开玩笑:模拟 console.error - 测试失败的主要内容,如果未能解决你的问题,请参考以下文章

开玩笑的模拟尺寸不变

开玩笑的测试在调用模拟的 firebase 函数之前完成,因此失败

开玩笑地模拟 useDispatch 并在功能组件中使用该调度操作来测试参数

如何开玩笑地模拟 i18next 模块

在开玩笑的模拟模块工厂中模拟一个承诺

如何在玩笑测试中模拟“readline.createInterface”