Reputation: 11970
The Problem:
I have a simple React component I'm using to learn to test components with Jest and Enzyme. As I'm working with props, I added the prop-types
module to check for properties in development. prop-types
uses console.error
to alert when mandatory props are not passed or when props are the wrong data type.
I wanted to mock console.error
to count the number of times it was called by prop-types
as I passed in missing/mis-typed props.
Using this simplified example component and test, I'd expect the two tests to behave as such:
Instead, I get this:
console.error
output is suppressed, so it's clear it's mocked for both.I'm sure I am missing something obvious, like clearing the mock wrong or whatever.
When I use the same structure against a module that exports a function, calling console.error
some arbitrary number of times, things work.
It's when I test with enzyme/react that I hit this wall after the first test.
Sample 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
};
Sample 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);
});
Final note: Yeah, it's better to write the component to generate some user friendly output when props are missing, then test for that. But once I found this behavior, I wanted to figure out what I'm doing wrong as a way to improve my understanding. Clearly, I'm missing something.
Upvotes: 48
Views: 86589
Reputation: 8055
This is all I needed:
let errorSpy: jest.SpyInstance
describe('MyComponent', () => {
beforeEach(() => {
errorSpy = jest.spyOn(console, 'error').mockImplementation()
})
it('logs an error to console', () => {
render(<MyComponent />)
expect(errorSpy).toHaveBeenCalled()
})
})
Upvotes: 0
Reputation: 9136
Given the behavior explained by @DLyman, you could do it like that:
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');
});
});
Upvotes: 36
Reputation: 35884
I ran into a similar problem, just needed to cache the original method
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')
})
Upvotes: 50
Reputation: 1011
For my solutions I'm just wrapping original console and combine all messages into arrays. May be someone it will be needed.
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)
})
})
}
Upvotes: 1
Reputation: 769
What guys wrote above is correct. I've encoutered similar problem and here's my solution. It takes also into consideration situation when you're doing some assertion on the mocked object:
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();
});
Upvotes: 18
Reputation: 96
You didn't miss anything. There is a known issue (https://github.com/facebook/react/issues/7047) about missing error/warning messages.
If you switch your test cases ('...when only id is passed' - the fisrt, '...when no props are passed' - the second) and add such
console.log('mockedError', console.error.mock.calls);
inside your test cases, you can see, that the message about missing id isn't triggered in the second test.
Upvotes: 7