TDD (25~28)

Young Min Sim ·2021년 6월 28일
0

TDD

목록 보기
5/6

테스트 주도 개발 패턴

💁🏻‍♂️ 어떻게 테스트 해야 될 것인지에 대한 이야기들

격리된 테스트

  • 전체 애플리케이션을 대상으로 하는 것보다는 좀더 작은 스케일로 하는게 좋다
  • 각각의 테스트는 다른 테스트와 완전히 독립적이어야 한다. 즉, 실행 순서에 독립적이어야 한다.
  • 이렇게 테스트를 격리하기 위한 작업은 결과적으로 시스템이 응집도는 높고 결합도는 낮은 객체의 모음으로 구성되도록 한다.

    의존성을 주입하고 작은 단위로 테스트하기 위해 책임을 나누고 테스트에 용이한 구조를 설계하다 보면 객체 간의 책임이 잘 나눠진다.

테스트 목록

  • TDD는 테스트 작성 - 최대한 빨리 통과 - 리팩토링 이 3단계를 거친다. 이 프로세스 한 번만에 기능을 완성하기는 어렵다.
  • 즉, 리팩토링 단계에서 구현을 하다 보면 새로운 테스트 케이스가 필요함을 알 수 있고 할 일 목록에 이를 작성한 뒤 테스트 작성 - 최대한 빨리 통과 - 리팩토링 이 3단계를 다시 거치는 식으로 계속해서 반복된다.

테스트 우선

  • 테스트는 언제 작성하는게 좋을까? -> (TDD책이니 당연하게도) 테스트 대상이 되는 코드를 작성하기 직전 작성하는 것이 좋다.
    • 코드를 작성한 후에는 테스트를 만들지 않을 확률이 높기 때문

      아래는 TDD에 대한 다른 사람들의 의견을 찾다가 더 나아가서 테스트 코드에 대한 의견 또한 보게 됐고 그 과정에서 찾은 토론입니다.
      즉, TDD 에 대한 토론은 아니고 '테스트 코드'를 작성해야 하는가? 에 대한 이야기입니다.

      '테스트 코드는 정상 케이스에 대한 검증만 수행하여 오류를 잡지 못한다' 라는
      테스트 코드에 대한 회의적인 의견

      그리고 그에 대한 답변
      -> 테스트코드가 없다면 개발 당시 당연하다고 생각했던 정상 케이스들을 몇 개월 뒤에 기억할 수 없고 다른 사람들은 모름.
      테스트 코드가 없으므로 간단한 수정이라고 생각했던 구현으로 인해 예상치 못한 버그가 발생할 가능성이 높고 배포까지 이어질 가능성도 있음.
      그렇기에 개발 시점에서는 필요성을 못 느낄 수도 있겠지만 이후를 위해서라도 작성하는 것이 좋다.

테스트 데이터

  • 1과 2사이에 어떠한 개념적 차이도 없다면 1을 사용하라
  • 즉, 세 항목만으로 동일한 설계와 구현을 이끌어낼 수 있다면 굳이 항목을 열 개나 나열할 필요는 없다.

    테스트 데이터가 단순히 많은게 중요한게 아니라 개념적 차이를 가진 다양한 테스트 케이스를 가지는 것이 중요하다는 뜻으로 보면 될 것 같습니다.

명백한 데이터

  • 데이터의 의도를 명확하게 표현해야 한다.
Bank bank = new Bank();
bank.addRate("USD", "GBP", STANDARD_RATE);
bank.commission(STANDARD_COMMISSION);
Money result = bank.convert(new Note(100, "USD"), "GBP");
assertEquals(new Note(49.25, "GBP"), result);

위와 같이 표현할수도 있지만, 좀 더 명확하게

Bank bank = new Bank();
bank.addRate("USD", "GBP", 2);
bank.commission(0.015);
Money result = bank.convert(new Note(100, "USD"), "GBP");
assertEquals(new Note(100 / 2 * (1 - 0.015), "GBP"), result);

와 같이 표현할 수도 있다.
단언 부분에 수식을 써놓으면 다음으로 무엇을 해야 할지 쉽게 알게 된다.
예제의 경우 어떻게든 나눗셈과 곱셈을 수행할 프로그램을 만들어야 한다는 것을 알게 되는 것이다.

명백한 데이터코드에 매직넘버를 쓰지 말라는 것에 대한 예외적인 규칙일 수 있다.

상수를 매직넘버로 다시 바꾸는건 그렇게 공감이 안 가지만, 49.25와 같은 숫자를 수식으로 바꾸는건 맞다고 생각합니다.


빨간 막대 패턴

💁🏻‍♂️ 테스트를 언제 어디서 작성할 것인지, 테스트 작성을 언제 멈출 것인지에 대한 것

시작 테스트

여러 연산을 필요로 하는 테스트보다는 최대한 간단한 테스트부터 작성하기 시작하라

앞선 예제에서도 나왔듯이 여러 개의 연산이 전제되는 복잡한 기능부터 구현하기 보다는 간단한 연산 하나하나부터 구현하는게 좋은 것 같습니다.

또 다른 테스트

  • 주제와 무관한 아이디어가 떠오르면 이에 대한 테스트를 할일 목록에 적어두고 다시 주제로 돌아올 것
  • '난 산만한 대화를 즐긴다' -> 대화를 엄격하게 한 주제로 묶는 것은 아이디어를 억압하는 최고의 방법이다.
    하지만 내가 가야 할 길을 놓치지 않는 것도 중요하기 때문에 작업 중 다른 아이디어가 떠오르면 일단 적어두고 하던 일로 다시 돌아가는 것 또한 중요하다.

테스팅 패턴

💁🏻‍♂️ 더 상세한 테스트 작성법에 대한 내용

자식 테스트

  • 지나치게 큰 테스트 케이스를 어떻게 돌아가도록 할 수 있을까?
    -> 현재 작성하고 있는 테스트 케이스의 깨지는 부분에 해당하는 작은 테스트 케이스를 작성하고 실행되도록 하라. 그 후에 다시 원래의 큰 테스트 케이스로 돌아가라

모의 객체

  • 비용이 많이 들거나 복잡한 리소스에 의존하는 객체를 테스트하려면 어떻게 해야 할까 ?
    -> 상수를 반환하게끔 만든 속임수 버전의 리소스를 만들면 된다.
  • 고전적인 예로는 DB가 있는데, 이 경우 마치 데이터베이스인 것처럼 행동하지만 실제로는 메모리에만 존재하는 객체를 통해 작성될 수 있다.
  • 이러한 모의객체모든 객체의 가시성(visibility)에 대해 고민하도록 하여 설계에서 커플링이 감소하도록 한다.

셀프 션트

  • 모의 객체를 만드는 대신 테스트 객체가 모의 객체 노릇을 하는 것
def testNotification(self):
  result = TestResult()
  listener = ResultListener()
  result.addListener(listener)
  WasRun("testMethod").run(result)
  asser 1 == listener.count

그리고 이벤트 통보 횟수를 셀 객체가 필요하다.

class ResultListener:
  def __init__(self):
    self.count = 0
  def startTest(self):
    self.count = self.count + 1

그런데 이렇게 별도의 리스너 객체를 만들기 보다는 테스트 객체 자체를 모의 객체로 사용할 수 있다.

def testNotification(self):
  self.count = 0
  result = TestResult()
  result.addListener(self)
  WasRun("testMethod").run(result)
  asser 1 == listener.count
def startTest(self):
  self.count = self.count + 1

이렇게 셀프 션트를 사용하여 작성한 테스트가 그렇지 않은 테스트보다 읽기에 더 수월하다. 통보횟수가 0이었다가 1이 됐다. 이 순서를 테스트에서 바로 읽어낼 수 있다.

깨진 테스트

혼자서 프로그래밍할 때 프로그래밍 세션을 어떤 상태로 끝마치는게 좋을까? 마지막 테스트가 깨진 상태로 끝마치는게 좋다.

프로그래밍 세션을 끝낼 때 테스트 케이스를 작성하고 이것이 실패하는 것까지 확인하고 다음 날부터 이어서 코딩을 시작한다. 그러면 다음 날 어느 작업부터 시작할 것인지 명백히 알 수 있고 시간을 절약할 수 있다.


초록 막대 패턴

💁🏻‍♂️ 최대한 빨리 테스트를 성공시키기 위한 패턴들

가짜로 구현하기(진짜로 만들기 전까지만)

여지껏 읽은 책 내용을 토대로 이해하자면, 테스트를 빨리 통과시켜야 한다는 이유로 모든 것을 가짜로 구현할 필요는 없고 한 번에 진행하기에 너무 큰 작업인 경우에 우선 가짜로 구현 후 테스트가 통과함을 확인하고 리팩토링을 진행하면 될 것 같다.

삼각측량

테스트 케이스를 2개 이상 만들어 가짜로 구현한 메서드를 추상화 하기 위한 방법.

두 정수의 합을 반환하는 함수를 작성하고 싶다고 가정할 때 다음과 같이 작성할 수 있다.

public void testSum() {
  assertEquals(4, plus(3,1));
}

private int plus(int augend, int addend) {
  return 4;
}

삼각 측량을 사용해서 바른 설계로 간다면 다음과 같이 테스트 케이스를 하나 더 추가해야 한다.

public void testSum() {
  assertEquals(4, plus(3,1));
  assertEquals(7, plus(3,4));
}

이렇게 되면 테스트가 실패하게 됨으로써 아래와 같이 어떻게 추상화 해야 하는지 실마리를 찾을 수 있다.

private int plus(int augend, int addend) {
  return augend + addend;
}

그래서 어떻게 해야 가짜로 구현한 메서드를 올바르게 추상화할 수 있을지 감잡기 어려울 때삼각측량을 사용한다.

예제가 너무 간단해서 와닿지는 않지만 다양한 테스트 케이스를 추가함으로써 어떻게 추상화해야 하는지 알기 위한 기법 정도로 정리할 수 있을 것 같습니다.

명백한 구현

덧셈, 뺄셈 정도의 간단한 연산의 경우 굳이 가짜 구현을 사용할 필요가 없다. 하지만 '제대로 동작하는' '깨끗한 코드' 라는 두 개의 문제를 한 번에 만족하기 어려운 경우 우선 가짜 구현을 통해 '제대로 동작하는'이라는 문제를 해결한 후 리팩토링을 통해 '깨끗한 코드' 문제를 해결하는 것이 좋다.

0개의 댓글