React-Intl-Unit-Test

Unit testing i18n components

Today, while at work, I got crazy trying to figure out how to unit test properly components that are using react-intl. The test runner was constantly throwing the error:

Invariant Violation: [React Intl] Could not find required `intl` object. <IntlProvider> needs to exist in the component ancestry.

Although the error states pretty much the solution, it was a bit tricky to make things work properly. So I decided to blog about this.

The problem

React-intl is a very common package used for localizing the application. By wrapping your components within an IntlProvider or injecting context using injectIntl we can easily use locales for our app. However, this creates a problem while unit testing. Our app will require a context object for locales in order to work. Let me show you with a concrete example:

App.js
import React from 'react'
import { FormattedDate } from 'react-intl'

const App = ({importantDate}) => (
    <div>
        <FormattedDate
            value={importantDate}
            year='numeric'
            month='long'
            day='numeric'
            weekday='long'
        />
    </div>
);

export default App

Our App is using a FormattedDate component which is part of react-intl package. This component will render the date based on the user's locale configuration. Now let's write a very simple test for our App.

App.test.js (not working)
/* global describe, it, expect, beforeEach */
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

describe('[Component] App', function () {
    let div

    beforeEach(function () {
        div = document.createElement('div')
        ReactDOM.render(<App importantDate={new Date(2017, 6, 11, 22, 13, 0)}/>, div)
    })

    it('should render', function () {
        expect(div.querySelector('span').innerHTML).toEqual('Tuesday, July 11, 2017')
    })
})
If you run the above using a test runner, you will see an error about a missing IntlProvider ancestor. Obviously, the solution is very simple, but a bit tricky depending on what you want to achieve.

Solution

To fix the problem we simply have to wrap our component with an IntlProvider. But imagine doing this every time you have to mount or re-render the component... It is not maintainable. To avoid this we can create a factory method which wraps our component and returns it. In this example I used the method within the same file, but eventually you can also include this into a helper file so that you don't have to rewrite the function before each test.

App.test.js (working)
/* global describe, it, expect, beforeEach */
import React from 'react'
import ReactDOM from 'react-dom'
import { IntlProvider } from 'react-intl'
import App from './App'

describe('[Component] App', function () {
    let div, comp

    // Create a factory method which injects the context to our component.
    // Call this function whenever you need to render the component or
    // update the properties.
    const factory = (props = {}) => (
        <IntlProvider locale="en">
            <div>
                {/* We are using ref here so that in case we want */}
                {/* direct access to the component we have that. */}
                <App {...props} ref={n => comp = n}/>
            </div>
        </IntlProvider>
    )

    // Create a new App component before every test with default props.
    beforeEach(function () {
        div = document.createElement('div')
        ReactDOM.render(factory({importantDate: new Date(2017, 6, 11, 22, 13, 0)}), div) 
    })

    it('should render', function () {
        expect(div.querySelector('span').innerHTML).toEqual('Tuesday, July 11, 2017')

        // Text should change
        ReactDOM.render(factory({importantDate: new Date(2017, 2, 9, 11, 15, 0)}), div)
        expect(div.querySelector('span').innerHTML).toEqual('Thursday, March 9, 2017')

        // Imagine that you want to test some methods of your component,
        // now you simply do comp.myMethod()
    })
})

Conclusion

I know that this solution might seem as a no-brainer but believe me - it took a bit to figure out an easy way to do this. I thought there was a way to mock internationalization or to set configurations and spent my time looking for the best solution, but I couldn't find any. That is why I came up with this solution and decided to blog about that. I hope that this helps somebody. Please comment below if you have any better solution. Stay happy!