Mock, SpyOn, Fn: 통합테스트의 핵심 요소 이해하기

hodu·2024년 2월 27일
2

✅ 통합테스트 핵심 이해: Mock, SpyOn, Fn

최근 통합테스트를 수행하며, 핵심 메소드에 대한 깊은 이해의 필요성을 느꼈습니다.
통합테스트를 보다 효과적으로 수행하기 위해, Mock, SpyOn, 그리고 Fn의 개념과 사용법을 명확히 파악하는 것이 중요하다고 생각했습니다.

이 세 가지 메소드는 테스트 코드의 각기 다른 요구사항을 충족시키기 위해 사용되며, 각각의 목적과 사용 상황을 이해하는 것은 테스트의 효율성과 정확성을 크게 향상시킬 수 있습니다.

본 정리에서는 통합테스트의 핵심 요소로서 mock, spyOn, fn의 개념과 사용 목적에 대해 살펴보고, 실제 적용 예시를 통해 이들 메소드의 적절한 사용 상황을 비교 분석합니다.

이 내용은 주로 Jest와 Vitest의 문서를 참조하여 정리했으며, 설명은 Jest를 중심으로 진행됩니다.

이 글의 주 목적은 통합테스트의 전반적인 설명보다는 Mock, SpyOn, Fn이라는 세 가지 주요 메소드의 차이점과 적절한 사용 상황을 이해함으로써, 개발자가 상황에 맞게 가장 적합한 도구를 선택하여 사용할 수 있도록 하는 데 있습니다.


🙌 통합테스트란?

Module Mocking Example 통합테스트는 개별 유닛 테스트를 완료한 후, 이들 유닛들이 서로 어떻게 상호작용하는지를 검증하는 과정입니다. 예를 들어, 두 개 이상의 함수나 컴포넌트가 함께 작동할 때 예상대로 기능하는지 확인합니다.

중요한 점은 통합 테스트가 개별 유닛의 내부 로직보다는 유닛들 간의 연결과 상호작용의 정확성에 초점을 맞춘다는 것입니다.

이를 위해, 테스트 중인 유닛 외부의 요소들을 제어하거나 예측 가능한 상태로 만드는 것이 중요합니다.
이 과정에서 Spyon, Mock 그리고 Fn 같은 메소드를 사용하는데 이에 대해 설명하겠습니다.


🕵️‍♂️ spyOn 소개

spyOn은 테스팅 프레임워크에서 제공하는 기능으로, 객체의 메서드 호출을 감시(spy)하고, 필요한 경우 메서드의 동작을 임시로 변경할 수 있게 해줍니다. 이를 통해 메서드가 호출되었는지, 어떤 인자로 호출되었는지 등을 테스트 코드 내에서 확인할 수 있습니다.

💡 기본 사용법

jest.spyOn(object, methodName);
  • object: 감시하려는 메서드가 속한 객체
  • methodName: 감시하려는 메서드의 이름 (문자열 형태)

📝 주요 특징

spyOn으로 생성된 스파이 객체는 원래 함수를 대체하지 않고 감싸므로, 함수가 호출될 때마다 스파이 객체가 이를 감지하고 추적 정보를 기록합니다.

  • 함수가 호출 횟수를 확인합니다.
  • 함수가 호출할 때 전달한 인자를 확인합니다.
  • 함수가 어떤 값으로 반환되었는지 확인합니다 (모의 구현을 사용한 경우).

🧹 주의할 점: 테스트 후 복원

테스트 간 영향을 줄이기 위해 각 테스트 이후에 restoreAllMocks를 호출하여 모의 설정을 초기화하는 것이 좋습니다.

afterEach(() => {
  jest.restoreAllMocks();
});

이제 직접 코드 예시를 보며 어떻게 적용하는지 살펴보겠습니다.

spyOn을 사용하여 함수 호출을 감시하면서 원래의 기능을 유지하거나 필요에 따라 모의하는 것이 주요 특징입니다. 그러므로, fetchUserData 함수를 감시하되 원래의 동작을 유지하면서 호출 정보(예: 호출 횟수, 전달된 인자 등)를 추적하는 예시가 더 적합할 것 같습니다. 이를 통해 jest.spyOn의 감시 기능을 보다 명확하게 드러낼 수 있습니다.

🎯 개선된 실무 적용 예시

원래 함수를 유지하면서 함수 호출을 추적하고 검증하는 예시를 살펴보겠습니다.

원래 함수의 동작을 유지하면서 함수 호출 추적하기

원래 함수:

// user.js
export async function fetchUserData(userId) {
  // 외부 API 호출을 가정
  return { id: userId, name: 'John Doe' };
}

테스트 코드:

// user.test.js
import * as UserModule from './user';

describe('fetchUserData', () => {
  afterEach(() => {
    jest.restoreAllMocks();
  });

  it('tracks the call to fetchUserData while keeping its original implementation', async () => {
    // fetchUserData 함수를 감시하지만 원래의 구현을 유지
    const spy = jest.spyOn(UserModule, 'fetchUserData');

    const userId = 'testId';
    const userData = await UserModule.fetchUserData(userId);

    // spyon 하고 있는 fetchUserData 함수가 호출되었는지 검증
    expect(spy).toHaveBeenCalledWith(userId);

    // 반환된 데이터가 원래 함수의 반환 값과 일치하는지 검증
    expect(userData).toEqual({ id: userId, name: 'John Doe' });

  });
});

이 예시에서는 jest.spyOn을 사용하여 fetchUserData 함수를 감시하되, mockImplementation을 통해 원래의 함수 구현을 유지합니다. 이 방식을 통해 함수가 올바르게 호출되었는지와 원래의 동작을 수행하는지를 동시에 검증할 수 있습니다. 이는 jest.spyOn의 능력을 보여주는 좋은 예시이며, 실제 테스트에서 원하는 함수의 동작을 유지하면서 추가적인 검증을 수행하고자 할 때 유용합니다.

jest.spyOn의 주요 장점은 기존 함수의 동작을 변경하지 않고 호출을 감시하거나, 필요에 따라 동작을 모의할 수 있다는 점입니다.


🕵️‍♂️ mock 소개

Module Mocking Example (mock은 위처럼 기존의 모듈을 완벽하게 대체할 수 있습니다.)

Jest의 jest.mock 함수는 통합 테스트를 수행할 때, 외부 라이브러리프로젝트 내 복잡한 유틸리티 모듈을 모의화하여 실제 구현 대신 가상의 구현을 사용함으로써 테스트의 복잡성을 줄이고 의존성을 제거할 수 있습니다. 이를 통해 테스트가 외부 요소에 의존하지 않고 일관되게 수행될 수 있도록 도와줍니다.

💡 mock의 기본 사용법

jest.mock('moment'); // 외부 라이브러리 대체
jest.mock('./databaseQuery'); // 프로젝트 내 유틸 파일 대체
  • 'moment'는 외부 날짜 처리 라이브러리를 모의화합니다.
  • './databaseQuery'는 프로젝트 내 데이터베이스 쿼리를 수행하는 유틸리티 파일을 모의화합니다.

📝 mock의 주요 특징

  • 모듈 전체 모의화: 지정된 모듈의 모든 함수를 자동으로 모의 함수로 대체하여 실제 구현 없이 통합 테스트를 수행할 수 있습니다.
  • 의존성 제거: 실제 외부 서비스나 복잡한 로직에 의존하지 않고도 해당 기능을 사용하는 코드를 독립적으로 테스트할 수 있습니다.

🎯 mock을 사용한 실무 적용 예시

1. 외부 라이브러리(moment) 모의화

// time.test.js
jest.mock('moment'); // moment 라이브러리를 모의화

import moment from 'moment';

// moment 함수에 대한 모의 구현 정의
moment.mockReturnValue('2024-02-27'); // 테스트 목적의 특정 날짜를 반환

// 실제 비즈니스 로직에서 moment를 호출하고 모의 날짜가 반환되는지 검증
function getCurrentDate() {
    return moment().format('YYYY-MM-DD');
}

// getCurrentDate 함수가 모의 날짜 '2024-02-27'을 반환하는지 검증
test('getCurrentDate returns mocked date', () => {
    expect(getCurrentDate()).toBe('2024-02-27');
});

이 예시에서는 moment 라이브러리를 모의화하여 테스트 중에는 항상 '2024-02-27'이라는 특정 날짜를 반환하도록 설정했습니다. 이를 통해 날짜 관련 로직을 테스트할 때 실제 시간의 변화에 영향을 받지 않습니다.

2. 프로젝트 내 유틸 파일(databaseQuery) 모의화

// database.test.js
import { fetchData } from './databaseQuery';

// databaseQuery 파일을 모의화합니다.
jest.mock('./databaseQuery', () => ({
  fetchData: jest.fn()
}));

// fetchData 함수에 대한 모의 구현 정의
fetchData.mockResolvedValue({ id: 1, name: 'Mocked Data' }); // 비동기 처리를 가정한 모의 데이터 반환

// fetchData를 사용하는 비즈니스 로직을 테스트하고, 모의 데이터가 반환되는지 검증
async function getData() {
    return await fetchData();
}

// getData 함수가 모의 데이터를 정상적으로 반환하는지 검증
test('getData returns mocked data', async () => {
    const data = await getData();
    expect(data).toEqual({ id: 1, name: 'Mocked Data' });
});

이 예시에서는 데이터베이스 쿼리를 수행하는 fetchData 함수를 모의화하여 테스트 중에는 항상 { id: 1, name: 'Mocked Data' }라는

특정 데이터를 반환하도록 설정했습니다. 이를 통해 데이터베이스와의 실제 상호작용 없이도 데이터 처리 로직을 검증할 수 있습니다.


🕵️‍♂️ fn 소개

fn() 메서드는 함수의 모의 구현을 생성하여, 함수 호출을 추적하고 그 동작을 제어할 수 있게 합니다. 이는 모듈 전체를 모킹할 필요 없이 특정 함수만을 대상으로 할 때 매우 유용합니다.

💡 기본 사용법

const mockFunction = jest.fn();

이 코드는 Jest를 사용해 모의 함수를 생성하며, 이 함수는 호출 시 그 정보를 추적하고 원하는 대로 동작을 설정할 수 있습니다.

🎯 실무 적용 예시: API 호출 테스트

API 호출 함수를 모킹하고 이를 활용하는 서비스 함수를 테스트하는 예시를 들어보겠습니다.

시나리오 개요

  • fetchUser: 사용자 정보를 서버로부터 가져오는 API 호출 함수입니다.
  • getUserProfile: fetchUser를 호출해 사용자 프로필 정보를 처리하는 서비스 함수입니다.

파일 구조

  • api.js: fetchUser와 같은 API 호출 함수들이 정의된 파일입니다.
  • userService.js: 사용자 관련 서비스 로직을 처리하는 파일로, api.jsfetchUser 함수를 사용합니다.

userService.js

// userService.js
import { fetchUser } from './api';

export async function getUserProfile(userId) {
  const user = await fetchUser(userId);
  // 사용자 프로필 정보 처리 로직
  return user;
}

🧪 테스트 코드 (userService.test.js)

jest.fn()을 사용해 fetchUser를 모킹하고 getUserProfile 함수를 테스트해 봅시다.

// userService.test.js
import { getUserProfile } from './userService';
import * as api from './api';

// `fetchUser` 함수 모킹
api.fetchUser = jest.fn();

describe('getUserProfile', () => {
  it('successfully fetches user profile', async () => {
    const mockUser = { id: '1', name: 'John Doe' }; // 모킹 데이터

    // `fetchUser`가 모킹 데이터를 반환하도록 설정
    api.fetchUser.mockResolvedValue(mockUser);

    // `getUserProfile` 함수 호출 및 결과 검증
    const userProfile = await getUserProfile('1');
    expect(userProfile).toEqual(mockUser);
  });
});

이 테스트는 jest.fn()을 활용해 fetchUser 함수를 모킹하고, 이 함수가 예상대로 특정 사용자 데이터를 반환하도록 설정합니다. 이후 getUserProfile 함수를 호출해, 모킹된 fetchUser를 통해 올바른 사용자 프로필 정보를 성공적으로 가져오는지 확인합니다.

이 예시는 jest.fn()을 이용해 의존성을 모킹하고, 복잡한 외부 시스템이나 API 호출 없이 함수의 동작을 효과적으로 테스트할 수 있는 방법을 보여줍니다.



👀 정리

테스트 프레임워크에서 제공하는 다양한 목킹 기능을 통해 테스트의 복잡성을 줄이고, 의존성을 효과적으로 관리할 수 있도록 도와줍니다.
이러한 기능들은 각각의 사용 목적과 주 사용 경우가 있으며, 필요에 따라 서로 조합하여 사용할 수 있습니다.

기능jest.mock()jest.fn()jest.spyOn()
사용 목적모듈 전체를 모킹개별 함수를 모킹기존 함수의 호출을 추적하며 모킹 가능
주 사용 경우모듈 내 여러 함수/변수를 모킹할 때단일 함수의 동작을 커스터마이즈할 때기존 함수의 동작을 유지하면서 감시할 때
특징모듈 전체를 자동으로 모킹 함수로 대체생성된 모킹 함수를 직접 조작 및 설정기존 함수를 유지하며 함수 호출을 추적
반환값/예외모듈의 함수들을 모킹하여 커스텀 가능모킹 함수의 반환값/예외를 커스텀 가능함수의 반환값을 모킹하거나 함수 호출을 가로채어 커스텀 가능
사용 예여러 외부 API 함수를 포함한 모듈 모킹이벤트 핸들러, 콜백 함수 모킹함수가 어떻게 호출되는지 감시하면서 테스트

출처:
https://jestjs.io/docs/jest-object#jestspyonobject-methodname
https://jestjs.io/docs/mock-function-api
https://github.com/tinylibs/tinyspy
https://vitest.dev/guide/mocking.html
https://vitest.dev/api/vi#vi-spyon
https://vitest.dev/api/vi#vi-fn

profile
잘부탁드립니다.

0개의 댓글