console-testing-library
Testing console the right way.
Why
It's rare to have console
in your code, it's more often seen in libraries to provide helpful debugging warnings. When trying to mock console
in tests, we often just spyOn
the methods being used and observe the mock calls. This works great if your message is simple, but it can also have false-negative.
Consider a situation where we want to log inspectable objects, to make it prettily printed in TTY environments, and interactable in browser's console. There are only 2 options we can do in order to achieve with the current console API. Either by string substitution or with arguments concatenation.
console.error('I want to log this object: %o, and make it inspectable', obj);
console.error('I want to log this object:', obj, ', and make it inspectable');
We could use Jest's toHaveBeenCalledWith
here, but the tests would look like this.
expect(console.error).toHaveBeenCalledWith(
'I want to log this object: %o, and make it inspectable',
obj
);
expect(console.error).toHaveBeenCalledWith(
'I want to log this object:',
obj,
', and make it inspectable'
);
Either way is not ideal, we are just repeating the source code here, we're not testing what the user really sees, but what the code looks like. Every time when the message changed, we have to update the test too, which makes it a fragile test. In addition, what if obj
is not inspectable? Or not valid? Or simply not what we want? We cannot be sure without actually logging the message.
A better solution would be to get the actual output of the logs and test it against the expected output.
expect(actualLog).toBe(
'I want to log this object: { "foo": 42 }, and make it inspectable'
);
With console-testing-library
, we can easily do that without extra hassles.
import { getLog } from 'console-testing-library';
expect(getLog().log).toBe(
'I want to log this object: { "foo": 42 }, and make it inspectable'
);
With the help of Jest's toMatchInlineSnapshot
, we can even let it generate the log snapshot inline.
import { getLog } from 'console-testing-library';
expect(getLog().log).toMatchInlineSnapshot();
expect(console.log).toMatchInlineSnapshot();
Installation
yarn add -D console-testing-library
Usage with Jest
Just import it before calling console.log
or the family.
import 'console-testing-library';
test('testing console.log', () => {
console.log('Hello %s!', 'World');
});
If you want to get the current logs, import the getLog
helper.
import { getLog } from 'console-testing-library';
test('testing console.log', () => {
console.log('Hello %s!', 'World');
expect(getLog().log).toBe('Hello World!');
});
All the methods in console
are available and automatically mocked.
console.log('Hello %s!', 'World');
expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith('Hello %s!', 'World');
expect(getLog().log).toBe('Hello World!');
All the methods in console
will also be automatically cleared and cleaned up after each tests.
test('testing console.log 1', () => {
console.log('Hello %s!', 'World');
expect(console.log).toHaveBeenCalledTimes(1);
});
test('testing console.log 2', () => {
console.log('Hello %s!', 'World');
expect(console.log).toHaveBeenCalledTimes(1);
});
Every log have a corresponding logging level, you can access each level's log via getLog().levels
, or access all of them in a list with getLog().logs
.
console.log('This is %s level', 'log');
console.info('This is %s level', 'info');
console.warn('This is %s level', 'warn');
console.error('This is %s level', 'error');
expect(getLog().levels).toEqual({
log: 'This is log level',
info: 'This is info level',
warn: 'This is warn level',
error: 'This is error level',
});
expect(getLog().logs).toEqual([
['log', 'This is log level'],
['info', 'This is info level'],
['warn', 'This is warn level'],
['error', 'This is error level'],
]);
Additionally, the mocked console also exposes the following higher level syntax accessors:
stdout
: will return everything that's been logged through console.log
or console.info
stderr
: will return everything that's been logged through console.warn
or console.error
console.log('This is %s level', 'log');
console.error('This is %s level', 'error');
console.info('This is %s level', 'info');
console.warn('This is %s level', 'warn');
expect(getLog().stdout).toEqual('This is log level\nThis is info level');
expect(getLog().stderr).toEqual('This is error level\nThis is warn level');
Since the logs are patched, in order to log or debug in the tests will not output as expected. You can import originalConsole
to obtain the un-patched, un-mocked console
.
import { originalConsole } from 'console-testing-library';
console.log('Oops, this will not show since console.log is mocked.');
originalConsole.log(
'However, this is the un-mocked console and will output the logs'
);
Usage without Jest
It is possible to use console-testing-library
without Jest, just that you have to manually mock the console yourself. We provide createConsole
and mockConsole
API for this.
import { createConsole, mockConsole } from 'console-testing-library';
const testingConsole = createConsole();
const restore = mockConsole(testingConsole);
console.log('Mocked console.log');
restore();
Manually mocking with Jest
If your tests run with Jest then the global.console
is automatically mocked. If you wish to have more control and manually mock it, then import the package from pure
entry.
import { createConsole, mockConsole } from 'console-testing-library/pure';
let restore = () => {};
beforeEach(() => {
restore = mockConsole(createConsole());
});
afterEach(() => restore());
Optionally stripping out Ansi characters
When working with the console, it's not unusual to make the output a bit prettier by
relying on libraries such as chalk. Those libraries rely on
ANSI escape codes for styling strings in the terminal.
Although those ANSI codes make the console output nicer, they may make the test data a bit harder to parse and work with for the mere human being (e.g. Hello [31mWorld[39m!
).
The createConsole()
function accepts a stripAnsi
option (defaulting to false
) to dynamically remove the control characters from what's being logged.
import { createConsole, getLog, mockConsole } from 'console-testing-library';
const chalk = require('chalk');
const strippingConsole = createConsole({ stripAnsi: true });
const restore = mockConsole(strippingConsole);
console.log('Hello %s!', chalk.red('World'));
expect(getLog().log).toEqual('Hello World!');
restore();
Custom matchers
It's often recommended to use console-testing-library
with Jest's toMatchInlineSnapshot
matcher. It makes it really easy to test the console output with confidence.
expect(getLog().log).toMatchInlineSnapshot();
We also support custom toMatchInlineSnapshot
matcher to test against mocked console[method]
and console
.
expect(console).toMatchInlineSnapshot();
expect(console.log).toMatchInlineSnapshot();
Silent / Log the output
By default, the console is mocked to be silent. That is, calling console.log
would not output any actual log to the console, but swallowed into getLog()
. If you still want to log the output, you can call silenceConsole
.
import { silenceConsole } from 'console-testing-library';
console.log('It should be silent.');
silenceConsole(false);
console.log('It should now output the log to console');
silenceConsole(true);
console.log('It should be silent.');
If the console
is created by createConsole
, silenceConsole
can be called like below.
const someConsole = createConsole();
mockConsole(someConsole);
silenceConsole(someConsole, false);
License
MIT