Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

react-testing-library mocking axios.create({}) instance

I want to test my api with react-testing-library And I exporting the instance created by axios.create from a file called apiClient.ts

import axios from 'axios'

const apiClient = axios.create({
  baseURL: process.env.REACT_APP_API_URL,
  responseType: 'json',
  headers: {
    'Content-Type': 'application/json',
  },
})

export default apiClient

Then use the axios instances I get from apiClient in my users.ts fetchUsersApi

import apiClient from './apiClient'

export interface ITrader {
  id: number
  name: string
  username: string
  email: string
  address: any
  phone: string
  website: string
  company: any
}

export const fetchTradersApi = async (): Promise<ITrader[]> => {
  const response = await apiClient.get<ITrader[]>('/users')
  return response.data
}

I created a mocks folder and added axios.ts in it

export default {
  get: jest.fn(() => Promise.resolve({ data: {} })),
}

My users.spec.tsx looks like:

import { cleanup } from '@testing-library/react'
import axiosMock from 'axios'
import { fetchTradersApi } from './traders'

jest.mock('axios')

describe.only('fetchTradersApi', () => {
  
  afterEach(cleanup)
  it('Calls axios and returns traders', async () => {
    axiosMock.get.mockImplementationOnce(() =>
      Promise.resolve({
        data: ['Jo Smith'],
      })
    )
    const traders = await fetchTradersApi()
    expect(traders).toBe([{ name: 'Jo Smith' }])
    expect(axiosMock.get).toHaveBeenCalledTimes(1)
    expect(axiosMock.get).toHaveBeenCalledWith(`${process.env.REACT_APP_API_URL}/users`)
  })
})

I run my test and I get: Test suite failed to run

TypeError: _axios.default.create is not a function

  1 | import axios from 'axios'
  2 |
> 3 | const apiClient = axios.create({

Please help me on solving the issue by creating a proper axios mock that work with react-testing-library, Tnx in advance.

like image 1000
Kamran Avatar asked Feb 10 '20 17:02

Kamran


2 Answers

After spending an entire day I found a solution for the exact same problem which I was having. The problem which I was having is related to JEST, Node and Typescript combo. Let me brief about the files which are playing role in this:

  1. axios-instance.ts // initializing axios
  2. api.ts // api controller
  3. api.spec.ts // api test files

axios-instance.ts

import axios, { AxiosInstance } from "axios";

const axiosInstance: AxiosInstance = axios.create({
    baseURL: `https://example-path/products/`,
    headers: {
        'Content-Type': 'application/json'
    }
});

export default axiosInstance;

api.ts

"use strict";

import {Request, Response, RequestHandler, NextFunction} from "express";
import axiosInstance from "./axios-instance";

/**
 * POST /:productId
 * Save product by productId
 */
export const save: RequestHandler = async (req: Request, res: Response, next: NextFunction) => {
    try {
        const response = await axiosInstance.post(`${req.params.id}/save`, req.body);
        res.status(response.status).json(response.data);
    } catch (error) {
        res.status(error.response.status).json(error.response.data);
    }
};

api.spec.ts

import { save } from "./api";
import axiosInstance from "./axios-instance";

describe.only("controller", () => {

    describe("test save", () => {

        let mockPost: jest.SpyInstance;

        beforeEach(() => {
            mockPost = jest.spyOn(axiosInstance, 'post');
        });

        afterEach(() => {
            jest.clearAllMocks();
        });

        it("should save data if resolved [200]", async () => {

            const req: any = {
                params: {
                    id: 5006
                },
                body: {
                    "productName": "ABC",
                    "productType": "Food",
                    "productPrice": "1000"
                }
            };
            const res: any = {
                status: () => {
                    return {
                        json: jest.fn()
                    }
                },
            };
            const result = {
                status: 200,
                data: {
                    "message": "Product saved"
                }
            };

            mockPost.mockImplementation(() => Promise.resolve(result));

            await save(req, res, jest.fn);

            expect(mockPost).toHaveBeenCalled();
            expect(mockPost.mock.calls.length).toEqual(1);
            const mockResult = await mockPost.mock.results[0].value;
            expect(mockResult).toStrictEqual(result);
        });

        it("should not save data if rejected [500]", async () => {

            const req: any = {
                params: {
                    id: 5006
                },
                body: {}
            };
            const res: any = {
                status: () => {
                    return {
                        json: jest.fn()
                    }
                },
            };
            const result = {
                response: {
                    status: 500,
                    data: {
                        "message": "Product is not supplied"
                    }
                }
            };

            mockPost.mockImplementation(() => Promise.reject(result));

            await save(req, res, jest.fn);

            expect(mockPost).toHaveBeenCalled();
            const calls = mockPost.mock.calls.length;
            expect(calls).toEqual(1);
        });
    });
});

For the posted requirement we have to mock the "axiosInstance" not the actual "axios" object from the library as we are doing our calling from axiosInstance.

In the spec file we have imported the axiosInstance not the actual axios

import axiosInstance from "./axios-instance";

Then we have created a spy for the post method (get/post/put anything you can spy)

let mockPost: jest.SpyInstance;

Initializing in before each so that each test case will have a fresh spy to start with and also clearing the mocks are needed after each.

beforeEach(() => {
    mockPost = jest.spyOn(axiosInstance, 'post');
});

afterEach(() => {
    jest.clearAllMocks();
});

Mocking the implementation resolved/reject

mockPost.mockImplementation(() => Promise.resolve(result));
mockPost.mockImplementation(() => Promise.reject(result));

Then calling the actual method

await save(req, res, jest.fn);

Checking for the expected results

expect(mockPost).toHaveBeenCalled();
expect(mockPost.mock.calls.length).toEqual(1);
const mockResult = await mockPost.mock.results[0].value;
expect(mockResult).toStrictEqual(result);

Hope it helps and you can relate the solution with your problem. Thanks

like image 100
Zaki Mohammed Avatar answered Nov 04 '22 05:11

Zaki Mohammed


Maybe is too late to register my answer, but it can help others.

What has happened in this case is the context. Your apiClient function runs in another context, so one of the ways is to mock your apiClient instead of the Axios library.

...
import apiClient from 'path-to-your-apiClient';

jest.mock('path-to-your-apiClient');

const mockedApi = apiClient as jest.Mocked<typeof apiClient>;

Now, let's make some changes to your api.spec.ts:

import { save } from "./api";
import axiosInstance from "./axios-instance";

import apiClient from 'path-to-your-apiClient';

jest.mock('path-to-your-apiClient');

const mockedApi = apiClient as jest.Mocked<typeof apiClient>;

describe.only("controller", () => {

    describe("test save", () => {
    beforeEach(() => {
      jest.clearAllMocks();
      mockedApi.post.mockeResolvedValue({ your-defalt-value }) // if you need
    })

        it("should save data if resolved [200]", async () => {

            const req: any = {
                params: {
                    id: 5006
                },
                body: {
                    "productName": "ABC",
                    "productType": "Food",
                    "productPrice": "1000"
                }
            };
            const res: any = {
                status: () => {
                    return {
                        json: jest.fn()
                    }
                },
            };
            const result = {
                status: 200,
                data: {
                    "message": "Product saved"
                }
            };

            mockedApi.post.mockResolvedValue(result);

            await save(req, res, jest.fn);

            expect(mockedApi.post).toHaveBeenCalled();
            expect(mockedApi.post).toHaveBeenCalledTimes(1);
            ... // add may assertions as you want
        });

        ....
    });
});

Hope this piece of code can help others.

like image 3
Carlos Querioz Avatar answered Nov 04 '22 06:11

Carlos Querioz