Skip to content

Latest commit

 

History

History
286 lines (196 loc) · 9.05 KB

README.md

File metadata and controls

286 lines (196 loc) · 9.05 KB

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.

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

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', () => {
  // 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 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';

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

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

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

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.'); // 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);

License

MIT