우리가 TestSmith를 만든 이유: 아무도 이야기하지 않는 테스트 커버리지 문제

발행: (2026년 5월 24일 AM 01:19 GMT+9)
11 분 소요
원문: Dev.to

Source: Dev.to

제가 참여한 모든 팀에서는 언젠가 같은 대화를 나눴습니다. 누군가가 커버리지 리포트를 열어 빨간색이 가득한 화면을 보고 “어떻게 하면 이걸 올릴 수 있을까?”라고 묻습니다. 답은 언제나 “테스트를 더 작성해야 한다”는 식이며, 그 뒤에는 긴 침묵이 흐릅니다. 왜냐하면 모두가 그 말이 실제로는 무엇을 의미하는지 알고 있기 때문입니다 — 의미 있는 어설션 하나를 쓰기 전에 수많은 보일러플레이트, 테스트 파일 설정, 목(mock) 연결, 그리고 픽스처 스캐폴딩에 몇 시간을 투자해야 합니다.
그것이 TestSmith가 해결하고자 만든 문제입니다.

개발자는 일반적으로 테스트를 작성하고 싶어합니다. 저항의 원인이 게으름이 아니라 설정 비용입니다. 테스트하고 싶은 새로운 모듈마다 다음 작업을 해야 합니다.

  1. 올바른 위치와 네이밍 규칙에 맞게 테스트 파일을 생성한다
  2. 테스트 대상 모듈을 import한다
  3. 테스트 프레임워크와 필요한 목 라이브러리를 import한다
  4. 외부 의존성을 위한 픽스처를 설정한다
  5. 프레임워크가 기대하는 보일러플레이트 클래스·함수 구조를 만든다
  6. 마침내 실제 테스트 로직을 작성한다

입출력이 명확한 잘 이해된 모듈이라면 1~5 단계가 6 단계보다 오래 걸릴 수 있습니다. 의미 있는 작업을 하기 전에 청소 작업을 먼저 해야 하는 것이죠. 그리고 이미 큰 코드베이스에 커버리지를 추가해야 하는 경우(모든 팀이 결국 마주하게 되는 커버리지 추격 프로젝트)에는 이 설정 작업을 수십·수백 번 반복하게 됩니다.

우리는 가장 직관적인 이유 때문에 첫 번째 TestSmith 버전을 파이썬으로 만들었습니다. 바로 우리의 당면 과제가 파이썬 코드베이스였기 때문이죠.

하지만 파이썬은 도구 자체에도 잘 맞았습니다. 파이썬의 ast 모듈은 뛰어납니다 — ast.parse()만으로 몇 줄이면 전체 파스 트리를 얻을 수 있고, 이를 순회하면서 클래스 이름, 함수 시그니처, import 문을 추출하는 것이 간단합니다. 실제 코드를 실행하지 않고도 소스 코드 구조를 이해해야 하는 도구에 정적 AST 분석은 딱 맞으며, 파이썬 표준 라이브러리 덕분에 구현도 쉽습니다.

import ast

tree = ast.parse(source_code)
for node in ast.walk(tree):
    if isinstance(node, ast.FunctionDef):
        if not node.name.startswith('_'):  # skip private
            public_functions.append(node.name)

두 번째 이유는 빠른 반복이 가능했기 때문입니다. 우리는 우리 스스로의 문제를 해결하고 있었고, 파이썬 개발자였기 때문에 파이썬으로 구현하면 첫 날부터 자체 프로젝트에 바로 적용해 볼 수 있었습니다. 이는 거친 부분을 빨리 잡아내게 하는 강력한 강제 요소가 됩니다.

핵심 아이디어는 단순합니다: 소스 파일을 주면, 손으로 직접 작성했을 법한 테스트 스캐폴드를 자동으로 생성한다.

예를 들어 다음과 같은 파이썬 서비스가 있다면:

# src/services/payment.py

class PaymentService:
    def __init__(self, stripe_client, db):
        self.stripe = stripe_client
        self.db = db

    def process_payment(self, order_id: str, amount: int) -> dict:
        ...

    def refund(self, payment_id: str) -> bool:
        ...

TestSmith는 다음과 같은 파일을 만들어 냅니다:

# tests/services/test_payment.py

import pytest
from unittest.mock import MagicMock, patch
from src.services.payment import PaymentService


@pytest.fixture
def stripe_client():
    return MagicMock()


@pytest.fixture
def db():
    return MagicMock()


@pytest.fixture
def payment_service(stripe_client, db):
    return PaymentService(stripe_client=stripe_client, db=db)


class TestPaymentService:
    def test_process_payment(self, payment_service):
        # TODO: implement
        pass

    def test_refund(self, payment_service):
        # TODO: implement
        pass

완전한 테스트는 아니지만, 스캐폴드가 완성된 상태입니다. 파일 위치, import 문, 생성자 의존성에 대한 픽스처 설정, 그리고 테스트 메서드까지 모두 자동으로 배치됩니다. 이제 개발자는 어설션 로직만 채우면 됩니다. 청소 작업은 이미 끝난 것이죠.

도구는 또 쉽게 실수하기 쉬운 부분을 자동으로 처리합니다. 예를 들어 테스트 파일이 소스 파일에 비해 어느 위치에 있어야 하는지(프레임워크·프로젝트 관례에 따라 다름), 생성자 파라미터에 기반한 픽스처 이름 지정, 어떤 목 라이브러리를 사용할지, 그리고 소스가 클래스 기반인지 함수 기반인지에 따라 테스트 클래스를 만들지 함수로 만들지 등을 자동으로 결정합니다.

커버리지 리포트는 무엇이 테스트되지 않았는지 알려 주지만 우선순위를 제시하지는 못합니다. 간단한 유틸 함수 3개가 있는 파일과 복잡한 결제 파이프라인 파일이 모두 “커버되지 않음”으로 표시됩니다. 어느 쪽부터 접근할지는 코드를 직접 읽어야만 알 수 있죠.

TestSmith는 여기서 한 걸음 더 나아가 커버리지 갭 명령을 추가했습니다. 각 미테스트 모듈이 얼마나 많은 다른 모듈에 import 되었는지를 기준으로 결합도(coupling) 점수를 계산합니다. 10개의 모듈이 import 하는 모듈은 전혀 import되지 않은 모듈보다 우선순위가 높습니다 — 왜냐하면 많이 사용되는 모듈에 버그가 있으면 파급 효과가 크기 때문이죠.

$ testsmith gaps

Coverage gaps (by coupling score):

  src/services/payment.py        coupling: 8   functions: 5 fix this first
  src/utils/currency.py          coupling: 6   functions: 3
  src/models/order.py            coupling: 4   functions: 7
  src/scripts/backfill.py        coupling: 0   functions: 12

이렇게 하면 팀은 “어디서 시작해야 할까?”라는 질문에 원칙적인 답을 얻을 수 있었고, 누군가가 코드베이스를 일일이 검토할 필요가 없었습니다.

도구는 실제로 효과가 있었습니다. 팀들은 사용하면서 가치를 체감했죠. 하지만 시간이 지나면서 두 가지 문제가 명확해졌습니다.

  • 배포가 고통스러웠다. pip install testsmith는 겉보기에 간단해 보이지만, 실제로는 파이썬 버전 관리, 가상 환경, 의존성 충돌 등을 다뤄야 했습니다. 특히 CI 환경에서는 더욱 번거로웠습니다. CI에서 동작하려면 자체 설정이 필요하니, 테스트 도구가 스스로를 방해하는 셈이었습니다.
  • 하나의 언어만으로는 부족했다. 도구가 존재한다는 소문이 퍼지자 모든 팀이 “TypeScript에서도 동작하나요?” 혹은 “Java도 지원하나요?”라고 물었습니다. 파이썬 전용 설계는 의도된 선택이 아니라 당장의 문제를 해결하기 위한 부수적인 결과였을 뿐이었습니다. 그러나 아키텍처가 언어별 로직을 핵심 코드에 뒤섞어 두었기 때문에 새로운 언어를 추가하기가 어려웠습니다.

이 두 문제를 해결하고자 우리는 Go로 v2를 재작성했습니다. Go는 단일 정적 바이너리 형태로 어떤 환경에도 바로 떨어뜨릴 수 있고, 플러그인 아키텍처를 통해 각 언어를 독립된 드라이버로 격리했습니다.

하지만 이것은 다음 포스트에서 다루겠습니다.

TestSmith는 오픈소스로 github.com/orieken/testsmith에서 확인할 수 있습니다. v1 파이썬 패키지는 참고용으로 archive/v1/에 보관되어 있습니다.

0 조회
Back to Blog

관련 글

더 보기 »

내 스킬

프로젝트를 위한 AI 지시문을 만들고, 설치하고, 관리하세요 — 코딩이 필요 없습니다. CREATE 이름을 정하고, 카테고리를 선택하고, 원하는 것을 설명하세요 — 마법사가 자동으로 구성합니다.