1장. 화폐 예제

  • 객체를 만들면서 시작하는 것이 아니라 테스트를 먼저 만들어야 한다.
  • 테스트를 작성할 때는 메소드의 완벽한 인터페이스에 대해 상상 해보는 것이 좋다.
  • 테스트와 코드 사이의 결합도를 낮추기 위해, 테스트하는 객체의 새 기능을 사용한다.

 

TDD를 할 때에는 아래와 같은 순서로 코드를 짜게 된다.

1. 테스트를 작성 -> fail
2. 컴파일 되게 하기
3. 테스트 pass
4. 중복 제거 (추상화)

화폐 예제를 위 순서에 따라 조금씩 기능을 추가하는 것을 따라가다 보니 어느새 필요한 여러 기능이 구현되어있었다.

`어떤 테스트들이 추가로 필요할까?` 생각하는 것, 그리고 `중복을 제거하기 위한 리팩토링`을 예제를 통해 볼 수 있는 챕터였다.

 

2장. xUnit

xUnit 챕터는 테스트 프레임워크를 만드는데, 그 것을 위한 테스트 케이스를 작성하고 있다.

테스트 프레임워크에서 테스트 케이스 진행 순서는 아래와 같다.

1. 테스트 메서드 호출
2. setUp - 자원 할당
3. tearDown (실패하더라도) - 자원 반납
4. 여러개의 테스트 실행
5. 결과 수집 및 출력
  • 테스트 커플링을 만들지 말아야 한다. -> 테스트(객체) 간에 영향을 주지 않도록 해야 함

나도 테스트 툴을 만들었던 경험이 있는데, 이 챕터를 읽었다면 좀 더 쉽게 구조를 잡을 수 있지 않았나 싶은 생각이 들었다.

큰 테스트를 작게 쪼개며 테스트를 격리하는 것에 대해 생각해 보게 되었다.

 

3장. 테스트 주도 개발의 패턴

  • 테스트 주도 개발 패턴
    • 테스트 데이터의 경우 산발적으로 쓰지 말자. (1과 2 중에 선택해야 한다면 1)
    • 테스트 데이터로는 여러 의미를 담는 동일한 상수를 쓰지 말자. (1+1 보다는 1+2를 사용)
  • 빨간 막대 패턴: 테스트를 언제 어디에서 작성할 것인지, 테스트 작성을 언제 멈출지
    • 오퍼레이션이 아무 일도 하지 않는 경우를 가장 먼저 테스트 하도록 하자
  • 테스팅 패턴
    • 큰 테스트 케이스는 작은 테스트케이스들로 나누자 (A, B, C를 한번에 동작하게 하기 보다는 A->B->C 순서대로 동작하도록)
    • 비용이 많이 들거나 복잡한 리소스에 의존하는 객체(ex. DB)를 테스트 할 땐, Mock 객체를 만들자.
      • Mock 객체: 스스로 검증할 수 있는 능력이 있고, 외부에서 결과값을 조정할 수 있다.
    • 한 객체가 다른 객체와 올바르게 대화하는지 보기 위해서는 Self Shunt 패턴을 이용 하자. 즉, 원래의 대화 상대가 아니라 테스트 케이스와 대화하도록 한다. => 테스트 케이스가 Mock 객체가 됨
      • Self Shunt 패턴의 경우 테스트 케이스가 구현 할 인터페이스를 얻기 위해 인터페이스 추출 과정이 필요하기 때문에, 인터페이스 추출이 쉬운지 존재하는 클래스를 블랙박스로 테스트 할 것인지 정해야 한다.
    • 로그 문자열 등을 가지고 있다면 테스트 시 문자열 집합을 저장하고 있다가 집합 비교 등을 수행하면 된다. 로그 문자열은 Self Shunt 패턴과도 잘 동작한다.
    • 발생하기 힘든 에러 상황의 경우, 그냥 예외를 발생시키는 특수한 객체를 익명 내부 클래스 등으로 만들어 이를 호출하도록 하면 된다.
      • 파일 시스템의 여유 공간이 없을 경우 발생 할 문제 = 실제로 파일을 채우기 보다는 fileSystemError() 등을 호출하도록
  • 초록 막대 패턴: 코드가 테스트를 통과하게 만들기 위해서
    • 일단은 깨진 테스트에 대해 상수를 반환하게 하자
    • 컬렉션을 다루는 연산은 먼저 컬렉션 없이 구현하고, 그 다음 컬렉션을 사용하게 하자
  • xUnit 패턴
    • 판단 결과는 true/false
    • 객체를 세팅하는 코드가 여러 테스트에 걸쳐 동일 할 때에는 테스트 fixture을 만들어 두는 것을 선택하는 것도 괜찮다 = setUp()
      • setUp을 만든다면 메서드가 자동으로 호출된다는 점과, 어떻게 초기화 되는지 기억해야 한다.
      • fixture 중 외부 자원이 있을 경우 tearDown()으로 해제 함으로써 테스트가 실행되기 전화 후의 외부 세계가 동일하게 유지되도록
  • 디자인패턴
    • 커맨드: 간단한 메서드 호출보다 복잡한 형태의 계산 작업에 대한 호출이 필요할 때 => 계산 작업에 대한 객체를 생성하여 이를 호출하도록 (ex. Java Runnable)
    • 값 객체: 널리 공유해야 하지만, 동일성은 중요하지 않을 때 => 객체가 생성될 때, 객체의 상태를 설정 후 상태가 변할 수 없도록 + 연산 결과는 항상 새로운 객체를 반환. 즉, 객체를 값처럼 행동하도록 만든다.
    • 널 객체
    • 템플릿 메서드: 작업 순서는 변하지 않지만 각 작업 단위에 대한 미래의 개선 가능성을 열어두고 싶은 경우 => 다른 메서들을 호출하는 내용으로만 이루어진 메서드를 만든다. => setUp, run, tearDown 구현
    • 플러거블 객체: 변이를 표현할 때 가장 간단한 방법은 명시적인 조건문을 사용하는 것이다. 그런데 이 명시적인 조건문은 중복될 수 있는데, 이 때 중복을 제거하기 위해 플러거블 객체를 사용하게 된다.
    • 플러거블 셀렉터: 인스턴스별로 다른 메서드가 동적으로 호출되게 하려면, 메서드의 이름을 저장하고 있다가 그 이름에 해당하는 메서드를 동적으로 호출하면 될 것이다. 플러거블 셀렉터란 이러한 경우 리플렉션을 이용하여 동적으로 메서드를 호출하는 것이다. (리플렉션이므로 남용하진 않도록 하자)
    • 팩토리 메서드: 새 객체를 만들 때 유연성을 원하는 경우
    • 임포스터: 기존의 코드에 새로운 변이를 도입하기 위해 기존의 객체와 같은 프로토콜을 갖지만 구현은 다른 새로운 객체를 추가한다. 이를 임포스터라고 하는데, 이 예로서는 널 객체와 컴포지트 패턴이 있을 수 있다.
    • 컴포지트 패턴: 하나의 객체가 다른 객체 목록의 행위를 조합한 것처럼 행도하게 만들기 위해서는 객체 집합을 나타내는 객체를 단일 객체에 대한 임포스터로 구현하게 된다. 이를 컴포지트 패턴이라고 한다. 
      • 중복이 나타나는 순간 컴포지트패턴을 도입해 보자
    • 수집 매개 변수: 여러 객체에 걸쳐 존재하는 오퍼레이션의 결과를 수집해야 할 때에는 결과가 수집될 객체를 각 오퍼레이션의 매개변수로 추가해 주면 된다.
      • ex. java.io.Externalizable - writeExternal 메서드는 객체와 그 객체가 참조하는 모든 객체를 기록한다. 이를 위해서 메서드는 수집 매개 변수로서 ObjectOutput을 전달한다

 

  • 리팩토링
    • 메서드 추출하기
    • 메서드 인라인
    • 인터페이스 추출/객체 추출하기
    • 메서드 객체 - 여러 개의 매개 변수 및 지역 변수를 갖는 복잡한 메서드의 경우 꺼내서 객체로 만든다

 

느낀 점

일단 책이 말하는 식으로 진행이 되어서 읽기 쉬웠고, 양도 얼마 되지 않아서 금방 다 읽었다.

내용 자체는 무엇을 말하려는지 알 것 같은데.. 흠,, 프로젝트에 직접 적용해 보아야 좀 더 이해한 내용이 명확해 질 것 같다.

+ Recent posts