이미지 로딩 중...
AI Generated
2025. 11. 5. · 5 Views
React Native 테스트 전략 완벽 가이드
React Native 앱의 품질을 보장하는 테스트 전략을 알아봅니다. 단위 테스트부터 E2E 테스트까지 실무에서 바로 적용할 수 있는 테스트 기법과 모범 사례를 다룹니다.
들어가며
이 글에서는 React Native 테스트 전략 완벽 가이드에 대해 상세히 알아보겠습니다. 총 10가지 주요 개념을 다루며, 각각의 개념에 대한 설명과 실제 코드 예제를 함께 제공합니다.
목차
- Jest_기본_설정
- 컴포넌트_단위_테스트
- 비동기_로직_테스트
- Hook_테스트_전략
- Navigation_테스트
- Redux_상태_테스트
- Snapshot_테스트
- E2E_테스트_Detox
- 모킹_전략
- 테스트_커버리지_관리
1. Jest_기본_설정
개요
Jest는 React Native의 공식 테스트 프레임워크입니다. 빠른 실행 속도와 스냅샷 테스팅, 모킹 기능을 제공하여 효율적인 테스트 환경을 구축할 수 있습니다.
코드 예제
// jest.config.js
module.exports = {
preset: 'react-native',
setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'],
transformIgnorePatterns: [
'node_modules/(?!(react-native|@react-native|@react-navigation)/)'
],
moduleNameMapper: {
'\\.(jpg|jpeg|png|gif)$': '<rootDir>/__mocks__/fileMock.js'
},
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.test.{js,jsx,ts,tsx}'
]
};
설명
이 설정은 React Native 프로젝트에서 Jest 테스트를 실행하기 위한 기본 환경을 구성합니다. Jest는 Facebook에서 만든 JavaScript 테스트 프레임워크로, React Native와 완벽하게 통합되어 있습니다. 첫 번째로, preset: 'react-native'는 React Native에 최적화된 Jest 설정을 자동으로 적용합니다. 이는 Babel 변환, 모듈 해석, 환경 설정 등을 자동으로 처리하여 별도의 복잡한 설정 없이 바로 테스트를 시작할 수 있게 해줍니다. setupFilesAfterEnv는 모든 테스트 파일이 실행되기 전에 추가적인 설정을 로드하는데, 여기서는 Testing Library의 커스텀 매처를 확장합니다. 두 번째로, transformIgnorePatterns는 Jest가 node_modules 내의 특정 패키지를 변환하도록 설정합니다. 기본적으로 Jest는 node_modules를 변환하지 않지만, React Native 관련 패키지들은 ES6 모듈을 사용하므로 변환이 필요합니다. 이 설정을 통해 react-native, @react-native, @react-navigation 등의 패키지가 올바르게 처리됩니다. 세 번째로, moduleNameMapper는 이미지나 스타일 파일 같은 정적 리소스를 모킹합니다. 테스트 환경에서는 실제 이미지 파일이 필요 없으므로 빈 객체나 문자열로 대체하여 테스트 속도를 향상시킵니다. collectCoverageFrom은 코드 커버리지를 수집할 파일을 지정하여, 테스트되지 않은 코드를 쉽게 파악할 수 있게 합니다. 이 설정을 사용하면 React Native 프로젝트에서 안정적인 테스트 환경을 구축할 수 있습니다. 실제 프로젝트에서는 이 기본 설정 위에 프로젝트 특성에 맞는 커스텀 매처, 모킹 함수, 테스트 유틸리티 등을 추가하여 더욱 효율적인 테스트 워크플로우를 만들 수 있습니다.
2. 컴포넌트_단위_테스트
개요
React Native Testing Library를 사용하여 컴포넌트를 테스트합니다. 사용자의 관점에서 컴포넌트가 올바르게 렌더링되고 동작하는지 검증하는 것이 핵심입니다.
코드 예제
import { render, fireEvent } from '@testing-library/react-native';
import Button from '../Button';
describe('Button Component', () => {
it('텍스트를 올바르게 렌더링한다', () => {
const { getByText } = render(<Button title="Click me" />);
expect(getByText('Click me')).toBeTruthy();
});
it('버튼 클릭 시 onPress 핸들러가 호출된다', () => {
const onPressMock = jest.fn();
const { getByText } = render(<Button title="Click" onPress={onPressMock} />);
fireEvent.press(getByText('Click'));
expect(onPressMock).toHaveBeenCalledTimes(1);
});
});
설명
이 테스트는 React Native 컴포넌트가 예상대로 렌더링되고 사용자 상호작용에 올바르게 반응하는지 검증합니다. Testing Library의 핵심 철학은 "사용자가 보고 경험하는 것을 테스트하라"입니다. 첫 번째 테스트에서는 render 함수를 사용하여 Button 컴포넌트를 가상 환경에서 렌더링합니다. render는 컴포넌트를 메모리상의 가상 DOM에 그려주며, 다양한 쿼리 함수를 반환합니다. getByText는 화면에 특정 텍스트를 가진 엘리먼트를 찾는 함수로, 실제 사용자가 텍스트를 보는 것처럼 테스트합니다. toBeTruthy()는 해당 엘리먼트가 존재함을 확인합니다. 두 번째 테스트에서는 사용자 상호작용을 시뮬레이션합니다. jest.fn()으로 모킹 함수를 생성하여 실제 핸들러 구현 없이도 함수 호출 여부를 추적할 수 있습니다. fireEvent.press는 버튼 탭 동작을 시뮬레이션하며, 이는 실제 사용자가 화면을 터치하는 것과 동일한 이벤트를 발생시킵니다. toHaveBeenCalledTimes(1)은 함수가 정확히 한 번 호출되었는지 검증합니다. describe와 it 블록을 사용한 구조화된 테스트 작성은 테스트의 가독성을 높이고 실패 시 어느 부분이 문제인지 빠르게 파악할 수 있게 합니다. 각 테스트는 독립적으로 실행되어 서로 영향을 주지 않습니다. 이러한 단위 테스트를 통해 컴포넌트의 렌더링과 이벤트 처리 로직이 정상적으로 동작함을 보장할 수 있습니다. 실제 프로젝트에서는 엣지 케이스(빈 문자열, null 값 등)도 함께 테스트하여 더욱 견고한 컴포넌트를 만들 수 있습니다.
3. 비동기_로직_테스트
개요
API 호출이나 타이머 같은 비동기 작업을 테스트할 때는 waitFor, findBy 등의 비동기 유틸리티를 사용합니다. 비동기 동작이 완료될 때까지 기다렸다가 결과를 검증하는 것이 중요합니다.
코드 예제
import { render, waitFor } from '@testing-library/react-native';
import UserProfile from '../UserProfile';
describe('UserProfile 비동기 테스트', () => {
it('API에서 사용자 데이터를 가져와 표시한다', async () => {
// API 모킹
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: 'John Doe', email: 'john@example.com' })
})
);
const { findByText } = render(<UserProfile userId="123" />);
// 비동기적으로 렌더링될 때까지 대기
const nameElement = await findByText('John Doe');
expect(nameElement).toBeTruthy();
});
});
설명
이 테스트는 React Native 앱에서 가장 흔한 패턴인 API 데이터 페칭을 검증합니다. 실제 네트워크 요청은 테스트를 느리고 불안정하게 만들므로 모킹을 사용합니다. 첫 번째로, global.fetch를 jest.fn()으로 모킹하여 실제 HTTP 요청 없이 가짜 응답을 반환합니다. Promise.resolve를 중첩하여 사용하는 이유는 fetch API의 실제 동작을 정확히 시뮬레이션하기 위함입니다. fetch는 Response 객체를 반환하고, 그 객체의 json() 메서드가 다시 Promise를 반환하는 구조이기 때문입니다. 이렇게 모킹하면 실제 API 서버 없이도 컴포넌트의 데이터 페칭 로직을 테스트할 수 있습니다. 두 번째로, findByText는 Testing Library의 비동기 쿼리 함수입니다. getByText와 달리 findByText는 엘리먼트가 즉시 없어도 에러를 발생시키지 않고, 기본적으로 1초 동안 주기적으로 DOM을 확인하며 엘리먼트가 나타날 때까지 기다립니다. await 키워드와 함께 사용하여 비동기 렌더링이 완료될 때까지 테스트 실행을 중단합니다. 세 번째로, async/await 문법을 사용하여 테스트 함수를 비동기 함수로 만듭니다. 이는 Jest가 비동기 작업이 완료될 때까지 기다렸다가 테스트 결과를 판단하도록 합니다. 만약 async를 빼먹으면 테스트가 비동기 작업이 완료되기 전에 종료되어 잘못된 결과를 낼 수 있습니다. 이 패턴은 실제 프로젝트에서 매우 중요합니다. API 응답에 따라 UI가 변경되는 모든 컴포넌트에 적용할 수 있으며, 로딩 상태, 에러 상태, 성공 상태를 각각 테스트하여 모든 시나리오를 커버할 수 있습니다. 또한 waitFor를 사용하면 더 복잡한 비동기 조건도 테스트할 수 있습니다.
4. Hook_테스트_전략
개요
커스텀 Hook을 테스트할 때는 @testing-library/react-hooks의 renderHook을 사용합니다. Hook의 반환값과 상태 변화를 독립적으로 테스트하여 로직의 정확성을 검증할 수 있습니다.
코드 예제
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from '../useCounter';
describe('useCounter Hook', () => {
it('초기값으로 카운터를 설정한다', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('increment 함수가 카운터를 증가시킨다', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
설명
이 테스트는 React Hook의 로직을 컴포넌트와 분리하여 테스트하는 방법을 보여줍니다. Hook은 재사용 가능한 상태 로직을 캡슐화하므로, 독립적으로 테스트하면 더 빠르고 명확한 테스트를 작성할 수 있습니다. 첫 번째로, renderHook 함수는 Hook을 실제 컴포넌트 없이 렌더링합니다. 일반 Hook은 React 컴포넌트 내부에서만 호출할 수 있지만, renderHook은 내부적으로 테스트용 컴포넌트를 생성하여 Hook을 실행합니다. 반환되는 result 객체의 current 속성은 Hook의 반환값을 담고 있으며, Hook이 리렌더링될 때마다 자동으로 업데이트됩니다. 두 번째 테스트에서는 act 함수가 핵심입니다. act는 React의 상태 업데이트를 래핑하여 모든 상태 변경과 효과가 완전히 처리될 때까지 기다립니다. increment() 같은 상태 업데이트 함수를 호출할 때 act로 감싸지 않으면, 테스트에서 경고가 발생하거나 상태가 예상대로 업데이트되지 않을 수 있습니다. 이는 React의 배칭(batching) 메커니즘 때문입니다. 세 번째로, 각 테스트는 독립적으로 renderHook을 호출합니다. 이는 테스트 간 격리를 보장하여 한 테스트의 상태가 다른 테스트에 영향을 주지 않도록 합니다. 테스트마다 새로운 Hook 인스턴스가 생성되므로 깨끗한 상태에서 시작할 수 있습니다. 이 패턴은 복잡한 상태 관리 로직, 데이터 페칭 Hook, 폼 핸들링 Hook 등을 테스트하는 데 매우 유용합니다. 실제 프로젝트에서는 useEffect의 부수 효과, 의존성 배열 변경에 따른 재실행, cleanup 함수 등도 함께 테스트하여 Hook의 전체 생명주기를 검증할 수 있습니다.
5. Navigation_테스트
개요
React Navigation을 테스트할 때는 내비게이션 모킹과 실제 내비게이션 컨테이너를 함께 사용합니다. 화면 전환, 파라미터 전달, 뒤로가기 등의 내비게이션 동작을 검증할 수 있습니다.
코드 예제
import { render, fireEvent } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';
import HomeScreen from '../HomeScreen';
describe('HomeScreen Navigation', () => {
it('버튼 클릭 시 Details 화면으로 이동한다', () => {
const navigation = {
navigate: jest.fn(),
};
const { getByText } = render(<HomeScreen navigation={navigation} />);
fireEvent.press(getByText('상세보기'));
expect(navigation.navigate).toHaveBeenCalledWith('Details', { id: 123 });
});
});
설명
이 테스트는 React Native 앱에서 화면 간 이동을 처리하는 내비게이션 로직을 검증합니다. 실제 화면 전환 없이 내비게이션 함수 호출만 확인하여 빠르고 안정적인 테스트를 수행합니다. 첫 번째로, navigation 객체를 직접 모킹합니다. React Navigation은 각 화면 컴포넌트에 navigation prop을 전달하는데, 이 객체에는 navigate, goBack, setOptions 등의 메서드가 포함됩니다. 테스트에서는 실제 내비게이션 스택을 생성하지 않고 필요한 메서드만 jest.fn()으로 모킹하여 함수 호출을 추적합니다. 두 번째로, 컴포넌트를 렌더링할 때 모킹된 navigation 객체를 prop으로 전달합니다. 이렇게 하면 컴포넌트 내부에서 navigation.navigate()를 호출할 때 실제 화면 전환은 일어나지 않지만, 함수 호출 자체는 기록됩니다. 이는 의존성 주입(Dependency Injection) 패턴의 한 형태로, 테스트 가능한 코드를 작성하는 핵심 기법입니다. 세 번째로, toHaveBeenCalledWith 매처를 사용하여 navigate 함수가 정확한 인자로 호출되었는지 검증합니다. 첫 번째 인자는 목적지 화면 이름('Details'), 두 번째 인자는 전달할 파라미터({ id: 123 })입니다. 이를 통해 화면 전환뿐만 아니라 데이터 전달도 올바르게 이루어지는지 확인할 수 있습니다. 네 번째로, 더 복잡한 시나리오에서는 NavigationContainer로 전체 내비게이션 스택을 감싸서 테스트할 수도 있습니다. 이 경우 실제 화면 전환이 일어나므로 통합 테스트에 가깝습니다. 예를 들어, 여러 화면을 거쳐 이동하거나 뒤로가기 스택을 테스트할 때 유용합니다. 실제 프로젝트에서는 인증 후 리다이렉션, 딥링크 처리, 조건부 내비게이션(로그인 상태에 따라) 등 다양한 내비게이션 시나리오를 테스트할 수 있습니다. 이를 통해 사용자 흐름이 예상대로 작동함을 보장할 수 있습니다.
6. Redux_상태_테스트
개요
Redux 스토어와 연결된 컴포넌트를 테스트할 때는 실제 스토어를 제공하거나 모킹합니다. 액션 디스패치와 상태 변경이 UI에 올바르게 반영되는지 검증하는 것이 핵심입니다.
코드 예제
import { render } from '@testing-library/react-native';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import userReducer from '../userSlice';
import UserList from '../UserList';
describe('Redux 연결 컴포넌트 테스트', () => {
it('스토어의 사용자 목록을 렌더링한다', () => {
const store = configureStore({
reducer: { user: userReducer },
preloadedState: {
user: { users: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] }
}
});
const { getByText } = render(
<Provider store={store}><UserList /></Provider>
);
expect(getByText('Alice')).toBeTruthy();
expect(getByText('Bob')).toBeTruthy();
});
});
설명
이 테스트는 Redux로 관리되는 전역 상태가 컴포넌트에 올바르게 연결되고 렌더링되는지 검증합니다. Redux 연결 컴포넌트는 스토어 없이는 동작하지 않으므로 테스트용 스토어를 제공하는 것이 필수입니다. 첫 번째로, configureStore를 사용하여 테스트용 Redux 스토어를 생성합니다. 이는 Redux Toolkit의 권장 방식으로, 미들웨어와 DevTools가 자동으로 설정됩니다. reducer 옵션에는 실제 프로덕션과 동일한 리듀서를 전달하여 상태 로직이 제대로 동작하는지 확인합니다. preloadedState를 통해 초기 상태를 명시적으로 설정하면 특정 시나리오를 쉽게 테스트할 수 있습니다. 두 번째로, Provider 컴포넌트로 테스트 대상 컴포넌트를 감쌉니다. Provider는 React Context API를 사용하여 하위 컴포넌트들이 스토어에 접근할 수 있도록 합니다. 이렇게 하면 컴포넌트 내부의 useSelector와 useDispatch 훅이 정상적으로 작동합니다. Provider 없이 Redux 연결 컴포넌트를 렌더링하면 에러가 발생합니다. 세 번째로, preloadedState에 설정한 사용자 데이터가 화면에 올바르게 렌더링되는지 검증합니다. getByText로 각 사용자의 이름을 찾아 존재 여부를 확인합니다. 이는 useSelector로 상태를 읽어오는 로직과 그 상태를 UI로 변환하는 렌더링 로직이 모두 정상임을 의미합니다. 네 번째로, 실제 프로젝트에서는 액션 디스패치도 함께 테스트합니다. fireEvent로 버튼을 클릭하여 액션을 디스패치하고, 스토어 상태가 변경되며, UI가 업데이트되는 전체 플로우를 검증할 수 있습니다. store.getState()를 사용하면 특정 시점의 스토어 상태를 직접 확인할 수도 있습니다. 이 패턴을 통해 Redux의 단방향 데이터 흐름(액션 → 리듀서 → 상태 → UI)이 올바르게 작동함을 보장할 수 있습니다. 복잡한 비즈니스 로직이나 여러 컴포넌트가 같은 상태를 공유하는 경우에 특히 유용합니다.
7. Snapshot_테스트
개요
스냅샷 테스트는 컴포넌트의 렌더링 결과를 저장하고 이후 변경사항을 자동으로 감지합니다. UI의 의도하지 않은 변경을 빠르게 발견할 수 있어 리팩토링 시 유용합니다.
코드 예제
import { render } from '@testing-library/react-native';
import ProfileCard from '../ProfileCard';
describe('ProfileCard Snapshot', () => {
it('프로필 카드가 올바르게 렌더링된다', () => {
const user = {
name: 'John Doe',
email: 'john@example.com',
avatar: 'https://example.com/avatar.jpg'
};
const { toJSON } = render(<ProfileCard user={user} />);
// 첫 실행 시 스냅샷 생성, 이후 비교
expect(toJSON()).toMatchSnapshot();
});
});
설명
이 테스트는 컴포넌트의 전체 렌더링 출력을 스냅샷으로 저장하고 변경사항을 추적합니다. 스냅샷 테스트는 많은 컴포넌트를 빠르게 테스트할 수 있지만, 무분별하게 사용하면 유지보수가 어려워질 수 있으므로 적절한 균형이 필요합니다. 첫 번째로, toJSON() 메서드는 렌더링된 컴포넌트 트리를 JSON 형태로 변환합니다. 이 JSON은 컴포넌트의 구조, props, 스타일, 텍스트 내용 등을 모두 포함합니다. React Native의 View, Text 같은 네이티브 컴포넌트도 모두 JSON으로 직렬화되어 저장됩니다. 이 과정은 자동으로 이루어지며 개발자가 별도로 작성할 코드가 없습니다. 두 번째로, toMatchSnapshot()은 Jest의 스냅샷 매처입니다. 첫 실행 시에는 snapshots 폴더에 .snap 파일을 생성하여 JSON을 저장합니다. 이후 테스트가 실행될 때마다 현재 렌더링 결과와 저장된 스냅샷을 비교합니다. 만약 차이가 있다면 테스트가 실패하고, 개발자는 변경사항을 검토하여 의도된 변경인지 확인할 수 있습니다. 세 번째로, 스냅샷이 의도적으로 변경되었다면 jest --updateSnapshot 또는 jest -u 명령으로 스냅샷을 업데이트합니다. 예를 들어, 디자인 개선으로 버튼 스타일이 변경되었다면 스냅샷도 함께 업데이트해야 합니다. 이때 코드 리뷰에서 스냅샷 변경사항도 함께 검토하여 의도하지 않은 변경이 없는지 확인하는 것이 중요합니다. 네 번째로, 스냅샷 테스트의 장점은 작성이 매우 간단하다는 것입니다. 복잡한 UI 구조를 일일이 assertion으로 검증하는 대신 한 줄의 코드로 전체를 테스트할 수 있습니다. 특히 여러 props 조합에 따른 렌더링 결과를 테스트할 때 유용합니다. 그러나 단점도 있습니다. 스냅샷은 크고 읽기 어려워 리뷰가 어렵고, 작은 변경에도 테스트가 실패하여 개발자가 습관적으로 업데이트를 승인할 수 있습니다. 따라서 중요한 UI 로직은 명시적인 assertion과 함께 사용하고, 스냅샷은 보조적인 역할로 사용하는 것이 좋습니다.
8. E2E_테스트_Detox
개요
Detox는 React Native를 위한 E2E 테스트 프레임워크입니다. 실제 디바이스나 시뮬레이터에서 앱을 실행하여 사용자 시나리오 전체를 검증할 수 있습니다.
코드 예제
// e2e/login.test.js
describe('로그인 플로우', () => {
beforeAll(async () => {
await device.launchApp();
});
it('이메일과 비밀번호로 로그인한다', async () => {
await element(by.id('emailInput')).typeText('user@example.com');
await element(by.id('passwordInput')).typeText('password123');
await element(by.id('loginButton')).tap();
// 로그인 후 홈 화면 확인
await expect(element(by.text('Welcome'))).toBeVisible();
});
});
설명
이 테스트는 앱의 전체 흐름을 실제 환경에서 검증하는 E2E(End-to-End) 테스트입니다. 단위 테스트나 통합 테스트와 달리 실제 사용자가 경험하는 것과 동일한 방식으로 앱을 테스트합니다. 첫 번째로, beforeAll 훅에서 device.launchApp()을 호출하여 iOS 시뮬레이터나 Android 에뮬레이터에서 앱을 실행합니다. Detox는 앱을 컴파일하고 설치한 후 시작하는 전체 과정을 자동화합니다. 이는 실제 사용자가 앱 아이콘을 탭하여 실행하는 것과 동일합니다. 앱이 완전히 로드될 때까지 자동으로 기다립니다. 두 번째로, element(by.id())를 사용하여 UI 엘리먼트를 찾습니다. by.id는 testID prop으로 지정한 식별자로 엘리먼트를 찾으며, by.text, by.label, by.type 등 다양한 매처를 지원합니다. typeText 액션은 실제 키보드 입력을 시뮬레이션하여 텍스트를 한 글자씩 입력합니다. 이 과정에서 React Native의 TextInput 이벤트(onChangeText, onFocus 등)가 실제로 발생합니다. 세 번째로, tap() 액션은 엘리먼트를 터치하는 동작을 시뮬레이션합니다. 이는 네이티브 터치 이벤트를 발생시키므로 onPress 핸들러뿐만 아니라 터치 애니메이션, 햅틱 피드백 등도 실제로 동작합니다. Detox는 액션 후 앱이 아이들 상태가 될 때까지 자동으로 대기하여 다음 단계로 진행합니다. 네 번째로, expect().toBeVisible() 매처로 로그인 성공 후 화면 전환을 검증합니다. Detox의 expectation은 자동으로 재시도하므로 애니메이션이나 네트워크 요청으로 인한 지연이 있어도 안정적으로 테스트할 수 있습니다. 기본적으로 1.5초 동안 재시도하며, 이 시간은 설정으로 조정할 수 있습니다. E2E 테스트는 실행 시간이 길고 설정이 복잡하지만, 가장 높은 신뢰도를 제공합니다. 실제 프로젝트에서는 핵심 사용자 플로우(회원가입, 로그인, 결제 등)에 E2E 테스트를 작성하고, 세부 로직은 단위 테스트로 커버하는 것이 효율적입니다. CI/CD 파이프라인에 통합하여 배포 전 자동으로 실행할 수도 있습니다.
9. 모킹_전략
개요
외부 의존성(API, 라이브러리, 네이티브 모듈)을 모킹하여 테스트를 격리하고 빠르게 만듭니다. jest.mock을 사용하여 모듈 전체를 교체하거나 특정 함수만 모킹할 수 있습니다.
코드 예제
// __mocks__/react-native-async-storage.js
export default {
setItem: jest.fn(() => Promise.resolve()),
getItem: jest.fn(() => Promise.resolve(null)),
removeItem: jest.fn(() => Promise.resolve()),
};
// MyComponent.test.js
import AsyncStorage from '@react-native-async-storage/async-storage';
import { saveUserData } from '../storage';
describe('Storage 모킹', () => {
it('사용자 데이터를 AsyncStorage에 저장한다', async () => {
await saveUserData({ name: 'Alice' });
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
'user', JSON.stringify({ name: 'Alice' })
);
});
});
설명
이 테스트는 React Native의 네이티브 모듈을 모킹하여 실제 디바이스 기능 없이 로직을 테스트합니다. 모킹은 테스트를 빠르고 안정적으로 만드는 핵심 기법입니다. 첫 번째로, mocks 폴더에 모킹 파일을 생성합니다. Jest는 jest.mock() 호출 시 자동으로 이 폴더에서 같은 이름의 파일을 찾아 원본 모듈을 대체합니다. 파일 위치는 node_modules 옆 또는 테스트 파일 옆이어야 하며, 폴더 이름은 정확히 __mocks__여야 합니다. 이 규칙을 따르면 수동으로 모킹 코드를 import하지 않아도 자동으로 적용됩니다. 두 번째로, AsyncStorage의 각 메서드를 jest.fn()으로 모킹합니다. jest.fn()은 모킹 함수를 생성하며, 호출 횟수, 인자, 반환값 등을 추적할 수 있습니다. Promise.resolve()를 반환하여 비동기 메서드의 동작을 시뮬레이션합니다. 실제 AsyncStorage는 네이티브 코드를 호출하지만, 모킹된 버전은 즉시 완료되는 Promise를 반환하여 테스트를 빠르게 만듭니다. 세 번째로, toHaveBeenCalledWith 매처로 함수가 정확한 인자로 호출되었는지 검증합니다. 이는 saveUserData 함수가 올바른 키와 직렬화된 데이터를 AsyncStorage에 전달했는지 확인합니다. 실제 저장 여부가 아닌 저장 시도를 검증하는 것이므로, 로직의 정확성에 집중할 수 있습니다. 네 번째로, 각 테스트 전에 jest.clearAllMocks()를 호출하여 모킹 함수의 호출 기록을 초기화하는 것이 좋습니다. 이를 beforeEach 훅에 넣으면 테스트 간 격리를 보장할 수 있습니다. 그렇지 않으면 이전 테스트의 호출이 누적되어 잘못된 결과를 낼 수 있습니다. 다섯 번째로, 모킹된 함수의 반환값을 테스트마다 다르게 설정할 수도 있습니다. AsyncStorage.getItem.mockResolvedValue('cached_data')처럼 특정 테스트에서만 다른 값을 반환하도록 설정하여 다양한 시나리오를 테스트할 수 있습니다. 이는 에러 케이스, 캐시 히트/미스, 권한 거부 등을 시뮬레이션하는 데 유용합니다. 실제 프로젝트에서는 위치 정보, 카메라, 푸시 알림 등 다양한 네이티브 모듈을 모킹하여 테스트합니다. 이를 통해 실제 디바이스 없이도 모든 기능을 검증할 수 있으며, CI/CD 환경에서도 안정적으로 테스트를 실행할 수 있습니다.
10. 테스트_커버리지_관리
개요
코드 커버리지는 테스트가 코드의 얼마나 많은 부분을 실행했는지 측정합니다. Jest의 coverage 옵션을 사용하여 테스트되지 않은 코드를 찾고 품질 기준을 설정할 수 있습니다.
코드 예제
// package.json
{
"scripts": {
"test:coverage": "jest --coverage --collectCoverageFrom='src/**/*.{js,jsx,ts,tsx}'"
},
"jest": {
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
설명
이 설정은 코드 커버리지를 측정하고 품질 기준을 강제하여 테스트의 완성도를 관리합니다. 커버리지는 테스트의 양적 지표로, 질적 지표인 테스트의 정확성과 함께 고려해야 합니다. 첫 번째로, --coverage 플래그는 Jest가 테스트 실행 중 코드 실행을 추적하도록 합니다. 내부적으로 Istanbul 이라는 커버리지 도구를 사용하며, 각 라인, 함수, 브랜치(if/else), 구문이 실행되었는지 기록합니다. 테스트 종료 후 터미널에 표 형식으로 커버리지를 출력하고, coverage 폴더에 HTML 리포트도 생성합니다. 두 번째로, collectCoverageFrom 옵션은 어떤 파일의 커버리지를 수집할지 지정합니다. src 폴더의 모든 JavaScript/TypeScript 파일을 대상으로 하되, 테스트 파일은 제외합니다. 이 설정이 없으면 import된 파일만 커버리지에 포함되어, 테스트가 없는 파일이 누락될 수 있습니다. 명시적으로 지정하면 테스트가 전혀 없는 파일도 0% 커버리지로 표시됩니다. 세 번째로, coverageThreshold는 최소 커버리지 기준을 설정합니다. 여기서는 전역적으로 모든 메트릭에 80%를 요구합니다. branches는 if/else 같은 조건문의 모든 경로, functions는 정의된 함수의 호출, lines는 실행 가능한 코드 라인, statements는 개별 구문을 의미합니다. 기준 미달 시 테스트가 실패하여 CI/CD에서 빌드가 차단됩니다. 네 번째로, 커버리지 기준은 프로젝트 특성에 맞게 조정해야 합니다. 신규 프로젝트는 80-90%를 목표로 하고, 레거시 프로젝트는 점진적으로 올리는 것이 현실적입니다. 또한 특정 폴더나 파일에 다른 기준을 적용할 수도 있습니다. 예를 들어, UI 컴포넌트는 70%, 비즈니스 로직은 90%처럼 차별화할 수 있습니다. 다섯 번째로, 100% 커버리지가 반드시 좋은 것은 아닙니다. 단순 getter/setter, 타입 정의, 상수 등을 테스트하는 것은 비효율적일 수 있습니다. coveragePathIgnorePatterns로 특정 파일을 커버리지에서 제외할 수 있습니다. 중요한 것은 커버리지 수치가 아니라 핵심 로직이 제대로 테스트되었는지입니다. 실제 프로젝트에서는 커버리지 리포트를 CI/CD에 통합하여 PR마다 커버리지 변화를 추적하고, Codecov나 Coveralls 같은 서비스로 시각화할 수 있습니다. 이를 통해 팀 전체가 테스트 품질을 지속적으로 모니터링하고 개선할 수 있습니다.
마치며
이번 글에서는 React Native 테스트 전략 완벽 가이드에 대해 알아보았습니다. 총 10가지 개념을 다루었으며, 각각의 사용법과 예제를 살펴보았습니다.
관련 태그
#React Native #Jest #Testing Library #E2E Testing #Component Testing