How do I mock react-i18next and i18n.js in jest?

Viewed 28981

package.json

"moduleNameMapper": {
  "i18next": "<rootDir>/__mocks__/i18nextMock.js"
}

i18n.js

import i18n from 'i18next'
import XHR from 'i18next-xhr-backend'
// import Cache from 'i18next-localstorage-cache'
import LanguageDetector from 'i18next-browser-languagedetector'

i18n
  .use(XHR)
  // .use(Cache)
  .use(LanguageDetector)
  .init({
    fallbackLng: 'en',
    // wait: true, // globally set to wait for loaded translations in translate hoc
    lowerCaseLng: true,
    load: 'languageOnly',
    // have a common namespace used around the full app
    ns: ['common'],
    defaultNS: 'common',
    debug: true,

    // cache: {
    //   enabled: true
    // },

    interpolation: {
      escapeValue: false, // not needed for react!!
      formatSeparator: ',',
      format: function (value, format, lng) {
        if (format === 'uppercase') return value.toUpperCase()
        return value
      }
    }
  })

export default i18n

i18nextMock.js

/* global jest */
const i18next = jest.genMockFromModule('react-i18next')
i18next.t = (i) => i
i18next.translate = (c) => (k) => k

module.exports = i18next

For some reason the jest unit tests are not getting a component.

Here is a unit test:

import React from 'react'
import { Provider } from 'react-redux'
import { MemoryRouter } from 'react-router-dom'
import { mount } from 'enzyme'

import { storeFake } from 'Base/core/storeFake'
import Container from '../container'

describe('MyContainer (Container) ', () => {
  let Component;

  beforeEach(() => {
    const store = storeFake({})

    const wrapper = mount(
      <MemoryRouter>
        <Provider store={store}>
          <Container />
        </Provider>
      </MemoryRouter>
    )

    Component = wrapper.find(Container)
  });

  it('should render', () => {
    // Component is undefined here
    expect(Component.length).toBeTruthy()
  })
})
4 Answers

I used Atemu's anwser in my jest tests, but ended up with the following one line in the mock:

module.exports = {t: key => key};

Also modified jest config as well since I import 't' from 'i18next':

"moduleNameMapper": {
    "i18next": "<rootDir>/__mocks__/reacti18nextMock.js"
}

In my case, using the useTranslation hook with TypeScript, it was as follows:

const reactI18Next: any = jest.createMockFromModule('react-i18next');

reactI18Next.useTranslation = () => {
  return {
    t: (str: string) => str,
    i18n: {
      changeLanguage: () => new Promise(() => {}),
    },
  };
};

module.exports = reactI18Next;

export default {};

The jest.config.ts:

const config: Config.InitialOptions = {
  verbose: true,
  moduleNameMapper: {
    'react-i18next': '<rootDir>/__mocks__/react-i18next.ts',
  },
};

Returning the key "as is" isn't the best. We are using English text as the key and it would be nice to "evaluate" values that we pass in (i.e. t('{{timePeriod}} left') evaluated to: '5 days left' ). In this case I created a helper function to do this. Below are the jest and extra files needed:

Jest configuration (i.e. jest.config.js) :

  moduleNameMapper: {
    'react-i18next': '<rootDir>/src/tests/i18nextReactMocks.tsx',
    'i18next': '<rootDir>/src/tests/i18nextMocks.ts',
    // ...
  },

i18nextMocks.ts:

function replaceBetween(startIndex: number, endIndex: number, original: string, insertion: string) {
  const result = original.substring(0, startIndex) + insertion + original.substring(endIndex);
  return result;
}

export function mockT(i18nKey: string, args?: any) {
  let key = i18nKey;

  while (key.includes('{{')) {
    const startIndex = key.indexOf('{{');
    const endIndex = key.indexOf('}}');

    const currentArg = key.substring(startIndex + 2, endIndex);
    const value = args[currentArg];

    key = replaceBetween(startIndex, endIndex + 2, key, value);
  }

  return key;
}

const i18next: any = jest.createMockFromModule('i18next');
i18next.t = mockT;
i18next.language = 'en';
i18next.changeLanguage = (locale: string) => new Promise(() => {});

export default i18next;

i18nextReactMocks.tsx:

import React from 'react';
import * as i18nextMocks from './i18nextMocks';

export const useTranslation = () => {
  return {
    t: i18nextMocks.mockT,
    i18n: {
      changeLanguage: () => new Promise(() => {}),
    },
  };
};

export const Trans = ({ children }) => <React.Fragment>{children}</React.Fragment>;

And I'll throw in the mock's unit tests for free :)

import * as i18nextMocks from './i18nextMocks';

describe('i18nextMocks', () => {
  describe('mockT', () => {
    it('should return correctly with no arguments', async () => {
      const testText = `The company's new IT initiative, code named Phoenix Project, is critical to the
        future of Parts Unlimited, but the project is massively over budget and very late. The CEO wants
        Bill to report directly to him and fix the mess in ninety days or else Bill's entire department
        will be outsourced.`;

      const translatedText = i18nextMocks.mockT(testText);

      expect(translatedText).toBe(testText);
    });

    test.each`
      testText                            | args                                          | expectedText
      ${'{{fileName}} is invalid.'}       | ${{ fileName: 'example_5.csv' }}              | ${'example_5.csv is invalid.'}
      ${'{{fileName}} {is}.'}             | ${{ fileName: '   ' }}                        | ${'    {is}.'}
      ${'{{number}} of {{total}}'}        | ${{ number: 0, total: 999 }}                  | ${'0 of 999'}
      ${'There was an error:\n{{error}}'} | ${{ error: 'Failed' }}                        | ${'There was an error:\nFailed'}
      ${'Click:{{li}}{{li2}}{{li_3}}'}    | ${{ li: '', li2: 'https://', li_3: '!@#$%' }} | ${'Click:https://!@#$%'}
      ${'{{happy}}y✔{{sad}}{{laugh}}'}  | ${{ happy: '', sad: '', laugh: '' }}    | ${'y✔'}
    `('should return correctly while handling arguments in different scenarios', ({ testText, args, expectedText }) => {
      const translatedText = i18nextMocks.mockT(testText, args);

      expect(translatedText).toBe(expectedText);
    });
  });

  describe('language', () => {
    it('should return language', async () => {
      const language = i18nextMocks.default.language;

      expect(language).toBe('en');
    });
  });
});
Related