Write Recoil Unit Tests with Mocked Providers

Purpose

I needed to create Recoil test with mocked provider in current project but there are not many sources because it’s specific one. So I will share it with you when you have the same problem.

I will mainly explain about this problem and solution so I don’t explain about recoil and react testing library, mocked provider in detail because there are a lot of sources for them.

The basic explanation: What’s these libraries?

As I already mentioned I don’t explain these libraries in detail but I will explain just basic with out going into detail.

Recoil

One of the most useful react state management library is Recoil which has several useful features such as atoms, selector. By using Recoil, you don’t need one big store such as Redux because you can create multiple store. If you want to know more about Recoil, please refer to official docs or any other articles, or my article.

https://blog.devgenius.io/how-to-use-recoil-which-is-a-state-management-library-in-react-736b3df65edf

React testing library

This is de facto standard in react testing because this library allows us testing nearing User action. Most of other testing libraries demand us the implementation the code in detail. It sounds good but it makes test breakable because if someone changes the code detail, it breaks easily. If you want to know about React testing library in detail, please refer to official docs or any other articles.

Mocked Provider

Mocked Provider is one of the recommended way to mock Apollo client’s request.

This way allows us easy mocking so it’s very useful before implementing server side or creating components for storybook or testing. If you want to know about Mocked Provider in detail, please refer to official docs or any other articles.

Usage of these libraries with combination.

If you want to use these libraries in real project, you need to setup your project. But I don’t explain about it because this is not my focus in this article.

Please refer below documents to setup.

Recoil: https://recoiljs.org/docs/introduction/getting-started

React testing library: https://testing-library.com/docs/react-testing-library/intro

Mocked provider(Apollo): https://www.apollographql.com/docs/react/get-started

Then you can use these libraries like below.

These code are almost production level and I simplified it to understand code easily.

In current project, we use graphql code generator instead of useQuery or useMutation to restrict type strongly. But I don't mention it because it's too complicated , I will just simplify it. Graphql code generator create a type and function file by query file like below.

viewSetting.ts

export const GET_VIEWS = gql`
  query getViews {
    views {
      id
      name
      authorId
      authorName
    }
  }
`;

export const DELETE_VIEW = gql`
  mutation deleteView($id: ID!) {
    deleteView(id: $id) {
      view {
        id
      }
    }
  }
`;

This file created by graphql code generator

graphql.ts

export type GetViewsQuery = {
  __typename?: 'Query';
  views: Array<{
    __typename?: 'view';
    id: string;
    name: string;
    authorId: string;
    authorName?: string | null;
  } | null>;
};

export type GetViewsQueryVariables = Exact<{ [key: string]: never }>;

export function useGetViewsLazyQuery(
  baseOptions?: Apollo.LazyQueryHookOptions<
    GetViewsQuery,
    GetViewsQueryVariables
  >
) {
  const options = { ...defaultOptions, ...baseOptions };
  return Apollo.useLazyQuery<GetViewsQuery, GetViewsQueryVariables>(
    GetViewsDocument,
    options
  );
}

Set atom file like below.

view.ts

import { atom } from 'recoil';

export const viewsState = atom({
  key: 'view/atoms/viewsState',
  default: [],
});

Create custom hook.

This file is for get and delete func.

useView.ts

import { useEffect } from 'react';
import _ from 'lodash';
import { useRecoilState, useSetRecoilState } from 'recoil';
import {
  useGetViewsLazyQuery,
  useDeleteViewMutation,
} from '@/apollo/graphql';
import { viewsState } from '@/store/view';

export const useView = () => {
  const [views, setViews] = useRecoilState(viewsState);

  const [getView] = useGetViewsLazyQuery({
    onCompleted: (data) => {
      if (!_.isEqual(views, data.views)) {
        setViews(data.individualViews);
      }
    },
    onError: (error) => {
      console.error(error);
  });

  const [deleteView] = useDeleteViewMutation({
    onCompleted: (data) => {
      console.log('delete view', data)
    onError: (error) => {
      console.error(error);
    },
  });

  return {
    getView,
    deleteView,
  };
};

When we want to create test for above hook, we need to create each mocks and set with Recoil root and mocked provider, also create mocks for mocked provider and useViewMock for testing.

useView.spec.tsx

import { ReactNode } from 'react';
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { renderHook, act } from '@testing-library/react';

import {
  GET_VIEWS,
  DELETE_VIEW,
} from '@/apollo/query/viewSetting';

import { RecoilRoot, useRecoilValue } from 'recoil';
import { viewsState } from '@/store/view';

type ChildrenProps = {
  children?: ReactNode;
};

const INITIAL_MOCK = [
  {
    id: '00000000-0000-0000-0000-000000000000_0',
    name: 'mock_0',
    authorId: '00000000-0000-0000-0000-000000000000_0',
    authorName: null,
  },
];

const GET_VIEWS_MOCK = [
  {
    id: '00000000-0000-0000-0000-000000000000_0',
    name: 'mock_view_0',
    authorId: '00000000-0000-0000-0000-000000000000_0',
    authorName: null,
  },
  {
    id: '00000000-0000-0000-0000-000000000000_1',
    name: 'mock_view_1',
    authorId: '00000000-0000-0000-0000-00000000000_1',
    authorName: null,
  },
];

const DELETE_VIEW_MOCK_ID = '00000000-0000-0000-0000-000000000000_0';

const renderRecoilHook = <P extends ChildrenProps, R>(
  callback: (props: P) => R,
  mock: MockedResponse<Record<string, unknown>>[]
) => {
  return renderHook(callback, {
    wrapper: ({ children }) => (
      <RecoilRoot
        initializeState={({ set }) => {
          set(viewsState, INITIAL_MOCK);
        }}
      >
        <MockedProvider mocks={mock} addTypename={false}>
          {children}
        </MockedProvider>
      </RecoilRoot>
    ),
  });
};

const useViewMock = () => {
  const { getView, deleteView } = useView();
  const views = useRecoilValue(viewsState);

  return {
    getView,
    deleteView,
    views,
  };
};

 describe('useView custom hook', () => {
  const mocks = [
    {
      request: {
        query: GET_VIEWS,
      },
      result: {
        data: {
          views: GET_VIEWS_MOCK,
        },
      },
    },
    {
      request: {
        query: DELETE_VIEW,
        variables: {
          id: DELETE_VIEW_MOCK_ID,
        },
      },
      result: {
        data: {
          deleteView: {
            view: {
              id: DELETE_VIEW_MOCK_ID,
            },
          },
        },
      },
    },
  ];

  test('initial values', () => {
    const { result } = renderRecoilHook(useViewMock, mocks);
    expect(result.current.views).toStrictEqual(INITIAL_VIEWS_MOCK);
    expect(result.current.isError).toBeFalsy();
    expect(result.current.isLoading).toBeFalsy();
  });

  test('getView', async () => {
    const { result } = renderRecoilHook(useViewMock, mocks);

    await act(async () => {
      await result.current.getView();
    });
    expect(result.current.views).toStrictEqual(GET_VIEWS_MOCK);
  });

  test('deleteView', async () => {
    const { result } = renderRecoilHook(useViewMock, mocks);

    await act(async () => {
      await result.current.deleteView({
        variables: {
          id: DELETE_VIEW_MOCK_ID,
        },
      });
    });

  expect(result.current.views).toStrictEqual([]);
  });

Above code seem to okay when run this test code.Although get test is fine, delete test is failed because it’s not working.

Why did this test fail?

The wrong point is here.

useView.ts

const [deleteView] = useDeleteViewMutation({
    onCompleted: (data) => {
      console.log('delete view', data)
    onError: (error) => {
      console.error(error);
    },
  });

Above code is totally fine in browser but not in test because Mocked provider can’t check real state. If we use getView func again or reload after using this deleteView, this code is no problem in browser. However, if we want to create a test by using Recoil, you need to update recoil state for testing because Mocked provider just check recoil state, not accessing browser in your app.

So, we just add some code to update recoil state.

useView.ts

const [deleteView] = useDeleteViewMutation({
    onCompleted: (data) => {
      const deletedView = data.deleteView?.view;

      if (deletedView) {
        const filteredView = views.filter((view) => view.id !== deletedView.id);
        setViews(filteredView);
      }
    },
    onError: (error) => {
      console.error(error);
    },
  });

Finally, useView.spec.tsx ‘s test is works file.

Conclusion

In real projects, there are many issues because these project’s size is big, the code is complicated, and using a lot of library. In my case, I didn’t find good solution in Websites and I asked ChatGPT to advise me to solve this issue but it doesn’t work. Finally I found this solution when I discussed this issue with my colleague. I hope this article would help you.

Reference

Recoil official: https://recoiljs.org/

React testing library official : https://testing-library.com/docs/

Mocked provider(Apollo): https://www.apollographql.com/docs/react/development-testing/testing/

Thank you for reading!!


#recoil 

Write Recoil Unit Tests with Mocked Providers
1.00 GEEK