chovy
chovy

Reputation: 75774

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

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()
  })
})

Upvotes: 29

Views: 50279

Answers (5)

Arturo Mendes
Arturo Mendes

Reputation: 748

As of 2023 and since this question has no accepted answer and I've had to modify slightly the examples provided by react-i18next I am posting the following hoping it will be of help to somebody. I am using jest and react-testing-library (RTL).

If you need different mocks in different tests the need for the mock is sparse, you can just mock the module at the beginning of each test (for example in one test you might just need to mock it and in another you want to spy on its use...). Nevertheless, if you are going to mock it, one way or another, in several test you'd better create the mocks separately as other answers recommend.

Just mock

If you just need to mock the module so the tests run seamlessly, react-i18next recommends you do the following:

jest.mock('react-i18next', () => ({
  // this mock makes sure any components using the translate hook can use it without a warning being shown
  useTranslation: () => {
    return {
      t: (str: string) => str,
      i18n: {
        changeLanguage: () => new Promise(() => {}),
        // You can include here any property your component may use
      },
    }
  },
}))

describe('Tests go here', () => {
   it('Whatever', () => {})
})

Mock and spy

If you are using the useTranslation hook and need to spy on its use, that's another story. In the example provided by react-i18next they use enzyme which hasn't kept up with react and it doesn't run with RTL, here is the fix:

import { useTranslation } from 'react-i18next'

jest.mock('react-i18next', () => ({
  useTranslation: jest.fn(),
}))

const tSpy = jest.fn((str) => str)
const changeLanguageSpy = jest.fn((lng: string) => new Promise(() => {}))
const useTranslationSpy = useTranslation as jest.Mock

beforeEach(() => {
  jest.clearAllMocks()

  useTranslationSpy.mockReturnValue({
    t: tSpy,
    i18n: {
      changeLanguage: changeLanguageSpy,
      language: 'en',
    },
  })
})

describe('Tests go here', () => {
   it('Whatever', () => {})
})

The only change is that you have to set up the mock return value before each test otherwise what will reach the component is undefined, then you can assert the t function and the changeLanguage function have been called.

Upvotes: 15

goamn
goamn

Reputation: 2136

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');
    });
  });
});

Upvotes: 0

Kidoncio
Kidoncio

Reputation: 149

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',
  },
};

Upvotes: 7

Bodhidharma
Bodhidharma

Reputation: 131

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"
}

Upvotes: 7

Atemu
Atemu

Reputation: 152

You don't need to mock the t function, only the translate one is required. For the second one, your usage of the parameters are confusing, also, you need to return a Component.

I was able to make it work on my project. Here are my mock file and my Jest configuration

Jest configuration

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

The source code to mock react-i18next

/* global jest */
import React from 'react'

const react_i18next = jest.genMockFromModule('react-i18next')

const translate = () => Component => props => <Component t={() => ''} {...props} />

react_i18next.translate = translate

module.exports = react_i18next

Upvotes: 9

Related Questions