본문 바로가기

개발/React

jotai atom을 모킹하는 다양한 방법

반응형

 

일반적으로 jotai의 공식문서를 보면 testing에 대한 이야기가 별도로 있다.

 

첫번째 예시부터 확인해보자.

 

// 출처: https://jotai.org/docs/guides/testing#injected-values

import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import { useHydrateAtoms } from 'jotai/utils'
import { countAtom, Counter } from './Counter'
import { Provider } from 'jotai'

const HydrateAtoms = ({ initialValues, children }) => {
  useHydrateAtoms(initialValues)
  return children
}

const TestProvider = ({ initialValues, children }) => (
  <Provider>
    <HydrateAtoms initialValues={initialValues}>{children}</HydrateAtoms>
  </Provider>
)

const CounterProvider = () => {
  return (
    <TestProvider initialValues={[[countAtom, 100]]}>
      <Counter />
    </TestProvider>
  )
}

test('should not increment on max (100)', () => {
  render(<CounterProvider />)

  const counter = screen.getByText('100')
  const incrementButton = screen.getByText('one up')
  fireEvent.click(incrementButton)
  expect(counter.textContent).toEqual('100')
})

 

 

이 내용을 하나씩 살펴보자

 

실제 테스트코드 내에서 render하는 CounterProvider 컴포넌트는 모킹의 대상인 Counter 컴포넌트와 TestProvider로 이루어져있다. 코드를 보니 TestProvider의 initialValues 속성에 특정 Atom과 이 Atom에 주입해줄 데이터를 배열형태로 작성해주고, 이러한 모킹데이터들을 배열에 넣는 이중배열구조로 넘겨주는 것을 확인할 수 있다.

 

TestProvider 컴포넌트의 내용도 크게 다르지 않다. jotai의 Provider라는 컴포넌트로 감싸주고, 이를 미리 선언해둔 HydrateAtoms 컴포넌트를 감싸주며 받아온 모킹할 데이터를 그대로 넘겨준다. HydrateAtoms은 jotai의 useHydrateAtoms 훅을 사용해서 모킹할 데이터를 넘겨 실제로 모킹을 수행해주는 부분으로 보인다. 이는 이름에서와 같이 hydrate를 활용하여 모킹이 정상적으로 수행될 수 있도록 하는 방식이다.

 

use-case에서 보다시피 이 방식은 실제 유저의 액션플로우를 검증하는 테스트에서 사전에 해당 환경을 render하고자할 때 이와 같은 모킹방식을 사용하라는 것을 권장하고 있는 것으로 보인다.

 

 

두번째로 소개하는 방법은 특정 Atom을 테스트환경에서 직접 실행하여 이를 활용하는 방식이다.

 

// 출처: https://jotai.org/docs/guides/testing#custom-hooks

import { renderHook, act } from '@testing-library/react-hooks'
import { useAtom } from 'jotai'
import { countAtom } from './countAtom'

test('should increment counter', () => {
  const { result } = renderHook(() => useAtom(countAtom))

  act(() => {
    result.current[1]('INCREASE')
  })

  expect(result.current[0]).toBe(1)
})

 

 

원래 React의 hooks은 브라우저환경에서만 수행이 가능하지만, testing library의 renderHook 메서드를 사용한다면 테스트 환경에서 훅을 실행할 수 있게된다. 이를 활용하여 특정 Atom에 대한 유닛테스트를 수행할 때 위와 같은 방법을 권장하는 것으로 보인다.

 

 

위의 내용을 정리해보자면

 

  1. 유저 액션에 기반한 테스트를 수행할 때에는 Provider와 useHydrateAtoms를 활용하여 모킹하는 것을 권장한다.
  2. Atom단위로 유닛 테스트를 수행할 때에는 testing library의 renderHook을 활용해 검증하는 것을 권장한다.

 

로 이해할 수 있다.

 

 

나는 이번에 이미 만들어진 프로젝트의 리팩토링 내성을 향상시키기 위해 유저 액션에 기반한 테스트코드를 작성하는 것이 목표였다.

따라서 이러한 목적을 가지고있다면 testing섹션에서 권장했던 1번 방식인 Provider와 useHydrateAtoms를 활용하는 것이 올바른 방법이라고 생각했다.

 

하지만 이는 원하는대로 액션에 대한 검증이 안되었는데, 해결한 방법을 설명하기에 앞서 어떤 상황이었는지에 대한 이해도를 높이는 것이 중요하다고 생각해서 적어본다.

 

 

우리의 프로젝트는 먼저 지면에서 엮인 비즈니스 로직은 최대한 훅으로 추출하여 UI코드 내에서 비즈니스 로직을 분리하는 것을 지향하고 있었고, 특정 atom에 대한 value와 이를 가공한 action역시 별도의 훅으로 만들어 사용하고 있었다.

 

추가적으로 공통으로 사용하게되는 modal이나 bottom sheet의 경우 라우터와 동일한 계층에 두고, 이에 대한 on/off를 전역 상태로 판단, atom을 활용해 이러한 공통 컴포넌트의 on/off와 어떠한 컴포넌트를 보여줄지에 대한 것을 주입하여 사용하는 방식을 사용하고 있었다.

 

 

 

"특정 atom에 대한 value와 이를 가공한 action역시 별도의 훅으로 만들어 사용하고 있었다" 라고 표현한 코드의 예시는 아래와 같다.

 

// useTestHook.ts

export const useTestActions = () => {
  const setTestData = useSetAtom(testAtom)

  const setSomethingData = () => {
    // 위의 setter를 가공한 A함수   
  }
  
  // 원 atom에 대한 setter와 가공한 setter함수를 모두 반환하여 사용처에서 알맞게 사용하도록 함
  return {
    setTestData,
    setSomethingData
  }
}

 

 

이러한 상황에서 특정 지면에서 testAtom에 대한 상태값을 활용하고 있었고, 이는 이전 지면에서 useTestActions를 활용한 특정 액션으로부터 저장된 testAtom 데이터였다. 이를 사용하는 지면에서는 추가적으로 특정 액션을 수행할 경우 modal 혹은 bottom sheet가 open되어 추가적인 정보를 제공하는 방식이었다.

 

정리해보면 테스트코드를 작성하려고 했던 지면은 atom이 2개 이상 엮여있었고, 지면에서 사용하는 데이터의 경우 이전 지면에서 서버로부터 받아온 데이터를 가공하여 다음 지면에서 사용하기위해 전역 상태로 저장하고 있었고, 이 데이터를 받아서 사용하는 지면이었기에 테스트코드를 작성할 때에는 지면 내에서의 유저 액션을 기반으로 검증하려 한다면 불가피하게 모킹이 필요했다. (더 좋은 구조로 만들 수도 있었지만 해당 코드의 작성자가 본인이 아니었기에 이를 통해 구조를 먼저 파악 및 정책을 정리하고 점진적으로 구조를 개선하기 위한 테스트코드를 만들고 있었다.)

 

 

 

배경에 대한 설명을 했기에 실제 테스트코드를 어떤 생각의 흐름으로 작성하려고했고, 어떤 부분에서 문제가 발생했으며 어떻게 풀어갔는지 간단하게 이야기해보겠다.

 

 

먼저 공식문서를 스스로 이해한 바를 바탕으로 생각할 때 내가 사용해야하는 atom의 모킹 방식은 유저 액션에 기반한 테스트코드를 작성하고 있었기에 Provider와 useHydrateAtoms를 활용한 방식으로 테스트코드의 렌더 시에 atom에 데이터를 모킹해주려고 했었다. 모킹이 필요한 부분은 해당 지면에서 사용하는 정보들이라고 생각했고 이를 주입해주는 데에는 성공했으나, 특정 액션을 통해 modal 혹은 bottom sheet가 나타나지 않아 해당 플로우의 검증에 실패했다.

 

처음에는 영문도 모른채 액션 자체는 정상 수행하지만 modal 이나 bottom sheet만 뜨지 않는 현상에서 현상을 파악하는데에 시간을 사용했고, 결국 이는 Provider를 사용하는 위의 방식에서는 모킹을 해주는 순간 Provider로 별도의 Context가 발생하여 다른 atom과는 격리가 되어 다른 atom에 대한 수행이 정상적으로 동작하지 않는 것임을 알게되었다.

 

그렇다는 것은 결국 특정 상황에서 여러개의 atom와 상호작용이 필요한 지면에서는 Provider를 통해 모두 모킹해줄 수는 없다는 것이었다.

 

따라서 고민 끝에 찾은 방법은 생각보다 단순했다.

 

오히려 해당 atom을 render하고, 이에 대한 setter를 통해 모킹을 수행하여 테스트코드를 작성하는 것이었다.

 

이를 코드로 확인해보자.

 

describe('test action', () => {
  beforeEach(() => {
    // 먼저 해당 훅을 렌더하여 이에 대한 결과값을 받아낸다.
    const { result } = renderHook(() => useTestHook())
    
    // 이후 이 훅을 통해 반환받은 atom의 setter를 사용하여 원하는 모킹데이터를 직접 넣어준다.
    result.current.setTestAtom(MOCK_DATA)
  })


  it('correctly render', async () => {
    // 모킹은 완료되었으니 별도 Provider없이 지면만 렌더해준다.
    await render(<TestPage />)
  })
}

 

 

 

위에서 작성한 바와 같이 renderHook 메서드를 활용하여 setter를 가지고있는 훅을 먼저 실행하고, 이로부터 반환받은 setter를 실행하여 직접 모킹 데이터를 넣어주는 방식이었다. 이렇게 한다면 Provider를 활용하여 사용할 Atom에 대한 Context를 나누지 않아 특정 상황에서 실행되어야하는 시나리오 그대로 atom에 대한 액션이 동작하여 테스트가 정상적으로 수행될 수 있다.

 

 

테스트코드를 작성하며 실제로 현재 코드가 테스트하기에는 불편하다고 생각이 들었고, 다만 지면별로 엮인 정책에 대한 파악도 어렵기에 이를 먼저 유저 액션에 기반한 테스트를 작성하여 전반적으로 파악 및 기본적인 안정성을 확보해두고, 점진적으로 테스트하기 용이한 구조로 바꿔나가려고 하고있다.

 

이러한 케이스에 대해 대처할 수 있는 글을 별도로 확인하지 못했기에 글로 남겨두지만 더 좋은 방법이 있다면 의견주시면 언제든 환영입니다 :)

 

 

반응형