Expo SDK 48にアップデートをしたらReactのバージョンが最新になり、Enzymeがサポートされなくなったので、すべてのテストケースをtesting-library/react-nativeに置き換えた
PR
実装
今までEnzymeのshallowを使用して、テスト対象となるcomponentのスナップショットのみを行っていた。
■ Enzymeでのテストコード src/components/pages/Home/tests/index.test.tsx
import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; import { Home, Props } from '../'; const propsHomeData = (): Props => ({ navigation: { setParams: jest.fn(), navigate: jest.fn(), }, route: { params: {}, }, } as any); describe('components/pages/Home/index.tsx', () => { let wrapper: ShallowWrapper; describe('Home', () => { beforeEach(() => { wrapper = shallow(<Home {...propsHomeData()} />); }); it('正常にrenderすること', () => { expect(wrapper).toMatchSnapshot(); }); }); });
shallowを使用すると直下のテストで参照する直下のコンポーネント以外は自動でMockされるので深く考えずにコンポーネントだけimportしたテストコードを書けばOKだった。 この辺はEnzymeの楽なところではあったが、実際の運用的にはテストというよりは修正による影響検知に近い使い方をしていた。
ただ、上記にも記載した通り、Reactの最新バージョンをEnzymeがサポートしなくなったので、testing-library/react-nativeに置き換えた。
書き換えた結果は以下の通り。
■ react-native-testing-libraryでのテストコード src/components/pages/Home/tests/index.test.tsx
import React from 'react'; import * as Recoil from 'recoil'; import { items } from '__mockData__/item'; import * as useHomeItems from 'hooks/useHomeItems'; import * as client from '@apollo/client'; import { testRenderer } from 'lib/testUtil'; import { screen } from '@testing-library/react-native'; import { Home, Props } from '../'; const propsHomeData = (): Props => ({ navigation: { setParams: jest.fn(), navigate: jest.fn(), setOptions: jest.fn(), addListener: jest.fn(), }, route: { params: {}, }, } as any); describe('components/pages/Home/index.tsx', () => { beforeEach(() => { jest.spyOn(Recoil, 'useRecoilValue').mockImplementation((): any => ({ items: items(), })); jest.spyOn(Recoil, 'useRecoilState').mockImplementation((): any => [ { date: '2020-01-01', }, jest.fn(), ]); jest.spyOn(useHomeItems, 'default').mockImplementation((): any => ({ loading: false, error: null, refetch: jest.fn(), })); jest .spyOn(client, 'useQuery') .mockImplementation((): any => [jest.fn(), { loading: false }]); jest.spyOn(client, 'useMutation').mockImplementation((): any => [ jest.fn(), { loading: false, }, ]); }); describe('Home', () => { it('正常にrenderすること', () => { testRenderer(<Home {...propsHomeData()} />)(); expect(screen.findAllByText('今週のmemoirを確認する')).toBeTruthy(); }); }); });
react-native-testing-libraryを使用すると基本mockしないので実際に動かす時と同様の状態でテストさせる感じになるのでテストコードは長めになる。
API通信の部分は前の記事でも紹介したmswとgraphql-codegen-typescript-mock-dataを使用して実装。
APIのモック部分は以下のようなコードで実装。
import { graphql } from 'msw'; import { ItemDocument, UpdateItemDocument, DeleteItemDocument, ItemsByDateDocument, ItemsInPeriodDocument, RelationshipsDocument, InviteDocument, InviteByCodeDocument, UpdateInviteDocument, CreateInviteDocument, CreateRelationshipRequestDocument, DeleteUserDocument, RelationshipRequestsDocument, AcceptRelationshipRequestDocument, NgRelationshipRequestDocument, UpdateUserDocument, } from 'queries/api/index'; import { aPageInfo, anItem, anItemsInPeriodEdge, aRelationshipEdge, anInvite, aUser, aRelationshipRequest, aRelationshipRequestEdge, } from 'queries/api/mocks'; export const handlers = [ graphql.query(ItemDocument, (req, res, ctx) => { return res( ctx.data({ item: { ...anItem(), id: req.variables.id, }, }) ); }), graphql.mutation(UpdateItemDocument, (req, res, ctx) => { return res( ctx.data({ updateItem: { id: req.variables.input.id, date: req.variables.input.date, }, }) ); }), graphql.mutation(DeleteItemDocument, (req, res, ctx) => { return res( ctx.data({ deleteItem: { id: req.variables.input.id, }, }) ); }), graphql.query(ItemsByDateDocument, (_, res, ctx) => { return res( ctx.data({ itemsByDate: [{ ...anItem(), categoryID: 1 }], }) ); }), graphql.query(ItemsInPeriodDocument, (_, res, ctx) => { return res( ctx.data({ itemsInPeriod: { pageInfo: aPageInfo(), edges: [anItemsInPeriodEdge()], }, }) ); }), graphql.query(RelationshipsDocument, (_, res, ctx) => { return res( ctx.data({ relationships: { pageInfo: aPageInfo(), edges: [aRelationshipEdge()], }, }) ); }), graphql.query(InviteDocument, (_, res, ctx) => { return res( ctx.data({ invite: anInvite(), }) ); }), graphql.query(InviteByCodeDocument, (_, res, ctx) => { return res( ctx.data({ inviteByCode: aUser(), }) ); }), graphql.mutation(UpdateInviteDocument, (_, res, ctx) => { return res( ctx.data({ updateInvite: anInvite(), }) ); }), graphql.mutation(CreateInviteDocument, (_, res, ctx) => { return res( ctx.data({ createInvite: anInvite(), }) ); }), graphql.mutation(CreateRelationshipRequestDocument, (_, res, ctx) => { return res( ctx.data({ createRelationshipRequest: aRelationshipRequest(), }) ); }), graphql.mutation(DeleteUserDocument, (_, res, ctx) => { return res( ctx.data({ deleteUser: aUser(), }) ); }), graphql.query(RelationshipRequestsDocument, (_, res, ctx) => { return res( ctx.data({ relationshipRequests: { pageInfo: aPageInfo(), edges: [aRelationshipRequestEdge()], }, }) ); }), graphql.mutation(AcceptRelationshipRequestDocument, (_, res, ctx) => { return res( ctx.data({ acceptRelationshipRequest: aRelationshipRequest(), }) ); }), graphql.mutation(NgRelationshipRequestDocument, (_, res, ctx) => { return res( ctx.data({ ngRelationshipRequest: aRelationshipRequest(), }) ); }), graphql.mutation(UpdateUserDocument, (_, res, ctx) => { return res( ctx.data({ updateUser: aUser(), }) ); }), ];
上記の方法でほとんどのコンポーネントは置き換えが成功。
一部のコンポーネントで以下のようなエラーが発生した。
console.error Warning: An update to Connected inside a test was not wrapped in act(...). When testing, code that causes React state updates should be wrapped into act(...): act(() => { /* fire events that update state */ }); /* assert on the output */ This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
こちらのエラーはテスト実行中にstate or propsが更新されて再レンダリングがかかっている場合に発生していた。 移行したコード内だと以下のようなコードがある場合に発生。
src/components/pages/Search/Plain.tsx
import React, { memo } from 'react'; import Loading from 'components/atoms/Loading'; import TemplateSearch from 'components/templates/Search/Page'; import { ConnectedType } from './Connected'; type Props = ConnectedType & { loading: boolean; }; const Plain: React.FC<Props> = (props) => { if (props.loading) return <Loading />; return <TemplateSearch users={props.users} onSearch={props.onSearch} />; }; export default memo(Plain);
props.loading=true(APIのレスポンス待ちの状態)の場合はローディング画面を表示させて、falseになると画面が表示させるようなコードがある場合は、実際にテストしたいのはローディング後のコンポーネントなので、props.loading=falseになるのを待ってからテストする必要がある。
なので、以下のようなテストコードにすればOK。
■src/components/pages/Search/tests/index.test.tsx
import React from 'react'; import * as Recoil from 'recoil'; import { testRenderer } from 'lib/testUtil'; import { screen, waitFor, waitForElementToBeRemoved, } from '@testing-library/react-native'; import { user } from '__mockData__/user'; import IndexPage, { Props } from '../'; const propsData = (): Props => ({ navigation: { setParams: jest.fn(), navigate: jest.fn(), }, route: { params: {}, }, }); describe('components/pages/Search/index.tsx', () => { beforeEach(() => { jest.spyOn(Recoil, 'useRecoilValue').mockImplementation((): any => ({ ...user(), })); }); it('正常にrenderすること', async () => { testRenderer(<IndexPage {...propsData()} />)(); await waitForElementToBeRemoved(() => screen.getByTestId('atoms_loading')); await waitFor(async () => { expect(screen.findByText('検索')).toBeTruthy(); }); }); });
以下のコードの部分でローディング画面の表示が消えるのを待って、テストを実行すれば、このエラーは回避できる。
await waitForElementToBeRemoved(() => screen.getByTestId('atoms_loading'));
こんな感じで諸々テストコードを置き換えて、無事Expo Sdk 48のバージョンアップが行えた。