> For the complete documentation index, see [llms.txt](https://yamoo9.gitbook.io/learning-react-app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://yamoo9.gitbook.io/learning-react-app/tip-and-references/react-testing-library/event-test.md).

# 이벤트 테스트

## fireEvent 테스트 <a href="#fire-event-test" id="fire-event-test"></a>

[검색 함수](/learning-react-app/tip-and-references/react-testing-library/get-by-element.md), [어설션 함수](/learning-react-app/tip-and-references/react-testing-library/jest-dom-test.md#assertive)와 달리, 사용자와의 인터랙션 테스트에서 사용되는 것이 `fireEvent` 입니다. `fireEvent`를 사용하면 사용자의 인터랙션 시뮬레이션 테스트를 수행할 수 있습니다.

```jsx
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()`](/learning-react-app/tip-and-references/react-testing-library/react-component-test.md#screen-debug)를 출력해 업데이트 된 UI 코드를 비교해볼 수도 있습니다.

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

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

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

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

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

})
```

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

```jsx
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 테스트 <a href="#user-event-test" id="user-event-test"></a>

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

```jsx
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();

  });
});
```

## 콜백 핸들러 <a href="#callback-handler" id="callback-handler"></a>

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

**fireEvent 예** (1회만 콜백 가능)

```jsx
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 예** (모든 키 입력에 대한 업데이트 콜백)

```jsx
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)
  })
})
```

## 비동기 테스트 <a href="#async-test" id="async-test"></a>

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

```jsx
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를 목업해야 합니다.

```jsx
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);
  });
});
```

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

```jsx
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이 표시 되기를 기다리지 않는 경우에도 효과적입니다.

```jsx
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 컴포넌트의 데이터를 기다리거나 다시 렌더링합니다.
