Please enable JavaScript to view the comments powered by Disqus.

동적으로 import된 컴포넌트의 테스트

React.lazy를 사용한 컴포넌트의 테스트 케이스가 이유없이 실패할 때

앱을 개발할 때 번들 파일의 사이즈가 커지면 로딩 속도를 개선하기 위해 코드를 분할할 필요가 생긴다. React에서는 react-loadable을 사용하는 방법도 있지만, React.lazy를 사용해서 간단하게 동적 import를 사용할 수 있다.

import React from 'react';

const Login = React.lazy(() => import('page/Login'));

const App = () => {
	return (
		<React.Suspense fallback={<div>loading...</div>}>
			<Login />
    </React.Suspense>
	)
}

다만 이렇게 구현한 컴포넌트를 테스트할 때는 주의가 필요하다. 테스트 코드는 비동기적으로 가져온 컴포넌트가 완전히 렌더링된 후에 실행될 수도 있고, 아닐 수도 있기 때문이다. 비동기 컴포넌트가 렌더링되었음을 확실히 보장하지 않으면 테스트도 성공을 보장할 수 없다.

동적 import로 가져온 컴포넌트 렌더링 확인

Login 컴포넌트가 아래와 같이 구현되어 있다고 가정하자.

// Login.tsx
import React from 'react';

export default function Login(): JSX.Element {
  return (
    <div>
      <div>
        <label htmlFor="">User Id</label>
        <input type="text" />
      </div>
      <div>
        <label htmlFor="">Password</label>
        <input type="password" />
      </div>
    </div>
  );
}

그리고 Login 컴포넌트를 마운트한 App 컴포넌트 테스트 코드를 작성한다.

// App.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

describe('App component test', () => {
  const arrangeRender = () => {
    render(
      <App/>
    );
  };

  test('아이디, 패스워드 입력을 제공한다.', () => {
    arrangeRender();
    expect(screen.getByRole('textbox', { name: /User Id/i })).toBeInTheDocument()
    expect(screen.getByRole('textbox', { name: /Password/i })).toBeInTheDocument()
  });
});

위의 테스트 코드는 잘 동작할 것 같다. 하지만 실패할 가능성이 있는 테스트 코드다. 테스트 케이스가 실행되기 전에 동적으로 가져온 컴포넌트가 렌더링되지 않았다면 /User Id/i로 찾을 수 있는 textbox 요소가 없다면서 테스트 케이스가 실패할 것이다.

비동기 컴포넌트의 렌더링은 testing-library를 사용해서 확인 가능하다.

export default function Login(): JSX.Element {
  return (
    <div data-testid="loginComponent">
				...
    </div>
  );
}

여기에서는 컴포넌트에 테스트 아이디 loginComponent를 추가해서 컴포넌트의 렌더링을 확인하도록 한다.

import { render, screen, waitFor } from '@testing-library/react';

describe('App component test', () => {
  const arrangeRender = async () => {
    render(
      <App/>
    );

		// waitFor 인스턴스는 Promise이므로 resolve될때까지 기다린다.
		await waitFor(() => {
      expect(screen.getByTestId(/loginComponent/i)).toBeInTheDocument();
    })
  };

  test('아이디, 패스워드 입력을 제공한다.', async () => {
		await arrangeRender();

    expect(screen.getByRole('textbox', { name: /User Id/i })).toBeInTheDocument()
    expect(screen.getByRole('textbox', { name: /Password/i })).toBeInTheDocument()
  });
});

waitFor 함수를 사용해서 Login 컴폰넌트 안에 있는 테스트 아이디가 확인될 때까지 기다리게 했다. 이렇게 작성하면 테스트 케이스에서 expect 메소드가 호출되기 전에 비동기 컴포넌트가 렌더링됨을 확실히 보장할 수 있다. 이렇게 테스트 아이디를 추가해서 확인해도 되고, 이미 존재하는 라벨 텍스트로 확인해도 된다.

테스트 케이스가 실패할 때는 비동기 코드를 의심하자

mokcing한 API가 제대로 동작하지 않은 케이스, intersection observer 때문에 리스트가 일부만 렌더링된 케이스, 비동기 컴포넌트 때문에 DOM 요소를 찾지 못하는 케이스, 단순한 내 실수(🙀) 등 테스트 케이스가 실패하는 원인은 무척 다양하다. 게다가 렌더링 결과를 브라우저로 바로 확인이 불가능하고 screen.debug() 같은 함수를 사용해서 텍스트로만 문제를 파악해야 하기 때문에 원인을 파악하는 것이 더 어려운 것 같다. 그래도 가장 많은 원인은 이 글에서 살펴본 것처럼 비동기적으로 실행되는 코드에 의한 것이었다.