테스트 주도 개발 3부(ch 25-32)

AUSG

TDD

12/22/2020


본 글은 켄트 백의 <테스트 주도 개발>을 읽고 개인적으로 정리한 내용입니다. 내용에 오류가 있을 시 지적해주시면 감사하겠습니다.

25장 테스트 주도 개발의 (디자인) 패턴

베이스

  • 각각의 테스트는 다른 테스트와 완전히 독립적이어야 한다
  • 시스템은 응집도는 높고 결합도는 낮은 객체의 모음이어야 한다

1. 테스트 목록을 짜기

  1. 목표를 위해 달성해야하는 항목으로 To-do list를 작성한다.
  2. To-do list의 각 항목에 대해 구현해야하는 테스트를 목록에 붙여 적는다.
  3. 각 테스트마다 구현해야하는 모든 테스트 케이스를 적는다.

(4. 아직 구현하지 않은 테스트에 대해서는 아무것도 하지 않는 버전(null 오퍼레이션)을 적어둔다)

2. 하나의 테스트를 작성하는 순서

  • 단언(Assert) 우선. 단언문을 가장 먼저 쓰고 시작하자.

시스템을 개발하려면, 완료된 시스템의 유저 스토리부터 작성한다.

유저 스토리의 기능을 개발하려면, 해당 기능이 완료되었을 때 통과할 테스트부터 작성한다.

테스트를 작성하려면, 해당 테스트가 완료되었을 때 통과할 단언(테스트케이스)부터 작성한다.

3. 테스트 데이터의 원칙

  • 데이터에 차이가 있다면, 그것은 어떤 의미를 담고있어야 한다. 1과 2의 차이가 없다면 1을 이용하라.
  • 여러 의미를 담는 동일한 상수를 사용하지 마라. 예를 들어, 두 인자로 (1,1)을 사용하는 경우와, (1,2)를 사용하는 경우를 생각해보자. 1+1나 1+2는 두 인자의 순서가 바뀌어도 문제가 없다. 그러나, 1/1과 1/2의 경우 인자의 순서가 바뀌면 결과값이나 에러의 종류도 달라진다. 따라서, 제수와 피제수 역할을 하는 수는 각각 다른 숫자로 표현되는 것이 좋다. (1은 dividend, 2는 divider라는 식으로)
  • 대안으로, 실제 데이터를 사용해볼 수도 있다.

26장 빨간 막대 패턴

다음으로 구현할 테스트를 고르는 법

  • 처음으로 개발할 테스트(시작 테스트)라면...
    • 오퍼레이션이 아무것도 하지 않는 경우, 즉 입력과 출력이 같은 경우부터 만들것
    • 그게 너무 뻔하다면, 오퍼레이션의 입력이 적은 경우부터 만들 것
  • 테스트를 만들었고, 다음 테스트를 골라야 하는 상황이라면...
    • 뻔하지 않으면서 구현할 수 있다는 확신이 있는 테스트부터 만들것

테스트를 사용하는 방법

설명 테스트

  • 팀 차원에서 테스트를 사용하도록 퍼뜨리고 싶다면, 설명을 하거나 요구할 때 테스트를 예시로 들 것.
  • 또는, 기존의 시퀀스 다이어그램을 테스트 케이스의 집합으로 바꿔서 작성해볼것.

학습 테스트

  • 외부 라이브러리나 소프트웨어를 사용할 경우에도 예상한 대로 라이브러리가 동작하는 지 확인하기 위해서 학습 테스트를 작성하는 것이 좋다.
    • 내가 제대로 라이브러리의 동작법을 이해했는지 테스트하기 위해서
    • 라이브러리의 버전 업 시 이전과 다르게 동작할 수 있어서

회귀 테스트

  • 처음 코딩할 때 작성하는 것이 가장 좋음
  • 시스템 장애로 인해 실패하는 테스트
  • 시스템 장애를 통과할 경우 장애가 수정되었음을 확인시켜주는 테스트

27장 테스팅 패턴

상세한 테스트 작성법

자식 테스트

  • 커다란 테스트의 깨지는 부분에 하위 테스트(자식 테스트)를 만들어 테스팅하라.
  • 그렇게 해서 하위 테스트들이 차례차례 해결되면, 커다란 테스트 케이스가 해결되도록 만들어라.

Mock Object

  • DB와 같이 비용이 많이 들거나 복잡한 리소스에 의존하는 객체를 테스팅하려면, 그걸 본딴 Mock 객체를 만들어 테스팅하라.
  • 사실적인 데이터가 있는 DB를 테스팅하면, 오히려 데이터가 복잡해 가독성이 떨어지기도 한다.
  • 물론, Mock 객체가 진짜 객체와 동일하게 동작하는지도 테스팅 해야한다(...)

Self Shunt(자신으로 되돌아오는 루프백 테스트)

로그 문자열

  • 메시지의 호출 순서가 올바른지 확인하기 위하여 사용한다.
  • 각 메서드가 호출될 때마다 로그 문자열에 메서드명을 추가하도록 만든다.

크래시 테스트 더미(Crash Test Dummy)

  • 잘 호출될 것 같지 않은 에러를 테스팅할 때에는, 그냥 예외를 발생시키기만 하는 특수한 크래시 객체를 만들어 호출한다.
  • 예를 들어 파일 시스템이 여유공간이 없는 경우를 테스팅하기 위해 실제로 파일 시스템을 꽉 채우는 것은 낭비이다. 그냥 IOException 예외 객체를 호출하는 편이 낫다.

깨진 테스트 남겨놓기 - 혼자서 프로그래밍 할 때

  • 다음에 작업 수행 시 할 일을 바로 발견할 수 있도록 깨진 테스트 하나를 두고 프로그래밍을 마친다.
  • 글쓰기를 할 때, 문장을 끝마치지 않고 두었다가 다음에 와서 작성하는 것과 비슷하다.
  • 테스트를 해결하고싶은 생각때문에, 이전의 테스트 맥락을 즉각적으로 떠올려 바로 작업에 돌입할 수 있도록 해준다.

깨끗한 체크인 - 팀 프로그래밍을 할 때

  • 자신의 마지막 코딩 이후 누가 코드를 건드렸는지 모르므로, 다음에 돌아오더라도 맥락을 어차피 정확히 파악할 수 없다.
  • 따라서, 다음 사람에게 최대한 상황을 확실하게 만들기 위해 반드시 모든 테스트가 통과한 상태로 프로그래밍을 마친다.
  • 테스트 슈트(Test Suite)가 실패하는데 끝마쳐야한다면, 그냥 당신이 이번에 작업한 모든 테스트를 다 지워버려라. 퇴근은 해야할 것 아닌가

28장 초록 막대 패턴

코드를 통과하게 만들기 위해 사용하는 패턴

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

  • 실패하는 테스트를 만들었다면, 일단 반환 값을 상수로 만들어라.
  • 테스트가 통과하면 단계적으로 상수를 변수로 리팩토링하라.
  • 이는 테스트가 통과하므로 심리적으로도 안정되고, 프로그래머가 현재 문제에만 집중하도록 도와준다.
  • 리팩토링이 끝나고 나면, 기존의 가짜 코드는 아마 다 사라져있을 것이다.

삼각측량

  • 예가 2개 이상일 때만 삼각측량법을 이용하여 추상화하라.
  • 추상화가 끝난 뒤, 만약 두 예시 중 중복이 있다면 하나를 삭제하고 상수 반환으로 단순화할 수도 있을 것이다.
  • 즉, 삼각측량은 추상화를 하긴 해야하는데, 어떻게 해야 올바르게 할 수 있을지 모르겠을 때 사용하는 방식이다.
  • 그 외에는 되도록 삼각 측량 대신, 명백한 구현 또는 가짜로 구현하기에 의존하자.

명백한 구현

  • 가짜로 구현하기나 삼각측량처럼 코드의 방향이 확실하지 않을 때와 달리, 뭘 해야할 지 정확히 알고있다면 그냥 그렇게 코드를 짜라.
  • 가짜로 구현하기나 삼각측량은 정확한 코드 타이핑보다 훨씬 작고 느린 step이다.
  • 확신하는 것에 대해서는 명백히 구현하되, 어느 순간 코드가 생각하는 대로 짜지지 않으면, 그 때 가짜로 구현하기나 삼각측량을 사용하라.

29장 xUnit 패턴

단언

  • 결과를 평가할 때 체크해야할 점들을 모조리 boolean으로 체크할 것.
  • 단언을 작성할 때, 구체적인 값을 명시 할 것(예외를 피하는 형태의 코드를 작성하지 말 것)
    • 예를 들어, assertTrue(rectangle.area() != 0)는 0이 아닌 아무 값이나 반환해도 통과하므로, 모호한 코드.
    • assertTrue(rectangle.area() == 50)과 같은 구체적 명시가 더 좋음.

픽스처

  • 객체 세팅 코드가 여러 테스트 코드에 걸쳐 동일하다면, 중복이 생긴다.
  • 이 중복에는 장단점이 모두 있다.
    • 장점
      • 테스트코드를 그냥 위에서 아래로 읽어내려갈 수 있다
    • 단점
      • 테스트 작성에 시간이 오래 걸린다
      • 세팅 코드 변경시, 여러 테스트를 모두 고쳐야 한다(중복의 일반적 폐해)
  • xUnit은 픽스처 생성을 분리하는 방식과, 테스트메서드에 포함하는 방식을 모두 지원한다.
  • 따라서 두가지 모두 사용해보고, 내 상황에 더 적합한 방식을 고르자.

외부 픽스처

  • 픽스처 중 외부 자원이 있을 경우, tearDown() 메서드를 재정의하여 자원을 해제하자.
  • xUnit계는 setUp()이 제대로 수행되었다면, 각 테스트가 끝난 후에 tearDown()을 수행한다.

테스트 메서드

  • xUnit계에서는 test로 시작하는 이름의 메서드는 테스트 메서드가 된다.
  • 픽스처를 사용하기 위해 클래스를 사용한다면, 테스트 메서드가 각각의 테스트가 된다.
  • 테스트 메서드의 이름은 아무것도 모르는 사람이 보더라도 이해할 수 있을만큼 구체적이고 명확해야한다(길어질 지라도).
  • 테스트 메서드에 두 세줄 정도의 아웃라인을 작성해두는 것도 좋은 방법이다.

예외 테스트

  • 예상되는 예외에 대해서만 catch한다.
  • 이외의 경우(다른 예외가 발생하거나, 예외가 발생하지 않은 경우)
    • fail() 메서드를 넣어 테스트 실패를 명시한다.

전체 테스트

  • 모든 테스트 슈트를 모으는 모음을 작성한다.
  • 패키지 내의 슈트들을 모두 모은 뒤, 각 패키지별로 해당 모음들을 다시 모아 애플리케이션 레벨의 슈트를 만든다

30장 디자인 패턴

요약

  • 커맨드
    • 계산 작업의 호출을 메시지가 아닌 객체로 표현한다
  • 값 객체
    • 객체 생성 이후 그 값이 절대로 변하지 않게 하여 별칭 문제가 발생하지 않게 한다
    • 즉, 하나의 객체에 여러 이름이 붙어 참조되어 바뀌는 경우가 없도록 한다
  • 널 객체
    • 계산 작업의 기본 사례를 객체로 표현한다
  • 템플릿 메서드
    • 계산 작업의 변하지 않는 순서를 여러 추상 메서드로 표현한다.
    • 해당 추상 메서드들은 상속하여 특정 작업을 수행하도록 구체화시킨다.
  • 플러거블 객체
    • 객체를 호출함으로써 다양성을 표출해 둘 이상의 구현을 할 수 있도록 한다.
  • 팩토리 메서드
    • 생성자 대신 메서드를 호출하여 객체를 생성한다.
  • 임포스터
    • 현존하는 프로토콜을 가진 다른 구현을 추가하여 새로운 시스템으로 차츰 변이한다.
  • 컴포지트
    • 하나의 객체로 여러 객체의 행위 조합을 표현한다.
  • 수집 매개 변수
    • 각기 다른 객체에서 계산한 결과를 모으기 위해 매개 변수를 여러 곳으로 전달한다.

31장 리팩토링

TDD에서의 리팩토링 개념

일반적인 리팩토링 : 어떤 상황에서도 프로그램의 의미론을 변경하지 않는 것
TDD에서의 리팩토링 : 통과하는 테스트 집합(현재 만들고 통과한 테스트)에 영향을 주지 않는다면, 의미론을 바꿀수도 있는 것

차이점 일치시키기

  • 두 코드가 단계적으로 닮아가게끔 하고, 최종적으로 완전히 동일해지면 둘을 합친다
  • 거꾸로 수행하는 경우도 있다
    • 변경 마지막 단계에 사소한 차이만 처리하도록 하기 위해 어떤 모양새가 되어야할 지 고민해본다.
    • 즉, 원하는 형태에 대한 가정을 바탕으로 코드 흐름을 아래서부터 거슬러 올라가며 짜본다

변화 격리하기

  • 객체나 메서드의 일부만 바꾸려면, 해당 파트를 새로운 객체, 메서드, 메서드 객체 등으로 추출한다.
  • 추출한 부분만 바꾼다면, 해당 환경은 격리되어 문제가 생겼을 때 롤백하기도 쉽다.

데이터 이주시키기

  • 표현양식을 변경하려면, 일시적으로 데이터를 중복시킨다.
  • 방법
    • 기존 인스턴스가 있는 곳에 새 인스턴스도 추가하고, 똑같이 세팅시킨다.
    • 기존 인스턴스를 사용하던 모든 곳에서 새 인스턴스를 사용하게 한다.
    • 기존 인스턴스를 제거한다.
    • 기존 인스턴스에 맞게 외부 인터페이스를 변화시킨다.

메서드 추출하기

  • 기존 메서드가 너무 길고 복잡하여 읽기 힘들다면, 일부분을 분리하여 새로운 메서드로 만든다.
  • 방법
    • 반복문 내부 코드나 반복문 전체, 조건문 가지 등을 분리할 수 있을지 찾아보기
    • 추출할 영역 외부에서 선언된 임시 변수에 대해 할당하는 문장이 없는지 확인하고, 있다면 분리할 메서드에 매개변수로 추가하기

메서드 인라인

  • 메서드 추출하기와 반대
  • 메서드가 너무 분리되어 있어 너무 흐름이 꼬여있거나 산재되어있다면 호출부를 해당 메서드 본문으로 교체한다.
  • 이후 흐름을 파악하고, 실제적으로 메서드 분리가 필요하다면, 다시 추상화를 수행한다.

메서드 객체

  • 여러개의 매개 변수와 지역 변수를 갖는 복잡한 메서드는, 그냥 꺼내서 객체로 만든다.
  • 방법
    • 메서드와 같은 매개변수를 가지는 객체를 만든다.
    • 메서드의 지역 변수는 객체의 인스턴스 변수로 만든다.
    • 원래 메서드와 동일한 내용을 가지는 run() 메서드를 만든다.
    • 원래 메서드에서는 새로 만들어진 클래스의 인스턴스를 생성후 run()을 호출한다.
  • 기존의 계산과 분리된 새로운 스타일의 계산을 작성할 수 있다.
  • 메서드 추출을 하기 어려울 정도로 임시변수와 매개 변수들로 얽혀있을 경우 이 방식을 사용하기도 한다.

감상

리팩토링이나 디자인패턴이 원래 이런건지는 모르겠지만, specific한 내용이나 내 입장에선 아주 당연한 것처럼 느껴지는 얘기도 있었다.
이 때문에 생략한 몇몇 부분도 있다.


32장 TDD 마스터하기

FAQ

단계가 얼마나 커야하는가?

  • 각 테스트가 다뤄야 할 범위는 얼마나 넓은가?
  • 리팩토링하면서 얼마나 많은 중간 단계를 거쳐야 하는가?

-> 아주 작은 테스트와, 애플리케이션 레벨의 거대한 테스트, 모두 수행할 수 있어야 한다.

초기에는 매우 작은 단계로 수행하다가, 몇 단계씩 건너뛰는 실험을 해볼 것.

테스트 할 필요가 없는 것은 무엇인가?

  • 다음을 테스트 할 것

    • 조건문
    • 반복문
    • 연산자
    • 다형성
  • 다음을 테스트하지 말것

    • 타인의 코드(라이브러리)
      • 단, 외부 코드에 버그가 있는 경우에, 버그 때문에 통과하는 테스트케이스를 작성해둘 것.
      • 버그가 수정되면, 해당 케이스는 실패하므로, 코드를 점검할 수 있음
    • 계속 테스트해왔어서 진저리가 나는 것들

좋은 테스트를 갖췄는지의 여부를 어떻게 확인하는가?

  • 긴 setUp 코드
    • 하나의 단언을 위해 아주 긴 객체 생성 코드가 필요하다면, 객체가 너무 크다는 뜻이므로 나눠야한다.
  • 셋업 중복
    • 공통 셋업 코드를 넣어둘 공통 장소를 찾기 힘들다면, 서로 밀접하게 엉킨 객체들이 너무 많다는 말이다.
  • 실행하는데 오래 걸리는 테스트
    • 이 경우 테스트를 실행하지 않게 되는 경향이 있다
    • 오래 걸린다는 것은 애플리케이션의 작은 부분만 따로 테스트하기 힘들다는 이야기로, 이는 설계 문제를 암시한다.
  • 깨지기 쉬운 테스트
    • 예상치 못하게 실패하는 테스트는 애플리케이션의 특정 부분이 다른 부분에 이상한 방식으로 영향을 끼친다는 말이다.

피드백이 얼마나 필요한가?(테스트를 얼마나 많이 작성해야 하는가?)

본인의 실패간 평균시간(MTBF, Mean Time Between Failures)에 따라 다르다.

어느 정도의 견고함이 확보되었다면, 테스트 케이스를 추가하여 얻는 견고함보다,
극히 예외적인 테스트 하나를 추가하여 늘어나는 테스트시간이 더 문제라고 판단될 때가 있다.

실용적으로 판단하자.

테스트를 지워야 할 때는 언제인가?

테스트가 많으면 좋다지만, 만약 겹치는 테스트가 있다면?

  • 테스트를 삭제하였을 때 자신감이 떨어질 것 같다면 삭제하지 마라(...)
  • 커뮤니케이션 : 동일한 코드부분을 수행하는 테스트라도, 다른 시나리오를 말한다면 삭제하지 마라.

애플리케이션 레벨의 테스트로도 개발을 주도할 수 있는가?

작은 규모의 테스트를 통한 TDD는 사용자가 원하지 않는 기능까지도 집착하여 구현하게 될 수 있다는 위험성이 있다.

그러나 애플리케이션 레벨의 테스트는 다음과 같은 위험성을 가진다.

  • 모든 프로그래머들을 동시에 책임관계로 끌어들이는 일이므로, 팀의 협조가 필요하다.
  • 테스트와 피드백 사이의 시간, 즉 테스트 통과에 걸리는 시간이 아주 길다면 빨간막대밖에 볼 수 없다.
  • 아직 만들지 않은 기능에 대한 테스트를 작성하는 것은 힘든 일이다.
    • 만약 이렇게 해야 한다면, 아직 만들지 않은 기능에 대한 테스트에 대해 우아한 에러를 뱉어내는 인터프리터를 도입하는 것이 일반적인 해결책이다.

따라서, 웬만하면 작은 규모의 테스트 주도 개발을 시도하라.

프로젝트 중반에 TDD를 도입하려면?

  • 확실히 하지 말아야 할 것은, 코드 전체 테스트를 한꺼번에 만들고 코드를 모두 리팩토링하는 것.
    • 몇 달이나 걸릴 텐데, 그 동안은 어떤 기능도 추가로 구현할 수 없다. 이는 비즈니스 측면에서 말도 안되는 짓.
  • 따라서, 우선적으로 변경의 범위를 제한한다.
  • 다음은, 테스트와 리팩토링 사이의 데드락을 풀어준다.
    • 테스트가 아닌 방법으로 피드백을 얻는다.
    • 예를 들어 아주 조심스럽게 작업하기, 또는 파트너와 함께 작업하기

TDD는 누구를 위한 것인가?

TDD의 가치체계는 다음과 같다.

  • 작동되는 코드를 넘어서서, 훨씬 더 나은 품질의 (우아한) 코드를 추구한다.
  • 더 깔끔한 설계를 할 수 있도록, 또 개선할 수 있도록 돕는다.
  • 적절한 때에 적절한 문제에 집중할 수 있도록 한다.
  • 시간이 지났을 때, 프로젝트가 더러워지고 지루해지는 것이 아니라, 테스트가 쌓여 더 자신있게 설계 변경을 할 수 있게 된다.

TDD와 패턴의 관계는?

  • 반복 행동을 규칙으로 환원해서 규칙을 규명해내는 것은, 처음부터 설계를 시도하는 것보다 훨씬 빠르다.
  • TDD는 패턴 주도 설계에 대한 구현 방법이다.
    • 초기에 완벽하다고 생각했던 설계는 결국 틀리다
    • 그냥 시스템이 무슨 일을 할 지 생각하고 나중에 설계를 정하는 편이 더 낫다.

WRITTEN BY

알파카의 Always Awake Devlog

Seoul