React Testing (2)

김동하·2024년 1월 1일
0

jest

목록 보기
6/6
post-custom-banner

들어가며

이제는 해보자 통합테스트!

예제

로그인 기능이 있는 앱을 테스트한다고 가정하자. 앱은 아래와 같은 구조로 되어있다.

// index

<AppProviders>
     <App />
</AppProviders>
// AppProviders
function AppProviders({children}) {
  return (
    <ReactQueryConfigProvider config={queryConfig}>
      <Router>
        <AuthProvider>{children}</AuthProvider>
      </Router>
    </ReactQueryConfigProvider>
  )
}
// App
function App() {
  const {user} = useAuth()
  return (
    <React.Suspense fallback={<FullPageSpinner />}>
      {user ? <AuthenticatedApp /> : <UnauthenticatedApp />}
    </React.Suspense>
  )
}
// AuthProvider
function AuthProvider(props) {
  // 로그인, 로그아웃 관련 로직 생략
  
  const value = React.useMemo(
    () => ({user, login, logout, register}),
    [login, logout, register, user],
  )

  if (isLoading || isIdle) {
    return <FullPageSpinner />
  }

  if (isError) {
    return <FullPageErrorFallback error={error} />
  }

  if (isSuccess) {
    return <AuthContext.Provider value={value} {...props} />
  }

  throw new Error(`Unhandled status: ${status}`)
}

앱 랜더링

이제 본격적으로 테스트를 해보자. <App/>을 렌더하고 UI를 확인해보면

test('로그인 성공 후 메인 페이지 진입', async () => {
  render(<App />)
  screen.debug()
})

이런 에러를 볼 수 있다. </AppProvider>가 없기 때문이다. 프로바이더를 맵핑해준다.

test('로그인 성공 후 메인 페이지 진입', async () => {
  render(<App />, {wrapper: AppProviders})
  screen.debug()
})

loading 화면이 나온 걸 볼 수 있다. 이제 loading을 걷어내보자. 테스팅 라이브러리에서 제공하는 waitForElementToBeRemoved()를 사용하면 된다. 위 메서드는 dom 변화 이후를 감지할 때 사용한다.

test('로그인 성공 후 메인 페이지 진입', async () => {
  render(<App />, {wrapper: AppProviders})
  await waitForElementToBeRemoved(() => [
    ...screen.queryAllByLabelText(/loading/i),
    ...screen.queryAllByText(/loading/i),
  ])
  screen.debug()
})

로그인 화면이 잘 나오는 것을 확인할 수 있다.

네트워크 모킹

이제 네트워크 요청을 보내보자. 로그인 로직은 현실 앱의 로직 그대로 사용하면 되는데, 로그인 후 서버로부터 토큰을 받고 local storage에 토큰을 저장하여 서버와 통신하는 기본적인 플로우를 따른다.

네트워크 요청 모킹은 MSW를 사용하면 된다.

test('로그인 성공 후 메인 페이지 진입', async () => {
  const SOME_FAKE_TOKEN = 'SOME_FAKE_TOKEN'
 
 // fake token을 localStorage에 저장한다. 
  window.localStorage.setItem(auth.localStorageKey, SOME_FAKE_TOKEN)

  render(<App />, {wrapper: AppProviders})

  await waitForElementToBeRemoved(() => [
    ...screen.queryAllByLabelText(/loading/i),
    ...screen.queryAllByText(/loading/i),
  ])
  screen.debug()
})

위처럼 fake token을 local storage에 저장하게 되면 현실 앱에서 지정한 401을 에러가 나온다. 네트워크를 통해 실요청을 보내기 때문이다. 그래서 테스트를 위해 네트워트 모킹이 필요하다.

 // fetch를 모킹해서 mock 데이터를 줌
  const originalFetch = window.fetch
  window.fetch = async (url, config) => {
    if (url.endsWith('/bootstrap')) {
      return {
        ok: true,
        json: async () => ({
          user: {
            username: 'dongha',
            token: SOME_FAKE_TOKEN,
          },
          listItems: [],
        }),
      }
    }
    return originalFetch(url, config)
  }

이렇게 fetch 함수를 오버라이딩해서 재정의하고 원하는 데이터를 반환한다.

usename이 화면에 잘 나오는 것을 확인할 수 있다.

이제 특정 페이지에 진입 시 특정 데이터를 렌더링하는지 테스트 해보자. 마찬가지로 네트워크 요청을 모킹하면 된다.


// navigator를 조작하여 특정 페이지 진입을 가정
 window.history.pushState({}, 'Test Page', `/book/${book.id}`)

// fetch를 모킹해서 mock 데이터를 줌
  const originalFetch = window.fetch
  window.fetch = async (url, config) => {
    if (url.endsWith('/bootstrap')) {
      return {
        ok: true,
        json: async () => ({
          user: {
            ...user,
            token: SOME_FAKE_TOKEN,
          },
          listItems: [],
        }),
      }
      // 특정 페이지 요청 시
    } else if (url.endsWith(`/books/${book.id}`)) {
      return {
        ok: true,
        json: async () => ({
          book,
        }),
      }
    }
    return originalFetch(url, config)
  }

만들어둔 목데이터를 사용하여 특정 페이지 진입을 가정하고 테스트하면

데이터가 잘 나온다.

이제 테스트 코드를 작성하면 된다.

test('로그인 성공 후 메인 페이지 진입', async () => {
  const SOME_FAKE_TOKEN = 'SOME_FAKE_TOKEN'
  const user = buildUser()
  const book = buildBook()

  window.localStorage.setItem(auth.localStorageKey, SOME_FAKE_TOKEN)

  window.history.pushState({}, 'Test Page', `/book/${book.id}`)

  // fetch를 모킹해서 mock 데이터를 줌
  const originalFetch = window.fetch
  window.fetch = async (url, config) => {
    if (url.endsWith('/bootstrap')) {
      return {
        ok: true,
        json: async () => ({
          user: {
            ...user,
            token: SOME_FAKE_TOKEN,
          },
          listItems: [],
        }),
      }
      // 특정 페이지 요청 시
    } else if (url.endsWith(`/books/${book.id}`)) {
      return {
        ok: true,
        json: async () => ({
          book,
        }),
      }
    }
    return originalFetch(url, config)
  }

  render(<App />, {wrapper: AppProviders})
  await waitForElementToBeRemoved(() => [
    ...screen.queryAllByLabelText(/loading/i),
    ...screen.queryAllByText(/loading/i),
  ])
  
  // 아래 테스트 코드를 작성한다.
  expect(screen.getByText(book.title)).toBeInTheDocument()
  expect(screen.getByRole('img', {name: /book cover/i})).toHaveAttribute(
    'src',
    book.coverImageUrl,
  )
})

통과 !

** epic react 학습하며 쓺

profile
프론트엔드 개발
post-custom-banner

0개의 댓글