# React Testing Pattern

{% hint style="success" %}
[Common mistakes with React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library)
{% endhint %}

## 설정과 해지를 통한 환경의 격리

> 유닛테스트를 수행할 때 각 테스트는 독립적으로 실행되어야 하고 외부 환경에 영향을 받지 않고 항상 동일한 환경에서 순수한 모듈의 동작만을 검증해야 한다.

### **설정(Setup)**

> 테스트가 시작되기 전에 수행되며 동일한 환경을 보장하기 위해 변수 설정 및 mock을 주입한다.

* jest에서 제공하는 `beforeEach` `beforeAll` 등의 함수들을 사용해서 미리 설정하고자 하는 값들을 정의하거나 모킹한다.
* **beforeAll()** → 동일한 스코프 내(자식포함) 테스트들이 시작되기 전 가장 먼저 한번 수행
* **beforeEach()** → 동일 스코프 내(자식포함) 각각의 테스트 함수가 실행되기 이전에 수행

### **해지(Teardown)**

> 테스트의 종료 후 마무리, 이후 테스트에 영향이 없도록 세팅

* **AfterEach()** → 동일 스코프 내(자식포함) 각각의 테스트 함수가 종료된 이후 수행
* **AfterAll()** → 동일한 스코프 내(자식포함) 테스트들이 모두 수행된 후 마지막에 한번 수행&#x20;

{% hint style="info" %}
parent Scope 먼저 수행되고 그 다음 child Scope가 나중에 수행된다.
{% endhint %}

{% hint style="warning" %}
React Testing Library 함수 내부에서 이미 설정/해지를 진행하고 있어 일반적으로 별도의 설정을 처리하지 않아도 되지만 테스트의 독립적 수행 보장을 위한 개념을 이해해야 하고 별도로 설정/해지가 필요한 코드가 존재
{% endhint %}

#### Timer를 사용하는 경우

* setTimeout, setInterval, clearTimeout, clearInterval
* Timer를 사용하여 몇초 뒤에 이벤트를 실행하는 등의 코드들은 테스트가 Timer가 종료될 때까지 기다려야 하며 이로 인해 테스트는 느려지고 예측 불가능해진다.
* 이런 경우 jest에서 제공하는 `fake timer` 를 사용하는것이 좋다.

```javascript
beforeEach(() => {
  // timer를 mocking
  jest.useFakeTimers()
})
```

```javascript
afterEach(() => {
  jest.runOnlyPendingTimers(); // 수행되지 않았던 모든 타이머 수행 
  jest.useRealTimers(); // 실제 타이머를 사용하게끔 timer mocking 제거 
})
```

## [Screen Query](https://testing-library.com/docs/queries/about/#priority)

{% hint style="success" %}
**테스트는 사용자들이 소프트웨어를 사용하는 방식을 모방해야한다.**
{% endhint %}

**Queries Accessible to Everyone**&#x20;

> 모두가 접근할 수 있는 쿼리를 사용하는것이 좋다. (화면을 보고있는 사용자든, 스크린리더기를 사용하는 사용자든)

* **`getByRole`**: 접근성 트리에 노출된 모든 요소를 쿼리하는데 사용됨
  * 거의 모든 경우에 해당 옵션이 가장 선호됨,
  * 이 옵션으로 얻을 수 없는 것들은 UI에 접근할 수 없는 것일 수 있음
* **`getByLabelText`**: 폼 필드에 적합, 레이블 텍스트를 사용하여 요소를 찾음
* **`getByPlaceholderText`**:레이블을 대체 할 수 없지만, 다른 대안보다는 낫다.
* **`getByText`**: 폼 외의 텍스트 컨텐츠를 찾는 주요 방법, `div`, `span`, `p` 비대화형 요소를 찾는데 사용됨
* **`getByDisplayValue`** : 폼 요소의 현재 값으로 요소를 찾는데 유용

#### **Semantic Queries**&#x20;

> 의미론적인 쿼리, HTML5 & ARIA 선택기, 해당 속성들은 브라우저와 보조 기술(스크린리더기)에 따라 크게 달라지므로 일관성이 조금 떨어짐,  -> 속성이 표시되는 방식이 일관되지 않다! \
> **사용자들이 소프트웨어와 상호작용하는 것과 동일한 방식으로 테스트가 진행되고 있는지 알 수 없다.**

* **`getByAltText`**:  요소에 대체 텍스트(이미지, 영역, 입력 및 모든 사용자 정의 요소)가 지원되는 요소인 경우 찾기 가능
* **`getByTitle`**:   스크린리더기에서 일관되게 읽히지 않고 시각 장애가 있는 사용자에게는 기본적으로 표시되지 않음

#### Test Ids

> 최후의 수단&#x20;
>
> 사용자들이 Test ID와 상호작용 할 일은 절대 없다.

* **`getByTestId`**: 사용자 입장에서 보거나 들을 수 없는 요소로 역할이나 텍스트별로 일치시킬 수 없거나 의미가 없는 경우(예: 텍스트가 동적인 경우)에만 이 방법을 사용

#### [Browser Extension](https://chromewebstore.google.com/detail/testing-playground/hejbmebodbijjdhflfknehhcgaklhano?pli=1)

> 요소를 찾는데 가장 적합한 쿼리를 찾아주는 크롬 익스텐션

## 인터렉션 단위의 검증

### AAA 패턴 (Arrange, Act, Assert)

> 테스트 코드는 3단계로 나누어 해당 테스트가 무엇을 하고 어떤 목적을 갖는지 보다 명확하게 이해할 수 있도록 구성&#x20;

* **Arrange (준비)**
  * 테스트 실행 중 필요한 것들을 준비
  * `BeforeEach`, `afterEach`
  * Render Components
  * Mocking,  State, Service 등
* **Act (실행)**
  * 테스트 코드 실행
  * User의 Action (Click, State change 등)
  * RTL은 act() wrapping 불필요
* **Assert (단언)**
  * 실행한 코드 결과의 동작을 검증
  * Text, State, Child count 등 &#x20;

### 복잡한 컴포넌트라면

* 컴포넌트가 복잡하다면 인터렉션 단위 검증에서 컴포넌트의 목적과 동작에 맞추어 테스트를 컴포넌트별 동작별로 분리해서 진행
* Ex) List - Item 관계 (Container - Child)
  * List 컴포넌트 → list 조건에 따라 list count가 잘되는지 파악
  * Item 컴포넌트 → child component들의 property, value 값을 가지고 올바르게 render 되는지&#x20;

## 테스트 유틸

<https://testing-library.com/docs/react-testing-library/setup/>

## 렌더링 검증

> Component를 Render하고 document.body에 붙인다.

### 반환값

* **query objects** → render 된 DOM에서 특정 element를 조회할 때 사용 === **`screen`** 사용
* **container** → React 컴포넌트가 render 된 DOM node, div
* **baseElement** → container가 들어있는 DOM node, default - body
* **rerender** → props를 변경하여 component update 처리할 때 사용&#x20;
* **unmount** → 메모리 유수를 유발하는 event handler를 남겨두는 테스트 시도 시 유용, 또는 컴포넌트가 unmount 되는 시점을 테스트하고 싶을 때 사용

## RenderHook

> 함수형 컴포넌트에서 사용되는 Hook을 테스트하는 함수

```javascript
import { renderHook } from '@testing-library/react';
```

* React v18 부터 RTL/react에 기본적으로 포함되어짐
* 리엑트 렌더링 메커니즘을 따르는 단순한 함수기 때문에 독립적인 단위 테스트를 작성하기 적합함
* renderHook 함수 안에 콜백으로 hook을 넣고 테스트
* return object의 reulst 속성을 통해 훅의 반환값을 확인할 수 있다.
* `result.current` → 콜백함수가 마지막으로 리턴한 값
* `current` → 커스텀 훅은 여러번 호출 가능하기 때문에 마지막 실행값인 current를  참조

```jsx
// 잘못된 테스트 (훅은 함수형 컴포넌트 안에서 실행되어야 한다.)
test('Check initial value of isOpen is false', () => {
  const result = useOpen();
  expect(result.open).toBe(false);
})

// 'renderHook'을 사용하지 않고 훅 테스트하는 방법
test('Check initial value of isOpen is false', () => {
  let useOpenRes = {} as ReturnType<typeof useOpen>;
  const Wrapper = () => {
    useOpenRes = useOpen();
    return <></>;
  }
  
  render(<Wrapper />);
  
  expect(useOpenRes.open).toBe(false);
})

// Better
test('Check initial value of isOpen is false', () => {
  const { result } = renderHook(() => useOpen());
  expect(result.current.open).toBe(false);
})

test('Check isOpen updated when setOpen is called', () => {
  const { result } = renderHook(() => useOpen());
  expect(result.current.open).toBe(false);
  
  act(() => result.current.setOpen(true));
  
  expect(result.current.open).toBe(true);
})
```

{% hint style="info" %}
`act()`

* 상호 작용(렌더링, 이펙트 등)을 함께 그룹화하고 실행해 렌더링과 업데이트가 실제 앱이 동작하는 것과 유사한 방식으로 동작
* 테스트 환경에서 `act()`를 사용하면 가상돔(jsdom)에 제데로 반영되었다는 가정하에 테스트 가능해짐
* **컴포넌트를 렌더링한 뒤 업데이트 하는 코드의 결과를 검증하고 싶을 때 사용**
* 테스트 환경에서 컴포넌트 렌더링 결과를 jsdom에 적용하기 위해서는 반드시 사용해야함
  * RTL의 Render, UserEvent API는 내부적으로 act()를 호출하여 상태를 반영하기 때문에 불필요
    {% endhint %}

### Context API 검증

> custom hook이 context API 또는 redux의 dispatch같은 context를 사용하는 경우&#x20;
>
> renderHook의 두번째 파라미터로 contextProvider Wrapper를 제공

```javascript
test('Should use custom step when incrementing', () => {
  const wrapper: React.FC<PropsWithChildren> = ({ children }) => {
    <CustomStepProvider step={2}>{children}</CustomStepProvider>
  }
  const { result } = renderHook(() => useCounter(), { wrapper });
  
  act(() => { result.current.increment });
  
  expect(result.current.count).toBe(2);
})

/* const CustomStepProvider = ({ step, children }) => (
*  <CustomStepContext.Provider value={step}>{children}</CustomStepContext>
* )
*/
```

### Asyc 검증

> custom hook이 async 동작을 수행할 때

```jsx
describe('useCounter with async test', () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });
  afterEach(() => {
    jest.runOnlyPendingTimers();
    jest.useRealTimers();
  });
  
  test('should increment counter after delay', async () => {
    const { result } = renderHook(() => useCounter());
    
    result.current.incrementAsync();
    
    await act(() => jest.runAllTimers());
    
    expect(result.current.count).toBe(1)
  })
})
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://taewoongs-organization.gitbook.io/jtwjs-dev-wiki/dev_note/testing/react-testing-pattern.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
