> 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_note/testing/react-testing-pattern.md).

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