JS, TS

LoginPage로 보는 React 통합테스팅(with. msw, testing-library, vitest)

Mitchell 2023. 10. 28. 13:48

들어가기 전에

이전 포스팅에서는 LoginForm 컴포넌트에 대한 단위테스트(Unit test) 코드를 작성하면서 리팩토링까지 해보았습니다. 단위테스트에 대한 내용은 아래 링크를 통해서 확인하실 수 있으며, 이 글을 이해하는데에도 도움이 됩니다.

testing-library, vitest로 리액트 Unit test 작성하며 리팩토링하기

늘어가는 기능과 화면에 비례해서 버그 또한 늘어나기 마련입니다. 그런데 그 버그를 수정하면 또 다른 곳이 터져버리는 불상사가 눈앞에 펼쳐지게 됩니다. 사람은 실수를 하기 마련이고 그것

mitchellglobal.tistory.com

 
아래 라이브러리들에 대해서 선행적으로 학습하시면 글을 이해하는데 도움이 됩니다.

  • React Query, React Router, React-toastify
  • Vitest, testing-library, msw

통합테스트란 무엇인가

이름 그대로 통합적으로 테스트하는 개념의 테스트입니다. 이전 포스팅에서는 LoginForm 컴포넌트에 대해 한정적으로 테스트를 구현하였었는데요. 그러한 작은 단위를 테스트하는 것을 단위테스트(Unit test)라고 합니다. 통합테스트(Integration test)는 그러한 작은 단위들로 묶여 있는 큰 컴포넌트나 페이지 같이 통합된 형태를 테스트하는 것을 목적으로 합니다. 그게 두 가지로 분류해 볼 수 있겠습니다.

  • 서버와의 통신에 대한 테스트
  • 컴포넌트 간의 상호작용에 대한 테스트

그래서 이번 포스팅에서는 먼저 서버와의 통신에 대한 테스트로 useLogin 커스텀훅을 테스트해보고 마지막으로 LoginForm과 useLogin 등 컴포넌트와 훅 등이 상호작용하는 LoginPage에 대한 테스트를 진행하겠습니다. 테스트를 진행하면서 발생하는 트러블들을 해결하는 과정을 통해 msw 사용법에 대해서도 익히실 수 있습니다.


useLogin 테스트

LoginForm 안에 구현되어 있던 로그인 기능과 관련된 로직을 useLogin 훅으로 분리하도록 리팩토링 했었습니다. React Query의 useMutation을 활용하여 useLogin 훅을 다음과 같이 작성하였습니다.

 
위 처럼 작성된 useLogin이 제대로 작동하는지 통합테스트를 작성하겠습니다. login API를 호출하는 로직이 포함되어 서버와의 통신을 테스트해야 합니다.
 
테스트 케이스 작성은 간단합니다. useLogin이 해야하는 역할에 대해서 정리하고 작성하면 되겠습니다.

  1. 로그인 API를 호출한다.
  2. 로그인 진행중에 대해 처리한다.
  3. 로그인 성공에 대해 처리한다.
  4. 로그인 실패에 대해 처리한다.
describe('Hook: useLogin', () => {
    it('로그인을 시도하면 로딩 상태가 된다.', () => {

    })
    
    it('로그인이 성공하면 onSuccess가 실행된다.', () => {

    })

    it('로그인이 실패하면 onError가 실행된다.', () => {

    })
})

 

공통 Render 함수

매 테스트케이스마다 사용할 공통 렌더함수를 먼저 정의하겠습니다. useLogin 로직에 따라 다음 사항을 고려하여 렌더 함수를 정의해야합니다.

  • Hook 전용으로 테스트하는 renderHook을 사용하기
  • useMutation을 위해 QueryClientProvider로 render wrapping하기
  • useMutation에 등록할 success, error side effect는 mocking 하기

 

실패하는 테스트 코드

공통 render 함수를 사용하여 매 케이스마다 테스트 코드를 아래와 같이 작성하고 테스트를 실행하겠습니다.

역시나 한번에 성공하는 법은 없습니다.

useLogin 첫번째 테스트 결과

희안하게 로딩 상태에 대해서는 성공을 했는데 왜 그럴까요? useLogin에서는 실제 서버의 login API를 호출하여 테스트 하고 있기 때문입니다. 테스트용으로 임의의 계정과 비밀번호로 시도했기 때문에 로그인 성공하는 테스트 단계에서 에러가 발생해 실패하였을 것입니다.
이렇게 실서버와의 통신으로 테스트 하는 과정은 E2E(End to End test) 단계에서 진행하기 때문에 통합테스트 단계에는 올바른 방법의 테스트가 아닙니다. 따라서 저희는 login API를 Mocking 해야하겠습니다.
 

MSW: REST API Mock

MSW(Mock Service Worker)는 API의 호출을 Intercept하고 응답을 원하는대로 Mocking 할 수 있는 방법을 제공합니다. 아래의 공식문서를 통해서 자세한 사용법을 참조하시면 좋습니다.

Mock Service Worker

API mocking library for browser and Node.js

mswjs.io

 
1) Login API Mocking 하기
실제 API의 응답처럼 약간의 지연을 주는 상황에서 성공과 실패 함수를 만들고, 성공함수는 handlers에 추가합니다.

 
2) Mock Server 셋팅하기

import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)

 
3) setup에 Server 실행 코드 추가하기
전체 테스트를 시작하기 전에 서버를 실행하고, 매 케이스마다 초기화하고, 종료시 서버를 종료하도록 setup에 설정합니다.

import '@testing-library/jest-dom'
import { server } from './mocks/server'
import { cleanup } from '@testing-library/react'

// onUnhandledRequest를 'bypass'로 설정하면
// handler에 등록되지 않은 url 호출에 대한 로그를 보여주지 않습니다.
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }))

afterEach(() => {
    cleanup()
    server.resetHandlers()
})

afterAll(() => server.close())

 

성공하는 테스트 코드

msw 셋팅을 마쳤으니 다시 테스트를 실행해보겠습니다.

로그인 실패 케이스의 FAIL

이번에는 로그인 실패에 대한 케이스만 FAIL이 났고 나머지는 PASS하였습니다. 왜냐하면 msw mock server에 셋팅된 handlers 안에는 "성공"하는 mock API만 추가되었기 때문입니다. 따라서 로그린 실패 케이스에서만 서버가 실패하는 응답을 보내줄 수 있도록 추가적인 설정이 필요합니다.

  • msw의 server.use를 사용하기

이제 모든 설정은 끝났습니다. 테스트를 실행하면 드디어 All Pass를 받을 수 있습니다.
이로서 useLogin의 통합테스트 작성은 완료하였습니다.

useLogin.test.tsx All Pass

LoginPage 테스트

마지막으로 LoginForm에 사용자 정보를 입력하고, useLogin의 기능을 사용하여 로그인이 처리되는 LoginPage에 대한 테스트를 진행하겠습니다.  테스트케이스는 매우 간단합니다. 로그인이 될 때와 안 될 때만 보면 되니까요.

describe('<LoginPage/>', () => {
    it('로그인을 성공하면 "/"로 이동한다.', () => {
    
    })

    it('로그인을 실패하면 에러 토스트메시지가 나타난다.', () => {
    
    })
})

 

공통 Render 함수

여기에서도 케이스마다 공통으로 사용할 렌더함수부터 정의하겠습니다. 다음 사항에 대해서 고려하세요.

  • 페이지 라우팅은 React Router로 하고 있습니다. 따라서 테스트시에는 createMemoryRouter로 Routes를 정의해두어야 합니다.
  • login API를 React Query의 useMutation로 호출하고 있기 때문에 QueryClientProvider로 컴포넌트를 랩핑 해야합니다.

 

실패하는 테스트 코드

앞서 작성한 테스트케이스에 테스크 코드를 작성합니다.

  • 페이지의 현재 경로를 체크하기 위해서 공통 렌더함수에서 전달하고 있는 memoryRouter를 사용합니다.
  • 에러메시지는 React-Toastify를 사용하였습니다.

실패하는 테스트 코드의 결과는 역시 실패입니다.

 

성공하는 테스트 코드

테스트를 PASS시키기 위해 LoginPage를 구현해보겠습니다. LoginForm 컴포넌트와 useLogin 훅을 사용하고 성공 실패에 맞는 사이드이펙트를 구현하겠습니다.

다시한번 테스트를 실행하면 성공하는 결과를 얻을 수 있습니다.

테스트가 완료된 로그인 페이지

LoginForm, useLogin, LoginPage에 대해 단위테스트, 통합테스트를 진행하면서 동시에 테스트 가능한 코드로 리팩토링도 함께 진행하였습니다. 이번 포스팅의 핵심은 Server와의 HTTP 통신을 msw를 통해서 Mocking 해보는 것이였는데요.
 
다음 포스팅은 실시간 통신에 대해 SocketIO로 Mocking 하면서 해당 페이지를 단위테스트, 통합테스트를 해보려 합니다.