본문 바로가기
source-code/FrontEnd

testing-library/react-native를 통한 컴포넌트 테스트 2

by mattew4483 2023. 8. 18.
728x90
반응형

2023.08.18 - [source-code/FrontEnd] - testing-library/react를 통한 컴포넌트 테스트 (with RN + expo)

테스트 코드 작성 중 겪었던 에러 핸들링.

 

1. Jest를 통한 React-Query (useQuery) 테스트

현재 서비스에서는 server data 관리를 위해 react-query를 사용하고 있다.

테스트 대상 컴포넌트에서는 토글 on/off 데이터를 서버로부터 받아온 후, 해당 데이터에 맞는 UI를 그려주고 있었다.

 

// useGetToggleStatus
const useGetToggleStatus = (options) => useQuery(['key'], getToggleApi, options)

// Page
const Page = () => {
	const {data} = useGetToggleStatus()
    
    return <Text> data ? '켜짐' ; '꺼짐' </Text>
}

위와 같은 형태로, useQuery를 반환하는 custom hooks를 작성해 필요한 페이지에서 호출하는 형태로 사용하고 있었다.

이 때 테스트 대상은 → data에 따라 적절한 문구(켜짐 or 꺼짐)가 화면에 그려지는가!

 

// Page.test.js
jest.mock("./useGetToggleStatus", () => ({
  useGetToggleStatus: jest.fn(),
}));

// ...
it("토글이 켜져있을 경우 '켜짐' 문구가 뜬다", () => {
    useGetToggleStatus.mockResolvedValueOnce({
      data: true,
    });

    const { getByText } = render(<Page />);
    expect(getByText("켜짐")).toBeTruthy();
}

처음에는 해당 useGetToggleStatus의 반환값을 mocking하면 되겠다!고 생각했다.

따라서 위와 같이 useGetToggleStatus을 mocking해

해당 custom hooks가 토글이 켜진 상태의 서버 반환값 을 반환하도록 작성했다.

 

위의 코드로도 테스트는 통과하지만... 문제.

// Page
const Page = () => {
	const [toggle, setToggle] = useState(false)

	const {data} = useGetToggleStatus({
        	onSuccess : () => setToggle(true)
    	})
    
    	return <Text> toggle ? '켜짐' ; '꺼짐' </Text>
}

토글 상태는 서버 데이터를 통해 초깃값을 그려줄 뿐, 사용자의 액션을 통해 UI 상으로 계속 변경되야하므로

사용자에게 보이는(화면에 그려지는) 토글은 toggle 이라는 state로 관리 중이었다.

그리고 해당 state는... useQuery의 onSuccess 옵션을 통해 변경 중이었다..!

 

처음 작성한 테스트 코드는 useGetToggleStatus란 custom hooks를 직접 mocking해 반환값을 제어했기 때문에...

테스트 환경에서는 onSuccess와 같은 useQuery 옵션들이 정상적으로 동작할리가 없다!

 

그렇다면..? useQuery hooks 자체를 직접 mocking 해야할까?

해당 데이터 뿐만 아니라, 다른 서버 데이터를 위해 useQuery가 여러 곳에서 호출되고 있다면 어쩌지??

 

// Page.test.js
jest.mock("./getToggleApi", () => ({
  getToggleApi: jest.fn(),
}));

// ...
it("토글이 켜져있을 경우 '켜짐' 문구가 뜬다", () => {
    getToggleApi.mockResolvedValueOnce({
      data: true,
    });

    const { getByText } = render(<Page />);
    expect(getByText("켜짐")).toBeTruthy();
}

해결 방법은 간단하다.

우리가 mocking 하고자 하는 함수는 custom query hooks나 useQuery가 아닌,

서버로부터 데이터를 받아오는 api 함수(여기서는 getToggleApi)이므로...

 

해당 의도를 구현  

useQuery hooks를 mocking하는 것이 아닌, 해당 query의 qureyFn 함수를 mocking하면 된다!

 

2. Redux Store State

그런데 위와 같이 수정해도, 이상하게 useQuery의 queryFn이 동작하지 않았다.

왜 그런지 살펴보니...

// useGetToggleStatus
const useGetToggleStatus = (options) => useQuery(['key'], getToggleApi, options)

// Page
const Page = () => {
	const {profile} = useSelector(authSelector)
	const {data} = useGetToggleStatus({
    		enabled : profile?.id === 'number'
    })
    
    return <Text> data ? '켜짐' ; '꺼짐' </Text>
}

아하... custom query hooks에 enabled 속성이 지정되어 있었고,

해당 값은 redux store에서 조회했기 때문에 → 따로 mocking 하지 않았으므로 undefined라 false가 되었던 것!

 

https://redux.js.org/usage/writing-tests

 

Writing Tests | Redux

Usage > Writing Tests: recommended practices and setup for testing Redux apps

redux.js.org

역시나 공식문서를 참고해

export function renderWithProviders(
  ui: React.ReactElement,
  {
    preloadedState = {},
    // Automatically create a store instance if no store was passed in
    store = configureStore({ reducer: { user: userReducer }, preloadedState }),
    ...renderOptions
  }: ExtendedRenderOptions = {}
) {
  function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element {
    return <Provider store={store}>{children}</Provider>
  }

  // Return an object with the store and all of RTL's query functions
  return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }
}

위와 같은 util 함수를 작성했고

 

// Page.test.js
jest.mock("./getToggleApi", () => ({
  getToggleApi: jest.fn(),
}));

// ...
it("토글이 켜져있을 경우 '켜짐' 문구가 뜬다", () => {
    getToggleApi.mockResolvedValueOnce({
      data: true,
    });

    const { getByText } = renderWithProvider(<Page />, {
    	preloadedState : {
        	auth : {
            	profile : {
                	id : 0
                }
            }
        }
    });
    expect(getByText("켜짐")).toBeTruthy();
}

preloadedState를 해당 store 구조에 맞게 넘겨주었다.

→ 해당 값으로 Provider가 생성되면서, 테스트 환경에서는 우리가 mocking한 preloadedState의 값을 조회할 수 있게 된다.

 

3. not wrapped in act 에러

위와 같이 다 작성했는데도... 여전히 실패하는 테스트.

위와 같은 에러 로그를 만날 수 있었다.

 

// Page
// ...
const [toggle, setToggle] = useState(false)

const {data} = useGetToggleStatus({
    onSuccess : () => setToggle(true)
})

문제는 해당 코드.

useQuery의 onSuccess를 통해 setState를 실행하고 있는데,

setState는 비동기적으로 상태를 업데이트 하므로... 해당 업데이트가 일어나기도 전에 테스트가 종료되버리고 만 것!

 

// Page.test.js
import { waitFor } from "@testing-library/react-native";

// ...
const { getByText } = render(<Page />);    
    
await waitFor(() => {
  expect(getByText("켜짐")).toBeTruthy();
});

waitFor 함수를 통해 상태 업데이트가 완료될 때까지 대기하면 된다!

 

https://davidwcai.medium.com/react-testing-library-and-the-not-wrapped-in-act-errors-491a5629193b

 

React Testing Library and the “not wrapped in act” Errors

How do I fix the “…not wrapped in act(…)” errors?

davidwcai.medium.com

해당 에러를 참고한 블로그!

728x90
반응형