이벤트 테스트

fireEvent 테스트

검색 함수, 어설션 함수와 달리, 사용자와의 인터랙션 테스트에서 사용되는 것이 fireEvent 입니다. fireEvent를 사용하면 사용자의 인터랙션 시뮬레이션 테스트를 수행할 수 있습니다.

import { render, screen, fireEvent } from '@testing-library/react'
 
import App from './App'
 
describe('App', () => {
  test('App 컴포넌트 렌더링', () => {
    render(<App />);
 
    // 사용자 인터랙션 시뮬레이션 테스트
    fireEvent.change(
      // 스크린에서 textbox 역할 요소를 가져와
      screen.getByRole('textbox'), {
        // 이벤트 대상 값을 'React 테스팅 라이브러리'로 변경합니다.
        target: { value: 'React 테스팅 라이브러리' }
      }
    )
 
  })
})

이벤트 전/후 screen.debug()를 출력해 업데이트 된 UI 코드를 비교해볼 수도 있습니다.

test('UserSearch 컴포넌트 렌더링', async () => {
  render(<UserSearch />)

  await screen.findByText(/로그인 사용자:/)

  // 이벤트 변경 전
  screen.debug()

  fireEvent.change(screen.getByRole('textbox'), {
    target: { value: 'React 테스트 수행' }
  })

  // 이벤트 변경 후
  screen.debug()

})

로그인 이후 또는 이벤트를 통한 UI 업데이트 이후, 대상을 찾아 UI에 존재하는지 또는 문서에 포함되어 있는지 테스트를 수행할 수 있습니다.

test('UserSearch 컴포넌트 렌더링', async () => {
  render(<UserSearch />)

  await screen.findByText(/로그인 사용자:/)

  expect(screen.queryByText(/검색어: React/)).toBeNull()

  fireEvent.change(screen.getByRole('textbox'), {
    target: { value: 'React 테스트 수행' },
  })

  expect(screen.queryByText(/검색어: React/)).toBeInTheDocument()

})

userEvent 테스트

RTL은 fireEvent 외에도 userEvent를 사용한 테스트를 제공합니다. userEvent가 실제 브라우저의 인터랙션에 근접한 방식으로 fireEvent를 대체하여 사용할 수 있습니다. 가능하다면 userEvent를 사용하는 것을 권장합니다. 단, 현 시점에서는 userEventfireEvent의 모든 기능을 대체할 수 없으니 사용에 주의가 필요합니다.

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
 
import UserSearch from './UserSearch';
 
describe('UserSearch 테스트', () => {
  test('UserSearch 컴포넌트 렌더링', async () => {
    render(<UserSearch />);
 
    // 로그인 할 때 까지 대기
    await screen.findByText(/로그인 사용자:/);
 
    expect(screen.queryByText(/검색어: React/)).toBeNull();
 
    // 사용자 이벤트(userEvent) 입력을 대기
    await userEvent.type(screen.getByRole('textbox'), 'React');
 
    expect(screen.getByText(/검색어: React/)).toBeInTheDocument();

  });
});

콜백 핸들러

때로는 React 앱이 아닌, 독립적으로 컴포넌트만 테스트 할 경우가 있습니다. 이런 경우 컴포넌트가 상태를 가지지 않고, 입력(props) 또는 출력(callback)만 있습니다. 콜백 핸들러 테스트 시, Jest 유틸리티를 사용할 수 있습니다.

fireEvent 예 (1회만 콜백 가능)

import Search from './Search'

describe('Search 컴포넌트 테스트', () => {
  test('onChange 콜백 핸들러를 호출합니다.', () => {
    
    // 콜백 핸들러
    const onChange = jest.fn()
  
    // 렌더링
    render(<Search value="" onChange={onChange}>검색</Search>)
  
    // fireEvent를 사용해 <input />의 value를 업데이트 
    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'React 테스팅 라이브러리' }
    })
    
    // 콜백 핸들러 1회 호출된 것으로 어설션
    expect(onChange).toHaveBeenCalledTimes(1)
  })
})

userEvent 예 (모든 키 입력에 대한 업데이트 콜백)

describe('Search 컴포넌트 테스트', () => {
  test('onChange 콜백 핸들러를 호출합니다.', () => {
    
    // 콜백 핸들러
    const onChange = jest.fn()
  
    // 렌더링
    render(<Search value="" onChange={onChange}>검색</Search>)
  
    // userEvent를 사용해 <input />의 value 업데이트 대기
    await userEvent.type(screen.getByRole('textbox'), 'React 테스팅 라이브러리')
    
    // 콜백 핸들러 1회 호출된 것으로 어설션
    expect(onChange).toHaveBeenCalledTimes(10)
  })
})

비동기 테스트

앞서 async await를 사용해 비동기 테스트 방식에 대해 살펴본 바 있습니다. findBy 검색 함수를 사용해 대기 상태에서 대상을 찾는 방법도 살펴봤죠. 이번에는 React에서 데이터를 가져오는(fetch) 테스트 예시를 살펴봅니다. Hacker News API에 데이터를 axios를 사용해 비동기 요청하는 React 컴포넌트 코드를 작성합니다.

import axios from 'axios';

// Hacker News API URL
const URL = 'http://hn.algolia.com/api/v1/search';


class App extends React.Component {  
  state = {
    stories: [],
    error: null
  }

  // 데이터 비동기 요청 핸들러
  handleFetch = async (event) => {
    let result;
    
    try {
      result = await axios.get(`${URL}?query=React`);
      this.setState({ stories: result.data.hits });
    }
    catch (error) {
      this.setState({ error });
    }
  }

  render() {
    const { stories, error } = this.state;

    return (
      <div>
        <button type="button" onClick={this.handleFetch}>스토리 데이터 요청</button>
   
        {error && <span>문제가 발생했습니다...</span>}
   
        <ul>
          {stories.map((story) => (
            <li key={story.objectID}>
              <a href={story.url}>{story.title}</a>
            </li>
          ))}
        </ul>
      </div>
    );
  }
}
 
export default App;

App 컴포넌트 테스트 파일 코드는 다음과 같습니다. App 컴포넌트를 렌더링하기 전에 axios를 목업해야 합니다.

import axios from 'axios';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
 
import App from './App';

// Jest 유틸리티를 사용해 axios 목업
jest.mock('axios');
 
describe('App 테스트', () => {
  test('API에서 스토리를 성공적으로 가져와 표시한 경우', async () => {
    // sotries 배열
    const stories = [
      { objectID: '1', title: 'Hello' },
      { objectID: '2', title: 'React' },
    ];

    // axios GET 요청: Promise 객체 반환
    axios.get.mockImplementationOnce(() =>
      Promise.resolve({ data: { hits: stories } })
    );

    // 앱 렌더링
    render(<App />);

    // 사용자가 버튼 역할 요소를 클릭할 때까지 대기
    await userEvent.click(screen.getByRole('button'));

    // 리스트 아이템을 찾을 때까지 대기
    const items = await screen.findAllByRole('listitem');

    // 리스트 아이템 개수가 2개인지 테스트
    expect(items).toHaveLength(2);
  });
});

데이터 비동기 요청 실패 시뮬레이션 테스트는 다음과 같이 수행합니다.

describe('App', () => {
  test('API에서 스토리를 성공적으로 가져와 표시한 경우', async () => {
    // ...
  });
 
  test('API에서 스토리를 가져오는 데 실패한 경우', async () => {
    
    // Promise 오류 반환
    axios.get.mockImplementationOnce(() =>
      Promise.reject(new Error())
    );
 
    render(<App />);
 
    await userEvent.click(screen.getByRole('button'));
  
    // 오류 메시지 출력될 때까지 대기
    const message = await screen.findByText(/문제가 발생했습니다/);

    // 오류 메시지가 문서에 출력되었는지 어설션
    expect(message).toBeInTheDocument();

  });
});

마지막 테스트는 보다 명확한 방식으로 Promise를 기다리는 방법을 보여 주며, HTML이 표시 되기를 기다리지 않는 경우에도 효과적입니다.

import axios from 'axios';
import { render, screen, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import App from './App';
 
jest.mock('axios');

describe('App 테스트', () => {
  test('API에서 스토리를 성공적으로 가져와 표시한 경우', async () => {
    const stories = [
      { objectID: '1', title: 'Hello' },
      { objectID: '2', title: 'React' },
    ];

    // Promise
    const promise = Promise.resolve({ data: { hits: stories } });

    // axios GET 요청 목업 (Promise 반환)
    axios.get.mockImplementationOnce(() => promise);
 
    render(<App />);

    await userEvent.click(screen.getByRole('button'));

    // act(Promise 반환) 대기
    await act(() => promise);
 
    expect(screen.getAllByRole('listitem')).toHaveLength(2);
  });
 
  test('API에서 스토리를 가져오는 데 실패한 경우', async () => {
    // ...
  });
});

결론은 RTL을 사용해 React의 비동기 액션을 테스트하는 것은 그리 어렵지 않습니다. Jest를 사용해 외부 모듈을 목업한 후, 테스트에서 React 컴포넌트의 데이터를 기다리거나 다시 렌더링합니다.

Last updated