티스토리 뷰

이번 포스팅에서는 React-Testing-Library (줄여서 RTL) 를 활용한 HOC 테스트에 대해 정리해보려 합니다.

 

리액트 컴포넌트 테스트를 위해 보통 Enzyme나 RTL을 사용하는데,
Enzyme가 Implementation Driven Test 방법론을 따르며 어플리케이션이 어떻게 동작하는지에 대해 초점이 맞춰진 반면,

RTL은 Behavior Driven Test (행위 주도 테스트) 방법론을 기반으로 어플리케이션을 사용하는 사용자의 행동에 초점을 맞추고 있습니다.

 

어쨌든, 예시로 들어볼 코드는 아래와 같습니다.

// loginRequired.tsx

import * as React from 'react';
import {useSelector} from 'react-redux';
import Page401 from '../components/errors/Page401';

const loginRequired = (Target: React.ComponentType) => {
  const LoginRequired = (props) => {
    const {access} = useSelector(({session: {access}}) => ({
      access
    }));

    return !access ? (
      <Page401/>
    ) : (
      <Target {...props}/>
    );
  };

  LoginRequired.displayName = 'LoginRequired';
  LoginRequired.getInitialProps = Target.getInitialProps;

  return LoginRequired;
};

export default loginRequired;

loginRequired 로그인이 필요한 페이지들에서 사용되는 HOC 이며, 비로그인 상태의 유저가 페이지에 접근할 시 401 에러 페이지를 보여주는 역할을 합니다.

 

해당 코드에서 저희가 테스트에 중점을 둬야 할 부분은 딱 2가지라고 할 수 있습니다.

 

  1. useSelector hooks를 통해 access를 받아오는지
  2. 받아온 access에 따라 각각 다른 페이지/컴포넌트를 보여주는지

이 두 부분에 중점을 둬서 테스트 코드를 작성해보면, 아래와 같이 나타낼 수 있을 것 같습니다.

// loginRequired.test.tsx // A

import * as React from 'react';
import {useSelector} from 'react-redux';
import {renderHook} from '@testing-library/react-hooks';
import loginRequired from '../loginRequired';
import {render} from '@testing-library/react';

const AccessedComp = () => ( // B
  <div>로그인 된 상태입니다.</div>
);

jest.mock('react-redux', () => ({ // C
  useSelector: jest.fn()
}));

jest.mock('next/router', () => ({ // D
  useRouter: () => ({
    pathname: '/test',
    asPath: '/test?message=hello'
  })
}));

const mockUseSelector = useSelector as jest.Mock; // C

describe('loginRequired 테스트', () => {
  it('access가 null일 때 Page401 반환', () => {
    mockUseSelector.mockImplementation(callback => callback({ // E
      session: {
      	access: null
      } 
    }));

    const {
      result: {
        current: {
          access
        }
      }
    } = renderHook( // F
      () => useSelector(({session: {access}}) => ({
        access
      }))
    );

    expect(access).toBeNull();

    const Comp = loginRequired(AccessedComp);
    const wrapper = render(<Comp/>); // G

    expect(wrapper.getByText('로그인 후 이용 가능한 서비스입니다.')).toBeInTheDocument(); // H
  });

  it('access가 null이 아닐 경우(= 로그인 된 상태일 때) 받은 컴포넌트 그대로 출력', () => {
    mockUseSelector.mockImplementation(callback => callback({
      session: {
        access: 'Access Token'
      }
    }));

    const {
      result: {
        current: {
          access
        }
      }
    } = renderHook(
      () => useSelector(({session: {access}}) => ({
        access
      }))
    );

    expect(access).not.toBeNull();

    const Comp = loginRequired(AccessedComp);
    const wrapper = render(<Comp/>);
    
    expect(wrapper.getByText('로그인 된 상태입니다.')).toBeInTheDocument();
  });
});

 

이제 주석 처리한 부분들에 대해 차근차근 설명해보겠습니다.

 

A

테스트를 위한 컴포넌트도 해당 파일 내에 같이 존재하기 때문에 확장자를 .ts가 아닌 .tsx로 설정하였습니다.

 

B

로그인 된 상태일 때 보여지는 임시 컴포넌트입니다.

해당 테스트의 목적은 access의 상태에 따라 다른 페이지/컴포넌트를 보여주는 것이기 때문에, 컴포넌트의 props나 state, 복잡한 정도 등은 고려할 필요가 없다고 판단하여 위와 같이 간단히 작성하였습니다 :D

 

C

Mocking은 유닛 테스트를 작성할 때, 해당 코드가 의존하는 부분을 가짜로 대체하는 기법을 말합니다.

보통 테스트하려는 코드가 의존하는 부분을 사용자가 직접 생성하기 부담스러운 경우 (Database, API...) 많이 사용하는데, useSelector에도 충분히 활용될 수 있을 것 같아 Mocking 작업을 진행하였습니다.

 

D

아래와 같이 next.js의 useRouter hooks를 사용하는 구성 요소를 테스트 하고자 할 때,

const Comp = () => {
  const {asPath} = useRouter();
  .
  .
  .
};
TypeError: Cannot destructure property 'asPath' of 'undefined' or 'null'.

의 에러를 반환하는 것을 확인할 수 있습니다.

useRouter는 React.useContext를 사용하여 《next server / dist / lib / router-context》 에서 컨텍스트를 사용하기 때문에, 이를 해결하려면

import {RouterContext} from 'next-server/dist/lib/router-context';

const wrapper = render(
  <RouterContenxt.Provider value={router}
    <Comp/>
  </RouterContext.Provider>
);

처럼 Wrapping 하는 것이 중요합니다.

다만, jest.mock 함수를 이용하여 useRouter 자체를 Mocking 하는 작업 역시 가능하기 때문에, 해당 방법을 사용하여 테스트 코드를 작성하였습니다.

 

E

https://jestjs.io/docs/en/mock-function-api#mockfnmockimplementationfn

 

Jest · 🃏 Delightful JavaScript Testing

🃏 Delightful JavaScript Testing

jestjs.io

해당 링크에서 확인하실 수 있듯이, mockImplementation 함수를 사용하면 실제 사용하고자 하는 함수를 재구현하는 것이 가능합니다.

useSelector가 각 케이스 마다 다른 state를 반환해야 하므로 (-> access의 상태) 함수 역시 다르게 구현하였습니다.

 

F

react-hooks에 대한 테스트를 진행할 때 사용하는 "react-hooks-testing-library"에서 제공하는 함수입니다.

renderHook은 hooks의 파라미터와 반환하는 값을 확인하기 위해 사용하여, 더욱 자세한 내용은 아래의 API 문서를 참고하시면 좋을 것 같습니다.

https://react-hooks-testing-library.com/reference/api#renderhook

 

API Reference

Simple and complete React hooks testing utilities that encourage good testing practices.

react-hooks-testing-library.com

 

G

컴포넌트의 렌더링을 하고자 할 때 render 함수를 이용합니다.

해당 함수를 호출할 시 DOM과 관련된 다양한 쿼리들과 container 에 대한 정보를 제공받을 수 있는데, 여기서 container는 해당 컴포넌트의 최상위 DOM을 가리킵니다. (document.body에 추가된 container를 렌더링한다고 합니다.)

 

H

getByText() 함수는 받은 TextMatch를 토대로 일치하는 DOM이 있는지 검색하여 HTMLElement 타입의 값을 반환합니다.

해당 함수를 사용하면 내가 원하는 DOM을 선택할 수 있고, TextMatch는 Matcher 타입이기 때문에 String, RegExp, MatcherFunction 셋 중 어느 타입의 값을 넘기더라도 문제가 없습니다,

 

여기서, MatcherFunction의 타입은 아래와 같습니다.

type MatcherFunction = (content: string, element: HTMLElement) => boolean;

 

따라서, 해당 구문은 access의 존재 여부에 따라 서로 다른 컴포넌트가 Document 내에 존재하는지를 테스트 한다는 의미를 담고 있습니다.

다만, toBeInTheDocument 함수는 jest에서 기본적으로 제공하는 속성이 아니기 때문에, 사용하기 위해서는 jest.config.js 파일에서 따로 jest-dom에 대한 설정을 해야 합니다.

 

아래 링크를 참고하시면 됩니다 :D

https://stackoverflow.com/questions/56547215/react-testing-library-why-is-tobeinthedocument-not-a-function

 

react-testing-library why is toBeInTheDocument() not a function

Can anyone help me please , I am really stuck on this. Here is my code for a tooltip that toggles the css property display: block on MouseOver and on Mouse Out display: none it('should show and ...

stackoverflow.com


이상 RTL로 HOC with React-Hooks 테스트 작성하기에 대한 포스팅이었습니다.

물론 상황에 따라 작성하는 방법은 언제든 달라질 수 있겠지만, 테스트를 하는 목적과 의도가 명확하다면 코드를 작성하는데에 있어서 큰 어려움은 없을 것 같습니다.

수정되어야 하거나 개선되어야 할 부분이 있다면 언제든 코멘트로 남겨주세요 :D

감사합니다! 

'React, Redux...' 카테고리의 다른 글

Jest를 이용한 Reducer 테스트  (0) 2020.03.09
react-waypoint를 이용한 Infinite-scrolling 구현  (0) 2019.11.10
댓글