고감귤 2025. 4. 7. 14:57

1.  개념

Mock은 테스트할 때 진짜 객체(DB, 외부 API, 파일 시스템 등) 대신 사용하는 가짜 객체다.

Mock은 서비스 코드 내부의 실제 로직은 그대로 유지하고 사용하되, 외부에 의존하는 객체 부분만 가짜로 대체한다.

 

예를 들어, 어떤 코드가 실제 DB에서 사용자 정보를 조회한다고 가정해보자.
테스트를 실행할 때마다 진짜 DB에 접근한다면 다음과 같은 문제가 발생한다.

  • 통신이 필요하기에 테스트가 느려진다.
  • 단위 테스트가 실패한 경우, 서비스 코드가 문제인지 DB의 문제인지 명확하게 파악해야한다.
  • 공유 의존성이 있기 때문에 테스트 실행 순서 바뀌면, 테스트 간 결과가 달라질 수 있다.
    • 순서 의존성을 파악하려면, 테스트 흐름을 추적해야 해서 디버깅이 더 어려워진다.
    • 병령 테스트가 불가능해진다.

이러한 문제를 해결하기 위해 진짜 DB 역할을 흉내내는 가짜 객체, 즉 Mock 객체를 사용한다.

 

2.  예시: 외부 API 호출

# main.py
from fastapi import FastAPI, Depends
from typing import Dict

app = FastAPI()

# 실제론 외부 API 호출
def get_weather_from_api() -> Dict[str, str]:
    print("🌧 외부 API 호출 중... (느림)")
    return {"location": "Seoul", "weather": "Rainy"}

@app.get("/weather")
def read_weather(weather: Dict[str, str] = Depends(get_weather_from_api)):
    return weather

위 코드에서 get_weather_from_api는 외부 API를 호출한다.

테스트 중 실제로 호출하면 느리고 불안정하다.

테스트 코드 (Mock 적용)

# test_main.py
from fastapi.testclient import TestClient
from unittest.mock import patch
from main import app

client = TestClient(app)

def test_read_weather_with_mock():
    mock_weather = {"location": "Busan", "weather": "Sunny"}

    with patch("main.get_weather_from_api", return_value=mock_weather):
        response = client.get("/weather")
        assert response.status_code == 200
        assert response.json() == mock_weather

핵심 포인트

get_weather_from_api 외부 API 호출 (실제론 느림, 불안정, 비용 발생 가능)
Depends(get_weather_from_api) FastAPI 라우터에 의존 객체로 주입됨
patch("main.get_weather_from_api") 테스트 중 이 객체를 가짜(mock)로 교체
효과 외부 API 없이도 빠르고 안정적으로 테스트 가능, 결과 통제 가능

 

3.  Mock이 항상 좋은 선택일까?

Mock은 유용하지만 무분별하게 사용하면 테스트의 신뢰도와 유지보수성에 문제를 일으킬 수 있다.

최근 검색 조회 서비스 기능은 그대로인데, 전체 데이터를 조회하던 방식을 1달치만 조회하도록 성능 개선했다.
하지만 mock이 기대하는 내부 호출 방식이 달라졌기 때문에 테스트가 실패한다.

즉, 기능(스펙)은 동일한데 내부 구조 변경만으로 테스트가 깨지는 상황이다.

이는 바람직하지 않다.

 

4.  좋은 테스트의 조건

좋은 단위 테스트는 다음의 특성을 가져야 한다.

구현 의존 특정 함수 호출을 mock으로 가로채서 검사 → 구조 바뀌면 테스트 깨짐
동작 의존 결과만 검증하고 내부는 신경 안 씀 → 구조 바뀌어도 테스트 통과
리팩터링 내성 기능이 동일하면 테스트는 깨지지 않아야 한다

Mock을 과도하게 사용하면 테스트가 내부 구현에 과도하게 의존하게 되어
작은 리팩터링에도 테스트가 쉽게 깨지는 구조가 된다.

 

내부 구현에 의존하는 Mock 기반 테스트 (❌ 좋지 않은 예)

# user_service.py
def get_user_name(user_id):
    user = db.fetch_user(user_id)  # DB 호출
    return user["name"]

# test_user_service.py
from unittest.mock import patch

def test_get_user_name():
    with patch("user_service.db.fetch_user") as mock_fetch:
        mock_fetch.return_value = {"id": 1, "name": "Alice"}
        assert get_user_name(1) == "Alice"

이 테스트는 db.fetch_user()가 호출된다는 구현 방식에 의존한다.

리팩터링 이후 문제

# user_service.py
def get_user_name(user_id):
    users = db.fetch_all_users()  # 여러 명 한 번에 조회
    return [u for u in users if u["id"] == user_id][0]["name"]

기능은 똑같은데, 내부적으로 fetch_user() → fetch_all_users()로 변경됨.
이제 테스트는 실패한다. 왜냐하면 mock한 함수 이름이 달라졌기 때문.

 

이게 바로 "Mock이 내부 구조에 의존하게 되어 테스트가 리팩터링에 취약"하다는 의미

 

하지만, 해당 조건을 지키는 좋은 테스트 코드가 어떤 것인지 현재는 와닿지 않는다....

그리고, 이러한 관점도 존재한다.

테스트를 정기적으로 실핸한다면, 마지막으로 한 수정을 알기 때문에 오히려 버그의 원인을 알아내는 것이 빠르다는 것이다.

 

5. Mock이 필수적인 상황

공유 의존성 (shared dependency)이 존재하는 경우 -> 테스트 순서에 따라 결과가 달라질 수 있기 때문

  • 테스트 간에 상태를 공유하고, 서로 결과에 영향을 줄 수 있는 의존성
  • 예: 데이터베이스, 전역 상태
  • 하지만, 테스트마다 도커로 새 DB 컨테이너를 띄우면 더 이상 공유 의존성이 아님 (격리된 상태)

 

+ TDD와 Mock

TDD(Test-Driven Development) 방식에서는 구현보다 테스트를 먼저 작성한다.

이 과정에서 실제 구현이 없기 때문에 내부 구조에 대한 가정을 할 수 없고, 자연스럽게 mock 사용이 줄어든다고 한다.

 

결론

Mock의 장점 - 외부 의존을 제거해 통신 과정이 제거되고, 테스트 속도가 빨라진다.
- 테스트가 실패했을 때, 해당 객체에서 명확하게 오류가 있다는 것을 파악할 수 있다.
Mock의 단점 - 과도한 사용 시 내부 구조에 의존하게 되어 테스트 유지보수가 어려워진다.

 

참고한 글

실전에서 TTD하기

단위-테스트의-두분파