Testing console the right way.
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.
// string substitution
console.error('I want to log this object: %o, and make it inspectable', obj);
// arguments concatenation
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.
// string substitution
expect(console.error).toHaveBeenCalledWith(
'I want to log this object: %o, and make it inspectable',
obj
);
// arguments concatenation
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();
// or
expect(console.log).toMatchInlineSnapshot();
yarn add -D console-testing-library
Just import it before calling console.log
or the family.
import 'console-testing-library';
test('testing console.log', () => {
// No logs will be output to the console
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');
// Note that in real world console.log will output a new line character in the end,
// but in our case, we remove that since we don't really care about it.
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 throughconsole.log
orconsole.info
stderr
: will return everything that's been logged throughconsole.warn
orconsole.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'
);
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';
// Create a testingConsole instance. It's possible to create multiple instances if needed
const testingConsole = createConsole();
// Mock the global.console with the testingConsole we just created
// It returns a restore function, which will swap back to the original console
const restore = mockConsole(testingConsole);
console.log('Mocked console.log');
// Calls restore function when it's done to restore it back to the original console
restore();
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());
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'); // or any other similar library
const strippingConsole = createConsole({ stripAnsi: true });
const restore = mockConsole(strippingConsole);
console.log('Hello %s!', chalk.red('World'));
// Yeah! Plain text without any funky ANSI codes.
expect(getLog().log).toEqual('Hello World!');
restore();
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();
// is essentially the same as
// expect(getLog().log).toMatchInlineSnapshot();
expect(console.log).toMatchInlineSnapshot();
// is basically `expect(console.log).toHaveBeenCalledWith` for actual output,
// it will only get the logs calling from `log` method.
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.'); // No output
silenceConsole(false); // Set to be not silent
console.log('It should now output the log to console'); // Has output
silenceConsole(true); // Can set it back to be silent
console.log('It should be silent.'); // No output
If the console
is created by createConsole
, silenceConsole
can be called like below.
const someConsole = createConsole();
mockConsole(someConsole);
silenceConsole(someConsole, false);
MIT