이제는 해보자 통합테스트!
로그인 기능이 있는 앱을 테스트한다고 가정하자. 앱은 아래와 같은 구조로 되어있다.
// 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 학습하며 쓺