티스토리 뷰

TDD(Test Driven Development)란?

  • TDD = TFD(Test First Development) + 리팩토링

TDD는 테스트 주도 개발이다. 테스트를 먼저 진행하고 리팩토링을 진행하는 것을 합쳐서 TDD라고 한다. 코드는 기능 구현을 위한 Production Code와 테스트를 위한 Test Code로 나눌 수 있다. 테스트 주도 개발은 컴파일되는 Test Code를 먼저 작성한 뒤, 테스트가 통과할 수 있게 Production Code를 작성한다. 그 후, 코드를 리팩토링해나가는 방식이다.

TDD를 하는 방법

  1. 컴파일되는 실패하는 테스트 코드를 구현한다.
  2. 테스트가 성공하도록 프로덕션 코드를 구현한다.
  3. 프로덕션 코드, 테스트코드를 리팩토링한다.

컴파일되는 실패하는 테스트코드를 구현하기 때문에 요구사항에 대한 파악을 통한 구조는 어느정도 구상이 되어있어야 한다. 어떤 클래스를 만들고 의존관계를 어떻게 해주는 것이 좋은지 정도는 구상이 되어 있어야 TDD를 하기 편해진다. 설계를 test(검증)하며 구체화나가는 과정이라고 생각한다. (하지만 TDD가 시간이 꽤 오래 걸리기 때문에 실제로는 프로덕션에 대한 테스트 코드를 작성하는 방식을 많이 취하는 것 같다.)

TDD를 하는 이유

  1. 안정감을 준다.
    각각의 단위 테스트를 진행하면 테스트가 통과한다는 것은 동작하는데 큰 어려움이 없다는 것을 의미한다. 리팩토링을 진행할 때, 테스트 코드가 통과되면 큰 버그의 발생이 일어나지 않을 것이라는 안정감을 준다. 또한, 버그가 발생한다면 어떤 부분에서 발생하는지 파악이 빨라진다. 테스트 코드를 작성한다고 하여 모든 가능성에 대한 예방책이 되는 것은 아니나 오류의 가능성을 많이 줄여준다.
  2. 많은 시도와 빠른 피드백이 가능하다.
    안정된 상태에서 더 많은 도전이 가능하다. 실패해도 결국 안정된 상태를 금방 찾을 수 있기 때문이다. 따라서 테스트 코드가 주는 안정감은 더 많고 다양한 시도를 가능하게 한다. 시도에 따른 성공, 실패여부에 대한 피드백을 빠르게 받음으로써 시도에 따른 개선을 빠르게 수정해나갈 수 있다. 결국 더 창의적인 시도가 가능하다.
  3. 좋은 설계인지 판단하는 기준이 될 수 있다.
    좋은 설계는 테스트 코드 작성이 쉽다. 즉, 테스트 코드 작성이 어렵다는 것은 테스트를 하기 어려운 경우일수도 있으나 좋은 설계가 아닐 수 있다는 것을 암시하는 것일수도 있다. 테스트 코드를 고민하면 좀 더 작은 범위에서 메소드와 객체의 역할과 책임에 집중할 수 있게 된다.

 

 

테스트 활용하기

테스트를 한다는 것은 내가 원하는 조건에서 원하는 결과가 나오는지를 파악하는 것이다. 따라서 테스트에서는✨ input과 output을 정의하는 것✨이 중요하다.

  • 메소드 단위로 단위테스트를 진행

    메소드 단위로 테스트를 진행하므로써 하나의 역할을 하는 메소드로 분리가 가능하며, 각각의 역할에 테스트를 통해 확신할 수 있다. input, output으로 테스트를 진행하기 때문에 보다 순수한 상태의 함수가 만들어지며 보다 side effect를 줄이는 코드 작성이 가능하다.

  • 학습테스트

    새로 학습하는 API나 프레임워크의 기능을 테스트 해보면서 학습하는 방법이다.

  • 경계값을 기준으로 테스트 진행

    다수의 경우 테스트의 실패를 가르는 경계값의 결과가 궁금할 것이다. 경계값 또는 의심되는 값을 기준으로 테스트를 진행하자.

  • given, when, then

    구체적인 상황에 대한 행동방식을 테스트하는 방식을 대표한다.

    • given : 행동을 하기 전에 상태를 설명하는 부분이다.
    • when: 특정한 행동을 하게되는 상황을 의미한다.
    • then: 특정상황에서의 행동이 예상되는대로 이루어지는지 검증한다.
  • Test Fixture

    테스트 하기 위한 객체들을 고정시켜 놓는 것을 의미한다. 여러 테스트에서 고정된 상태가 필요하다면 필드로 선언하거나 @BeforeEach등을 사용하여 객체들을 고정시켜 놓을 수 있다.

Private 메소드를 테스트 해야된다면?

  1. public을 통해 간접 테스트를 진행한다.
  2. private을 테스트하는 것은 다른 클래스로의 분리가 필요하다는 반증일 수 있다. 객체의 역할과 설계에 대해 다시 생각해 볼 수 있다.

테스트할 코드의 범위를 늘리자

다양한 생성자로 객체 활용성 높이기

테스트 상황을 작성하다 보면 다양한 생성자로 객체를 생성해야할 필요를 느끼게 된다. 다양한 생성자로 그 객체의 활용방법을 높일 수 있다. 이런 방식에서 Builder 패턴이 유용하게 사용되기도 한다.

테스트할 코드와 아닌 코드를 분리하기

테스트하기 어려운 코드를 알아낼 수 있어야 한다. 난수생성과 같은 랜덤값 생성 메소드를 활용하게 되면 그 코드는 테스트하기 어려워진다. 테스트하기 어려운 메소드를 최대한 외곽으로 보내 테스트 가능한 영역을 넓힐 수 있다.

 

 

객체간의 의존성이 강해지면 테스트하기 어려워진다. 객체간 의존성을 줄이면 객체가 보다 독립적으로 존재하게 되며, 테스트하는 범위가 늘어난다. 의존성을 줄이기 위한 방법 중 하나로 인터페이스 사용이 가능하다.

전략 패턴(Strategy Pattern)이란?

실행중에 구현 알고리즘을 선택할 수 있게 하는 패턴이다. 객체들이 할 수 있는 행위에 대해 각각의 클래스를 생성하고, 유사한 행위를 캡슐화하는 인터페이스를 정의한다.

// 유사한 행위를 하는 인터페이스로 정의
public interface LottoGenerator {
    LottoTicket generateLottoTicket();
}

// random하게 LottoTicket을 생성하는 클래스
public class RandomLottoGenerator implements LottoGenerator {
    private static final Random random = new Random();

    @Override
    public LottoTicket generateLottoTicket() {
        Set<LottoNumber> numbers = new HashSet<>();
        while (numbers.size() != LottoTicket.SIZE_OF_LOTTO_NUMBERS) {
            numbers.add(getRandomLottoNumber());
        }
        return new LottoTicket(new ArrayList<>(numbers));
    }

    private LottoNumber getRandomLottoNumber() {
        return new LottoNumber(random.nextInt(LottoNumber.MAXIMUM_NUMBER) + 1);
    }

}

// 고정된 값으로 LottoTicket을 생성하는 클래스
public class FixedLottoGenerator implements LottoGenerator {
    @Override
    public LottoTicket generateLottoTicket() {
        return new LottoTicket(Arrays.asList(
            new LottoNumber(1),
            new LottoNumber(2),
            new LottoNumber(3),
            new LottoNumber(4),
            new LottoNumber(5),
            new LottoNumber(6)
        ));
    }

}

로또가 필요한 곳에 인자로 LottoGenerator 를 받고 원하는 상황에 따라 new RandomLottoGenerator() 를 주입할수도 new FixedLottoGenerator() 를 주입할 수도 있다. LottoGenerator 라는 인터페이스를 구현한다면 상호 교체가 가능하다. 실행중에 어떤 클래스를 주입해주냐에 따라 구현알고리즘이 선택된다.

DI(Dependency Injection)란?

필요한 객체를 직접 생성하는 것이 아니라 의존 객체를 주입받는 것을 의미한다. 사용하는 자원에 따라 동작이 달라진다면 인스턴스를 생성할 때, 생성자에 필요한 자원을 넘겨주면(주입하면) 된다. 해당 자원이 필요할 때 받아 사용함으로써 유연한 코드로 재사용이 높아지며, 테스트가 용이해진다. 구성의 책임과 사용의 책임을 분리시키는 것이다.

 

의존성을 주입하는데는 3가지 방법이 있다. 1. 생성자로 주입 2. setter로 주입 3. 인터페이스를 통한 주입 이다. 전략패턴도 의존성 주입을 사용하고 있는 것이다.

 

이펙티브자바에서는 인터페이스를 통한 주입은 Supplier<T> 를 사용하는 것을 예시로 든다.

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

Supplier는 인자를 받지 않으면서 T 타입을 리턴하는 get() 메소드를 구현한다.

public class DiTest {
    @Test
    void testDI(){
        Supplier<String> sayHi = () -> "Hi";
        Supplier<Integer> getAge = () -> 20;

        print(sayHi); // Hi
        print(getAge); // 20
    }

    private <T> void print(Supplier<T> word){
        System.out.println(word.get());
    }
}

정리

TDD를 처음 시작했고, 작은 미션에서만 진행했지만 TDD의 효과를 일부 경험할 수 있었다. 사실 테스트 코드를 배우고 싶었던 이유는 내가 생각한 모든 예외상황들을 테스트해보기 위해 수정할때마다 입력하는게 너무 귀찮아져서 였다. 테스트 코드를 작성하면 이것을 자동화하여 한번 셋팅하면 테스트가 판단해준다. 지금 단계에서 내가 가장 편리함을 느꼈던 부분이다. 아무리 여기저기 리팩토링을 해도 테스트가 통과된다면 일단 큰 문제가 없음에 안심할 수 있고, 문제가 생기면 어디서 발생하는지 파악이 빨라진다.나는 좋은 설계를 모른다. 😅 하지만, 좋은 설계는 테스트코드를 작성하기 쉽다고 한다. 많은 범위를 테스트 하기 위한 방식을 고민하다보면 좋은 설계에 대해 알아갈 수 있다.

참고자료

TDD 설명된 참고 블로그

given,when,then

전략패턴 위키

의존성주입 위키

우아한테크코스 수업

이펙티브자바 - 아이템5

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함