TDD 정리 1탄

한강섭·2025년 4월 21일
0

학습 & 숙제

목록 보기
70/103
post-thumbnail

TDD(Test Driven Development)


🔥 Motivation

  • 코드 품질 저하
  • 프로젝트 리스크 증가
  • 제품 양산 이후 결함(Defects) 폭증
    말이 안 되는 일이다.

✅ Unit Test

단위 테스트(Unit Test)
: 하나의 작은 단위(함수, 메서드, 클래스)가 제대로 작동하는지 확인하는 테스트 코드 → TDD의 핵심

TDD(Test Driven Development)
: 개발보다 테스트 코드를 먼저 작성하는 개발 방법론

TDD 프로세스

  1. Red - 실패하는 테스트 코드 작성
  2. Green - 테스트가 통과하도록 최소한의 코드 작성
  3. Refactor - 테스트가 통과한 상태에서 코드 리팩토링

📌 개발 프로세스 비교


🎯 단위 테스트의 범위

  1. 하나의 함수 단위 테스트
  2. 여러 함수의 조합 테스트
  3. 통합 테스트까지 확장 가능

🔄 개발 플로우와 테스트의 연결

  1. 요구사항 분석: Input / Condition → Output (자연어 기반)
  2. SW 설계: 테스트 가능한 설계(Testable Design), 모듈화, 인터페이스 분리
  3. 구현: TDD 또는 단위 테스트 기반 개발
  4. SW 테스트: 사용자 관점 수용 테스트 (실패 시 설계로 회귀)
  5. 통합: 빌드 및 통합 테스트
  6. 배포: QA 테스트

🧪 TDD 실습 예제 (Java 버전 및 설명)


1️⃣ 첫 번째 방법: 테스트 도입 전 기본 코드

class Human {
    private int getTemperature() {
        return SystemEnvironment.getTemperature();
    }

    public String getFeeling() {
        int temp = getTemperature();
        if (temp > 30) return "hot";
        else if (temp < 10) return "cold";
        else return "good";
    }
}

➡ 위 코드는 외부 환경(System)에 직접 의존하고 있어 테스트가 불가능하다.


1️⃣-2 테스트 코드 작성

void expectEqual(String expected, String actual) {
    if (!expected.equals(actual)) {
        System.out.printf("Error: %s != %s\n", expected, actual);
    }
}

void testHuman() {
    Human human = new Human();
    expectEqual("good", human.getFeeling());
}

🔍 문제점: 외부 의존성이 있어 getFeeling()의 결과가 항상 일관되지 않을 수 있음 → Mock 필요


2️⃣ 두 번째 방법: 상속을 활용한 Fake(Mock) 객체 주입

class Human {
    protected int getTemperature() {
        return SystemEnvironment.getTemperature();
    }

    public String getFeeling() {
        int temp = getTemperature();
        if (temp > 30) return "hot";
        else if (temp < 10) return "cold";
        else return "good";
    }
}
class HumanTest extends Human {
    private int fakeTemperature;

    public HumanTest(int temp) {
        this.fakeTemperature = temp;
    }

    @Override
    protected int getTemperature() {
        return fakeTemperature;
    }
}

void testFakeHuman() {
    HumanTest human = new HumanTest(15);
    expectEqual("good", human.getFeeling());
}

✔ 상속으로 메서드를 오버라이드하여 테스트 대상 메서드 내부 동작 제어 가능


3️⃣ 세 번째 방법: 시스템 객체 주입 (Wrapper)

interface SystemInterface {
    int getTemperature();
}

class Human {
    private final SystemInterface system;

    public Human(SystemInterface system) {
        this.system = system;
    }

    public String getFeeling() {
        int temp = system.getTemperature();
        if (temp > 30) return "hot";
        else if (temp < 10) return "cold";
        else return "good";
    }
}
class FakeSystem implements SystemInterface {
    private int temp;

    public void setTemperature(int temp) {
        this.temp = temp;
    }

    @Override
    public int getTemperature() {
        return temp;
    }
}

void testSystemWrapper() {
    FakeSystem fake = new FakeSystem();
    fake.setTemperature(15);
    Human human = new Human(fake);
    expectEqual("good", human.getFeeling());
}

➡ 시스템 객체를 추상화하여 테스트에 유연한 구조 도입


4️⃣ 네 번째 방법: 의존성 역전 원칙 (DIP)

interface InputDevice {
    String getInput();
}

class Keyboard implements InputDevice {
    public String getInput() {
        return "keyboard input";
    }
}

class Computer {
    private final InputDevice inputDevice;

    public Computer(InputDevice inputDevice) {
        this.inputDevice = inputDevice;
    }

    public void input() {
        System.out.println(inputDevice.getInput());
    }
}
class FakeKeyboard implements InputDevice {
    public String getInput() {
        return "fake keyboard input";
    }
}

void testDIP() {
    Computer computer = new Computer(new FakeKeyboard());
    computer.input();  // 결과: fake keyboard input
}

✔ DIP 적용 → 테스트 시 진짜 키보드 대신 FakeKeyboard 사용


4️⃣-1 플랫폼 레벨 Fake 적용

class SystemTemperature {
    private static final SystemTemperature instance = new SystemTemperature();
    private int temperature;

    private SystemTemperature() {}

    public static SystemTemperature getInstance() {
        return instance;
    }

    public void setTemperature(int temp) {
        this.temperature = temp;
    }

    public int getTemperature() {
        return this.temperature;
    }
}

class Human {
    private int getTemperature() {
        return SystemTemperature.getInstance().getTemperature();
    }

    public String getFeeling() {
        int temp = getTemperature();
        if (temp > 30) return "hot";
        else if (temp < 10) return "cold";
        else return "good";
    }
}

void testPlatformFake() {
    SystemTemperature.getInstance().setTemperature(15);
    Human human = new Human();
    expectEqual("good", human.getFeeling());
}

➡ 플랫폼 전역 싱글톤 객체로 상태 제어


😵 Reality - 피할 수 없는 현실

  • 많은 제품군의 일정 압박
  • 코드 수정 시 복잡한 프로세스
  • 과도한 결함 할당
  • 시장에서 발생한 이슈 보고
  • 심리적 외로움

🛡️ 생존 전략 - 테스트 코드는 10배 더 작성하라

  • 실수 방지 → 무조건 통과되는 코드만 배포
  • 자동화 테스트로 품질 유지
  • 무의미한 end-to-end 테스트는 제거
  • 내부 상태 변화까지 포함한 테스트 전략 수립
  • TDD는 항상 리팩토링을 동반
  • QA 테스트 전에 테스트 완결이 이상적

TDD는 귀찮고 어려운 방식처럼 느껴질 수 있지만,
장기적으로 개발자 자신을 지키는 가장 강력한 무기가 됩니다.


"코드는 거짓말하지 않는다. 테스트는 진실을 말한다."

profile
기록하고 공유하는 개발자

0개의 댓글