Jest:mocking console.error - 测试失败

时间:2017-06-16 19:46:53

标签: javascript reactjs jestjs enzyme

问题:

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

我希望模仿console.error来计算prop-types在我传递丢失/错误输入的道具时调用它的次数。

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

  1. 第一次使用0/2必需道具的测试应该抓住模拟调用两次。
  2. 使用1/2必需道具的第二次测试应该捕获一次调用的模拟。
  3. 相反,我明白了:

    1. 第一次测试成功运行。
    2. 第二次测试失败,抱怨模拟函数被调用为零次。
    3. 如果我交换测试的顺序,第一个工作,第二个工作失败。
    4. 如果我将每个测试拆分成一个单独的文件,则两者都有效。
    5. console.error输出被抑制,因此很明显它被两个人嘲笑。
    6. 我确定我遗漏了一些明显的东西,比如清除模拟错误或其他什么。

      当我对导出函数的模块使用相同的结构时,调用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);
      });
      

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

5 个答案:

答案 0 :(得分:5)

鉴于@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;
}

答案 1 :(得分:5)

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

const original = console.error

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

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

答案 2 :(得分:2)

你没有错过任何东西。关于缺少错误/警告消息存在已知问题(https://github.com/facebook/react/issues/7047)。

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

答案 3 :(得分:2)

上面的人写的是正确的。我遇到了类似的问题,这是我的解决方案。在对模拟对象进行断言时,它还考虑了情况:

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

答案 4 :(得分:0)

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

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