> For the complete documentation index, see [llms.txt](https://taewoongs-organization.gitbook.io/jtwjs-dev-wiki/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://taewoongs-organization.gitbook.io/jtwjs-dev-wiki/dev_road/week-5/react-testing-library.md).

# React Testing Library

## React Testing Library

> 컴포넌트를 **내부 구현사항에 의존하지 않고 사용자 관점에서 테스트**를 작성할 수 있게 도와주는 라이브러리

```bash
npm i -D @testing-library/react
```

{% hint style="info" %}
**Jest 만으로도 UI 테스트를 할 수 있다. with jsdom**

**jsdom**: 브라우저에서 실행되는 JavaScript를 모방하여 Node.js 환경에서 DOM을 제공하는 라이브러리

Jest에서 제공하는 jsdom을 통해 화면단이 어떻게 보여지는지 테스트할 수 있다.&#x20;

실제 돔을 렌더링해서 테스트하는 것이 아니라 코드상에서 가상으로 돔 구조를 만들어 테스트하는 것이기 때문에 테스트 실행속도가 빠르다.

Jest만으로 UI 테스트가 가능하지만, 사용자 관점에서 더욱 직관적인 테스트를 작성하기 위해 React Testing Library를 사용한다.
{% endhint %}

### 장점

컴포넌트 내부에서 어떤 코드들을 가지고 있는지 어떤 CSS 클래스를 사용하는지 내부 구현사항에 의존하지 않고 화면상에 UI가 어떤 텍스트를 가지고 있고 보이는지 안보이는지 보다 외부적인 사용자 관점에서 테스트를 작성할 수 있게 도와준다.

{% hint style="info" %}
React Testing Library는 shallow rendering을 지원하지 않기 때문에, 컴포넌트를 테스트할 때 해당 컴포넌트의 모든 자식 컴포넌트를 함께 렌더링한다. 따라서 Jest mocking을 활용하여 자식 컴포넌트들의 의존성까지 모두 처리해주어야 한다
{% endhint %}

### API

#### Render

document.body 하위로 컴포넌트를 렌더시킨다.

```tsx
import {render, screen} from '@testing-library/react'

describe('Component', () => {
  it('render correctly', () => {
    render(<Component />);
    // 김아무개를 포함하는 텍스트가 UI 상에 표시되는지 검증
    expect(screen.getByText(/김아무개/)).toBeInTheDocument();
  })
})
```

#### waitFor

> 비동기적으로 처리되는 상황을 기다릴 때 사용된다.
>
> 즉, 사용자의 관점에서 화면이 제대로 렌더링되었는지 확인하기 위해 특정 요소가 화면에 보여질 때까지 기다린다

기본적으로 최대 5초까지 비동기 작업을 기다리고 `options` 매개변수를 사용하여 이 시간을 조절할 수 있다.

```typescript
await waitFor(() => ..., { timeout: 8000 }); 
```

* **waitFor()** → 일반적으로 요소가 화면에 나타날 때까지 기다리는 데 사용됩
* **waitForElementToBeRemoved()** → 요소가 화면에서 사라질 때 까지 기다리는 데 사용됨

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

const context = describe;

describe('Component', () => {
  it('render correctly', async () => { 
    render(<Component />);
    
    // 로딩 텍스트가 사라질 때 까지 기다림  
    await waitForElementToBeRemoved(screen.queryByText('Loading...'));
    
    // 김아무개를 포함하는 텍스트가 UI 상에 표시될 때까지 기다림
    await wailtFor(() => expect(screen.getByText(/김아무개/)).toBeInTheDocument());
    // findBy로 사용하면 waitFor 없이도 사용 가능하다.
    await screen.findByText(/김아무개/)
  })
})
```

{% hint style="info" %}
**`findBy` 쿼리는 `waitFor` 함수와 동일한 역할**을 한다.

**findBy** ->  DOM 요소가 화면에 나타날 때까지 자동으로 대기하며, 요소를 찾으면 해당 요소를 반환

**waitFor** -> 콜백 함수를 사용하여 비동기 작업의 완료 여부를 확인
{% endhint %}

### Queries

테스팅 라이브러리에서 제공하는 `screen` 객체를 사용하면, 렌더링된 dom 내부에서 특정 요소를 쉽게 찾을 수 있다.

`screen` 객체는 `getBy`, `queryBy`, `getAllBy`, `queryAllBy`, `findBy`, `findAllBy` 등의 함수를 제공하여 텍스트, 역할 등을 기준으로 요소를 선택할 수 있다.

* **getBy** → 찾는 요소가 없는 경우 에러를 던진다.
* **findBy** → `getBy` 비동기 버전
* **queryBy** → 찾는 요소가 없는 경우 null을 반환한다.

{% hint style="info" %}
찾고자 하는 요소가 여러 개인 경우 **\*\*AllBy**(ex: getAllBy)를 사용하면 배열 형태로 받아온다.
{% endhint %}

#### 자주쓰는 쿼리

* **getByText** → 지정한 텍스트를 포함하는 요소 선택
* **getByRole** → 지정한 역할(role)을 가진 요소 선택
* **getByLabelText** → 지정한 `aria-label` 가진 요소 선택&#x20;
* **getByDisplayValue** → 지정한 값을 가진 요소 선택 (input)<br>

## User Event

> 테스팅 라이브러리에서 제공하는 이벤트 처리 API
>
> 실제 사용자들의 동작을 시뮬레이션하여 테스트를 할 수 있도록 도와준다.&#x20;

### fireEvent

> 이벤트를 직접 발생시키는 함수

* 이벤트를 발생시키는 코드를 직접 작성한다.

```typescript
import { render, screen, fireEvent } from '@testing-library/react';

const context = describe;

describe('Input Component', () => {
  const text = 'woong';
  const setText = jest.fn();
  
  afterEach(() =>{
    setText.mockClear();
  })
  
  function renderTextField() {
    rneder(
      <TextField 
        label="Name"
        placeholder="Input your name"
        text={text}
        setText={setText}
      />
    )
  }
  context('when user types text', () => {
    it('calls the change handler', () => {
      renderTextField();
      
      fireEvent.change(screen.getByLabelText('Name'), {
        target: {
          value: 'New Name',
        },
      });
      
      expect(setText).toBeCalledWith('New Name');
    })
  })  
})
```

### userEvent

> 실제 사용자가 수행하는 것과 유사한 방식으로 이벤트를 발생 시키는 함수

```bash
import userEvent from '@testing-library/user-event';
```

* 사용자가 직접 인터렉션한듯이 효과를 준다.
* 보다 직관적이고 복잡한 동작도 간편하게 작성할 수 있다.

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

const context = describe;

describe('Input Component', () => {
  const text = 'woong';
  const setText = jest.fn();
  
  afterEach(() =>{
    setText.mockClear();
  })
  
  function renderTextField() {
    rneder(
      <TextField 
        label="Name"
        placeholder="Input your name"
        text={text}
        setText={setText}
      />
    )
  }
  context('when user types text', () => {
    it('calls the change handler', () => {
      renderTextField();

      userEvent.type(screen.getByLabelText('Name'), 'New Name');
      
      expect(setText).toBeCalledWith('New Name');
    })
  })  
})
```

## Mocking

> 실제 코드에서 쓰이는 외부 의존성 모듈을 대체할때 쓰이는 것으로 테스트의 **격리성**(isolation)을 확보할 수 있다.
>
> "테스트를 독립시키기 위해 의존성을 개발자가 컨트롤하고 검사할 수 있는 오브젝트로 변환하는 테크닉"

### Mock

> 구현사항이 없고 내가 원하는 부분만 가짜로 흉내내는 용도로 사용

#### jest.fn()

> 테스트 코드에서 **함수**를 목(mock) 함수로 대체할 때 사용

실제 함수처럼 동작하며, 호출된 내용을 기록하고 반환값을 지정할 수 있다.

```typescript
//#1. 선언과 동시에 반환값 지정
const mockFn = jest.fn(() => 'mock'); 

const mockFn = jest.fn();
//#2. 각 테스트마다 다르게 반환값을 지정해주고 싶을때 사용
mockFn.mockImplementation(() => 'mock');
```

{% hint style="info" %}
Mock Function과 자주 쓰이는 **Matcher**

* **toHaveBeenCalledTimes** → 호출된 횟수
* **toHaveBeenCalledWith** → 호출된 함수의 인자
  {% endhint %}

#### jest.mock()

> 테스트 코드에서 **모듈**을 목킹(mocking)하고 그 안에 함수와 객체를 대체할 때 사용

모듈이 exports하는 모든 것들을 Mocking한다.

```typescript
import UserService from '../user_service';
import UserClient from '../user_client';

jest.mock('../user_client', () => jest.fn().mockImplementation(() => ({
  login: jest.fn(async (id: string, password: string) => true),
})));

const context = describe;

describe('user service', () => {
  let userService: UserService;
  let client: UserClient;

  beforeEach(() => {
    client = new UserClient();
    userService = new UserService(client);
  });

  context('when a successful login', () => {
    it('isLogedIn should be true', async () => {
      await userService.login('tv', 'dd');
        expect(userService.isLogedIn).toBeTruthy();
        expect(client.login).toHaveBeenCalled();
        expect(client.login).toHaveBeenCalledWith('tv', 'dd');
      });
  });
});

```

#### jest.mocked() 활용 ver

> 목킹할 모듈의 특정 부분만 목킹할때 사용

```typescript
import UserService from '../user_service';
import UserClient from '../user_client';

jest.mock('../user_client');

const context = describe;

describe('user service', () => {
  const login = jest.fn(async (id: string, password: string) => true);
  let userService: UserService;

  jest.mocked(UserClient).mockImplementation(() => ({login}));

  beforeEach(() => {
    userService = new UserService(new UserClient());
  });

  context('when a successful login', () => {
    it('isLogedIn should be true', async () => {
      await userService.login('tv', 'dd');
        expect(userService.isLogedIn).toBeTruthy();
        expect(login).toHaveBeenCalled();
        expect(login).toHaveBeenCalledWith('tv', 'dd');
      });
  });
});

```

### Mock Clear

> 각 테스트는 독립적으로 실행되고 다른 테스트에 영향을 끼치지 않아야 한다.

```javascript
{
  clearMocks: true,
  resetMocks: true,
  restoreMocks: true,
  resetModules: true // 상황에 따라 다릅니다.
}
```

#### jest.clearAllMocks()

> 모듈 내의 모든 mock 함수의 호출 내역과 호출 횟수를 초기화

#### jest.resetAllMocks()

> 모듈 내의 모든 mock 함수의 호출 내역, 호출 횟수 + 구현을 초기화

#### jest.restoreAllMock()

> mock 함수를 실제 구현으로 복원

#### jest.resetModules()

> 완전히 분리된 환경에서 실행되도록 모든 모듈을 초기화

### Stub

> 기존에 쓰이는 찐 인터페이스를 다 충족하는 실제로 구현된 코드이며 대체 가능한 것

```typescript
// 찐 인터페이스
export default class ProductClient {
  async fetchItems(): Promise<Item[]> {
    return fetch('http://example.com/login/id+password').then(async response =>
      response.json(),
    );
  }
}
// Stub
export default class StubProductClient {
  async fetchItems(): Promise<Item[]> {
    return [
      {key: '💪🏻', available: true},
      {key: '👋', available: false},
    ];
  }
}

// Test Code
import ProductService from '../product_service';
import StubProductClient from './stub_product_service';

describe('Product Service', () => {
  let service: ProductService;

  beforeEach(() => { // 찐 인터페이스 대신 Stub를 주입해서 사
    service = new ProductService(new StubProductClient());
  });

  it('should filter out only available items', async () => {
    const items = await service.fetchAvailableItems();
    expect(items.length).toBe(1);
    expect(items).toEqual([{
      key: '💪🏻',
      available: true,
     }]);
   });
});
```

## Test fixture

> 테스트를 수행하는데 필요한 환경을 설정하는 코드 블록
>
> 즉, 테스트에서 특정 객체, 모듈, 함수 등을 재사용하기 위해 필요한 데이터나 설정등을 정의하는 코드이다.

테스트는 일관된 결과를 얻기 위해 매번 같은 환경에서 실행되어야 하며, 이를 위해 `Test fxitures` 를 사용하여 환경을 설정해야 한다.
