Clean your Tests from React-Intl "Missing message" Errors in Console
Yannick Wolff6 min read
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:
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:
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 messageid
.
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
- 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",
}
- Each time you mount or shallow a component, you should pass it as a
messages
props in theIntlProvider
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.