Clean your Tests from React-Intl “Missing message” Errors in Console

Why is it important?

While programming on a project, you run your tests all the time, so it’s important not to lose time analysing the results of your tests.
We want to immediatly spot which tests are failing, not being disturb by any flashing red false negative errors, which take all the place in your console and make you miss the important information.

On the project I’m working on, the output of our tests was this kind of mess:

Tests output before

That’s why we decided to create this standard on our project:

If my tests are passing, there must be no errors in the output of the test command

So we started to tackle this issue, and noticed that 100% of our errors in tests were either due to required props we forgot to pass to components, or errors from the React Intl library.
I explain here how we managed to remove all these annoying React Intl errors from our tests:

Tests output after

How to avoid console errors from React-Intl?

The library complains that it does not know the translation for a message you want to render, because you did not pass them to the IntlProvider which wrap your components in your tests:

console.error node_modules/react-intl/lib/index.js:706
[React Intl] Missing message: “LOGIN_USERNAME_LABEL” for locale: “en”
console.error node_modules/react-intl/lib/index.js:725
[React Intl] Cannot format message: “LOGIN_USERNAME_LABEL”, using message id as fallback.

There are two ways to remove these errors.

  • The first one consists in explicitly giving the translations to the provider.
    It has the benefit of writting the real translations in your shallowed components instead of the translation keys, which makes snapshots more readable.
    It is easy to implement when you already have all your translations written in a file.
    However, it can be a bit tedious when your translations come from an API, because you have to update the list with the new translation every time you add a message.

  • The second solution works without having to update a list of translations, by automatically setting for every message a defaultMessage property equal to the message id.
    This will not impact your snapshots: you will still have the message id and not its translation.

1st solution: explicitly give the translations to your tests

  1. You have to write all your translations in a JSON file, which looks like this:
// tests/locales/en.json

{
  "LOGIN_USERNAME_LABEL": "Username",
  "LOGIN_PASSWORD_LABEL": "Password",
  "LOGIN_BUTTON": "Login",
}
  1. Each time you mount or shallow a component, you should pass it as a messages props in the IntlProvider which wraps the component:
// components/LoginButton/tests/LoginButton.test.js

import React from 'react';
import { IntlProvider } from 'react-intl';

import LoginButton from 'components/LoginButton';
import enTranslations from 'tests/locales/en.json';

it('calls login function on click', () => {
  const login = jest.fn();
  const renderedLoginButton = mount(
    <IntlProvider locale='en' messages={enTranslations}>
      <LoginButton login={login} />
    </IntlProvider>
  );
  renderedLoginButton.find('button').simulate('click');
  expect(loginFunction.toHaveBeenCalled).toEqual(true);
});

But actually, if you respect what is advised by React Intl documentation to shallow or mount components with Intl, you already have mountWithIntl and shallowWithIntl helper functions, and you pass your messages in the IntlProvider defined in these functions:

// tests/helpers/intlHelpers.js

import React from 'react';
import { IntlProvider, intlShape } from 'react-intl';
import { mount, shallow } from 'enzyme';

import enTranslations from 'tests/locales/en.json';

// You pass your translations here:
const intlProvider = new IntlProvider({
    locale: 'en',
    messages: enTranslations
}, {});

const { intl } = intlProvider.getChildContext();

function nodeWithIntlProp(node) {
    return React.cloneElement(node, { intl });
}

export function shallowWithIntl(node, { context } = {}) {
    return shallow(
        nodeWithIntlProp(node),
        {
            context: Object.assign({}, context, { intl }),
        }
    );
}

export function mountWithIntl(node, { context, childContextTypes } = {}) {
    return mount(
        nodeWithIntlProp(node),
        {
            context: Object.assign({}, context, { intl }),
            childContextTypes: Object.assign({},
                { intl: intlShape },
                childContextTypes
            )
        }
    );
}

And you can use these functions instead of mount and shallow:

// components/LoginButton/tests/LoginButton.test.js

import React from 'react';

import LoginButton from 'components/LoginButton';
import { mountWithIntl } from 'tests/helpers/intlHelpers';
import enTranslations from 'tests/locales/en.json';

it('calls login function on click', () => {
  const login = jest.fn();
  const renderedLoginButton = mountWithIntl(<LoginButton login={login} />);
  renderedLoginButton.find('button').simulate('click');
  expect(loginFunction.toHaveBeenCalled).toEqual(true);
});

2nd solution: pass a customized intl object to your shallowed and mounted components

Unlike the previous one, this solution works without having to update a list of translations, by using a customized intl object in your tests.

Customize the intl object

The idea is to modify the formatMessage method which is in the intl object passed to your component.

You have to make this formatMessage automatically add a defaultMessage property to a translation which does not already have one, setting its value the same as the translation id.

If we call originalIntl the intl object before customizing it, here is how you can do it:

const intl = {
  ...originalIntl,
  formatMessage: ({ id, defaultMessage }) =>
    originalIntl.formatMessage({
        id,
        defaultMessage: defaultMessage || id
    }),
};

How to use this customized intl in your tests

As in the previous solution, we’re going to modify the intlHelpers that React Intl documentation advise to use in tests.
The idea is to modify the two helper functions mountWithIntl and shallowWithIntl to give to the component our custom intl object instead of the original one.

In order to make the defaultMessage properties taken into account, you also have to give a defaultLocale props to the IntlProvider, with the same value as the locale props.

Here is the modified intlHeplers file:

// tests/helpers/intlHelpers.js

import React from 'react';
import { IntlProvider, intlShape } from 'react-intl';
import { mount, shallow } from 'enzyme';

import enTranslations from 'tests/locales/en.json';

// You give the default locale here:
const intlProvider = new IntlProvider({
    locale: 'en',
    defaulLocale: 'en'
}, {});

// You customize the intl object here:
const { intl: originalIntl } = intlProvider.getChildContext();
const intl = {
  ...originalIntl,
  formatMessage: ({ id, defaultMessage }) =>
    originalIntl.formatMessage({
        id,
        defaultMessage: defaultMessage || id
    }),
};
function nodeWithIntlProp(node) {
    return React.cloneElement(node, { intl });
}

export function shallowWithIntl(node, { context } = {}) {
    return shallow(
        nodeWithIntlProp(node),
        {
            context: Object.assign({}, context, { intl }),
        }
    );
}

export function mountWithIntl(node, { context, childContextTypes } = {}) {
    return mount(
        nodeWithIntlProp(node),
        {
            context: Object.assign({}, context, { intl }),
            childContextTypes: Object.assign({},
                { intl: intlShape },
                childContextTypes
            )
        }
    );
}

Then, you only have to use these functions instead of mount and shallow and all the warnings from React Intl will disappear from your shell.


You liked this article? You'd probably be a good match for our ever-growing tech team at Theodo.

Join Us