본문 바로가기
Book

[독서 기록] 테스트 주도 개발로 배우는 객체지향 설계와 실천

by Renechoi 2024. 2. 9.
 
객체 지향 설계와 실천
테스트 주도 개발로 배우는 『객체 지향 설계와 실천』. 소프트웨어 개발의 여러 층위에서 TDD가 어떻게 작동하는지 보여주면서 테스트로 코드를 객체 지향적으로 구성하고 객체 간 관계를 설명하는 방법을 제시함으로써 TDD를 사용하는 팀이 실제 개발 프로젝트에서 부딪힐 법한 문제를 체계적으로 풀어낸다. TDD를 효과적으로 구현하는 법, 더 깔끔하고 유지 보수하기 좋은 코드를 만드는 법 등을 다룬다.
저자
스티브 프리먼, 냇 프라이스
출판
인사이트
출판일
2013.06.20

 

 

 

 

왜 테스트의 '안내를 받는다'고 했나?

 

우리는 테스트 코드를 먼저 작성한다. 그 편이 더 나은 코드를 작성하는 데 도움이 된다는 사실을 알기 때문이다. 테스트를 먼저 작성하면 의도가 더 분명해지므로 코드가 무엇을 해야 하는지 명확히 기술하기 전에는 아무리 작은 작업이라도 곧바로 시작하지 않는다. 설계가 너무 경직돼 있거나 산만할 때 '테스트를 먼저 작성한다'는 절차는 이를 파악하는 데 도움된다. 그러고 나서 설계를 따라 개발하거나 설계상의 결함을 수정해야 할 때 이미 작성된 테스트는 회귀 테스트라는 안전망이 돼 준다.

- XV

 

 

 

 

 

우리는 시스템 규모를 믿을 수 있는 방식으로 키우고, 늘 일어나는 예상치 못한 변화에 대처하고 싶다면 두 가지 기술적인 토대가 필요하다는 사실을 알게 됐다. 먼저 회귀 오류를 잡아줄 꾸준한 테스트가 필요한데, 그러면 기존 기능을 망가뜨리지 않고도 새 기능을 추가할 수 있다. 시스템 규모와 상관없이 수동 테스트를 자주하는 것은 비실용적이므로 구축과 배포, 시스템 버전 변경에 드는 비용을 줄이려면 되도록 테스트를 자동화해야 한다.

- 5p

 

 

다음으로 코드를 가능한 한 단순하게 유지해야 하는데, 그렇게 하면 코드를 이해하고 수정하기가 더 쉽다. 개발자들은 코드를 작성하는 것보다 코드를 읽는 데 훨씬 더 많은 시간을 보내므로 거기에 맞게 최적화해야 한다. 

-5p 

 

 

 

TDD의 핵심에 놓인 주기는 이렇다. 테스트를 작성한다. 해당 테스트가 동작하게 만들 코드를 작성한다. 코드를 가급적 테스트한 기능의 단순한 구현으로 리팩터링한다. 이러한 과정을 반복한다.

- 6p 

 

 

인수 테스트: 전체 시스템이 동작하는가?

통합 테스트: 변경할 수 없는 코드를 대상으로 코드가 동작하는가?

단위 테스트: 객체가 제대로 동작하는가? 객체를 이용하기가 편리한가? 

- 11p 

 

 

 

테스트가 시스템에 관해 말해줄 수 있는 바를 살펴보는 또 한 가지 방법이 있다. 우리는 외부 품질과 내부 품질을 구분할 수 있다. 외부 품질은 시스템이 고객과 사용자의 요구를 얼마나 잘 충족하는가이며(기능, 신뢰성, 가용성, 응답성 등), 내부 품질은 시스템이 개발자와 관리자의 요구를 얼마나 잘 충족하는가이다(이해하기 쉬운가, 변경하기 쉬운가 등). 외부 품질의 요점을 이해하지 못할 사람은 없다. 외부 품질은 보통 계약의 일부다. 내부 품질은 외부 품질과 똑같이 중요하지만 달성하기가 더 어려울 때가 있다. 내부 품질은 거듭되고 예상할 수 없는 변경에 대처하는 것으로, 1장을 시작할 때 봤듯이 소프트웨어를 이용하는 것과 관련이 깊다. 내부 품질을 유지하는 것과 관련해 가장 중요한 것은 시스템 동작 방식을 안전하고 예상 가능한 상태로 바꿀 수 있게 만드는 것이다. 그렇게 해야만 변경으로 인해 큰 규모의 재작업을 해야 할 위험을 최소화할 수 있기 때문이다.

- 12p 

 

 

통합 테스트는 그림처럼 두 테스트 사이의 어딘가에 놓인다. 

 

 

 

-13p 

 

 

 

설계를 잘못하면(예를 들어 클래스가 멀리 떨어져 있는 시스템의 일부와 긴밀하게 결합돼 있거나 암시적인 의존성이 있거나, 불분명한 책임이 너무 많으면) 단위 테스트를 작성하거나 이해하기 어렵다는 사실을 알게 됐다. 따라서 테스트를 먼저 작성하면 설계에 관한 귀중하고 즉각적인 피드백을 얻을 수 있다.

-13p 

 

 

 

객체 지향 설계는 객체 자체보다 객체 간의 의사소통에 더 집중한다. 

- 15p 

 

 

시스템을 설계할 때는 값과 객체를 구분하는 것이 중요하다. 여기서 값은 변하지 않는 양이나 크기를 나타내며, 객체는 시간이 지남에 따라 상태가 변할지도 모르지만 식별자(identity)가 있는 계산 절차를 나타낸다. 

- 16p 

 

 

객체는 변경 가능한 상태를 이용해 시간의 추이에 따른 객체의 행위를 나타낸다. 타입이 똑같은 두 객체는 현재 상태가 정확히 동일하더라도 별개의 식별자를 지닌다. 이는 두 객체가 향후 어떤 메시지를 전달받느냐에 따라 상태가 달라질 수 있기 때문이다.

- 16p 

 

 

 

객체는 그것이 내부적으로 보유하고 있거나 메시지를 통해 확보한 정보만 가지고 의사 결정을 내려야 한다. 객체는 다른 객체를 탐색해 뭔가를 일어나게 해서는 안된다. 이 스타일을 일관되게 따른다면 코드가 좀 더 유연해지는데, 이는 같은 역할을 수행하는 객체를 손쉽게 교체할 수 있기 때문이다. 호출자는 객체의 내부 구조나 역할 인터페이스 너머에 존재하는 시스템의 나머지 구조에 관해 알 필요가 없다.

- 19p 

 

 

어떻게 해당 객체의 내부 상태를 드러내지 않고 그러한 일이 올바르게 수행되는지 테스트할 수 있을까?

 

한 가지 방법은 그림 2.5처럼 테스트에 존재하는 대상 객체의 이웃을 다른 대체물, 즉 목 객체(mock object)로 대체하는 것이다. 그렇게 하면 발생하는 이벤트에 대해 대상 객체가 가짜 이웃과 어떻게 상호 작용할지 지정할 수 있다. 이 같은 명세를 예상 구문이라 한다. 테스트가 진행되는 동안 몸 객체는 자신이 예상 대로 호출됐는지 단정한다. 게다가 나머지 테스트가 동작하는 데 필요한 행위를 구현하기도 한다.

- 22p 

 

 

 

- 필요한 목 객체 생성

- 대상 객체를 포함한 실제 객체 생성

- 대상 객체에서 목 객체가 어떻게 호출될지 예상하는 바를 기술

- 대상 객체에서 유발 메서드를 호출

- 결과 값이 유효하고 예상되는 메서드 호출이 모두 일어났는지 확인

 

- 23p 

 

 

 

 

실패하는 인수 테스트를 작성하는 것으로 신기능을 작업하는 데 착수한다. 인수 테스트는 우리가 작성하려는 기능을 아직 시스템에서 갖추지 못했다는 사실을 보여주고 그 기능이 완성되기까지 진행 상황을 반영한다.

- 47p 

 

인수 테스트가 통과되면, 기능 구현은 끝이다.

- 47p 

 

 

테스트를 가장 간단한 성공 케이스로 시작하라

- 49p 

 

 

메서드가 아닌 행위를 단위 테스트하라

- 52p 

 

 

 

테스트에 귀를 기울이라

 

경험상 코드가 테스트하기 어렵다면 주로 설계 개선이 필요하기 때문이다. 지금 당장 코드를 테스트하기 어렵게 만드는 구조 때문에 나중에도 코드를 변경하기 어려울 것이다. 시간이 지나면 변경하기가 더 어려울 텐데 코드를 작성할 때 무슨 생각을 했는지 잊어버리기 때문이다. ...

이 경우 테스트를 작성하는 과정을 잠재적인 유지 보수 문제를 조기에 알려주는 귀중한 경고로 여기고 그 힌트를 이용해 아직 심각하지 않을 때 문제를 해결한다. 그림 5.3에서 보다시피 다음으로 실패할 테스트를 작성하기가 어렵다면 제품 코드의 설계를 다시 살펴보고 앞으로 나아가기 전에 리팩터링한다.

 

- 54p 

 

 

 

 

우리는 작성하기 쉬운 코드보다는 유지 보수하기 쉬운 코드를 높게 평가한다. 

- 57p 

 

 

 

코드 규모가 커질 경우 해당 코드를 계속해서 이해하고 유지 보수할 수 있는 방법은 기능을 객체로, 객체를 패키지로, 패키지를 프로그램으로, 프로그램을 시스템으로 구조화하는 것밖에 없다. 다음과 같은 두 가지 주요한 휴리스틱을 활용해 이 같은 구조화를 이끌어 나가겠다.

- 관심의 분리

- 더 높은 수준의 추상화

 

- 58p 

 

 

앞서 설명한 두 가지를 일관되게 적용하면 애플리케이션 구조가 콕번의 '포트와 어댑터' 아키텍처 같은 것으로 나아갈 것이다. 여기서 포트와 어댑터'란 비즈니스 도메인의 코드가 데이터베이스나 사용자 인터페이스 같은 기술 기반 구조의 의존성과 격리된 아키텍처를 의미한다. 우리는 기술적인 개념이 애플리케이션 모델로 스며 들기를 바라지 않으므로 인터페이스를 작성해 애플리케이션 모델과 외부 세계의 관계를 애플리케이션 모델의 용어로 기술한다(포트). 그러고 나서 애플리케이션의 핵심부와 각 기술 도메인 사이에서 브리지 역할을 하는 코드를 작성한다(어댑터). 이는 에릭 에반스가 '손상 방지 계층(anticorruption layer)'이라고 부르는 것과 관련이 있다. 

 

여기서 브리지는 애플리케이션 모델에서 정의한 인터페이스를 구현하고 애플리케이션 수준의 객체와 기술 수준의 객체를 매핑한다. 이를테면 브리지는 주문장(order book) 객체를 SQL 문과 매핑해 주문이 데이터베이스 저장되게 할 것이다. 그러자면 브리지에서는 애플리케이션 객체에서 값을 질의하거나 하이버네이트 같은 객체 관계형 도구를 사용해 자바의 리플렉션으로 객체에서 값을 꺼내야 한다.

 

- 58~59p 

 

 

 

복합 객체의 API는 반드시 구성 요소의 존재와 구성 요소 간의 상호 작용을 감추고 더 단순한 추상화를 이웃에게 드러내야 한다. 기계식 시계를 생각해보면 시계는 시간을 표시하는 두세 개의 침과 시간을 조정하는 데 쓰는 태엽 감는 꼭지가 있지만, 동작하는 부품은 모두 한데 조립되어 있다.

- 65p 

 

 

 

호출자는 객체가 무슨 일을 하고 무엇에 의존하는지 알고 싶어하지, 해당 객체가 어떻게 동작하는지는 알고 싶어 하지 않는다. 

- 69p 

 

 

 

첫째, 테스트로 시작한다는 것은 '어떻게'를 고려하기 전에 달성하고자 하는 바가 '무엇인지'를 기술해야 함을 의미한다. 이는 대상 객체에 대한 추상화를 올바른 수준으로 유지하는 데 기여한다. 단위 테스트의 의도가 불분명하다면 개념이 뒤죽박죽이 되고 코드 작성을 시작하지 못할 것이다. 

- 69p 

 

둘째, 단위 테스트를 이해 가능한 상태로 유지하려면 (그리고 그렇게 해서 유지 보수할 수 있게) 단위 테스트의 범위를 제한해야 한다. 지금까지 단위 테스트가 수십 줄씩이나 돼서 테스트의 요점이 테스트가 시작하는 부분 어딘가에 묻혀버리는 경우를 목격해왔다. 그러한 테스트는 테스트 대상 컴포넌트의 규모가 너무 커서 좀 더 작은 컴포넌트로 쪼개야 함을 말해준다. 

- 70p 

 

셋째, 단위 테스트를 위한 객체를 만들려면 해당 객체의 의존성을 전달해야 하는데, 이는 그러한 의존성이 어디에 있는지 알아야 한다는 의미다. 그러면 컨텍스트 독립성이 높아지는데, 이것은 단위 테스트를 수행하기에 앞서 대상 객체의 환경을 구성할 수 있어야 하기 때문이다. 

- 70p 

 

 

 

어떤 객체가 손쉽게 테스트할 수 없을 정도로 몸집이 크거나 테스트가 실패한 이유를 해석하기 어렵다면 해당 객체를 분해한다. 그러고 나서 새로 나눈 부분들을 따로따로 단위 테스트한다.

- 74p 

 

 

코드가 좀 더 안정화되고 어느 정도 구조를 갖추고 나면 새로운 타입을 현실로 끄집어내는 식으로 그것들을 발견한다. 이때 객체에 행위를 추가하고, 그리고 설계 원칙을 따르더라도 어떤 새로운 기능은 그러한 원칙에 속하지 않는다는 사실을 발견할 지도 모른다. 

 

이러한 경우 인터페이스를 만들어 객체 관점에서 필요한 서비스를 정의한다. 대상 객체와 해당 객체의 협력 객체 간의 관계를 기술하는 데 도움이 되는 목 객체를 이용해 서비스가 이미 존재하는 양 새로운 행위에 대한 테스트를 작성한다.

- 74p 

 

 

 

변경할 수 없는 타입에 대해서는 목 객체를 적용하지 말라

- 83p 

 

 

 

 

테스트 주도 개발을 할 때 우리는 구현하고자 하는 행위를 일으키는 외부 이벤트로 시작해서 한 번에 한 객체씩 코딩을 계속해 나가며, 이 같은 과정은 목표를 달성했음을 가리키는 가시적인 효과가 나타날 때까지 이어진다.

- 132p 

 

 

 

 

다음은 흔히 볼 수 있는 예다. 

 

Date now = new Date(); 

 

내부적으로 생성자에는 싱글턴인 System을 호출하고 System.currentTimeMillis()를 이용해 새 인스턴스를 현재 시간으로 설정한다. 편리한 기법이지만 대가가 따른다. 테스트를 다음과 같이 작성하고 싶다고 해보자. 

 

@Test public void rejectsRequestsNotWithinTheSameDay(){
	receiver.acceptRequest(FIRST_REQUEST);
    // 다음날 
    assertFalse("too late now", receiver.acceptRequest(SECOND_REQUEST));
}

 

구현은 다음과 같다. 

 

public boolean acceptRequest(Request request){
	final Date now = new Date();
    if (dateOfFirstRequest == null ) { 
    	dateOfFirstRequest = now; 
    } else if (firstDateIsDifferentFrom(now)){
    	return false; 
    }
    // 요청 처리 
	return true; 
}

 

dateOfFirstRequest는 필드이며, firstDateIsDifferentFrom()은 자바 날짜 라이브러리와 관련된 그다지 보고 싶지 않은 부분을 감춰주는 도우미 메서드다. 

 

이 제한 시간을 테스트하려면 테스트가 하룻밤을 기다리게 하거나 뭔가 현명한 조치를 취해 생성자를 가로채서 테스트에 필요한 적당한 Date 값을 반환해야만 한다. 이 테스트에서 어려움은 코드를 변경해야 한다는 조짐이다. 테스트를 좀 더 이해하기 쉽게 만들려면 Date 객체가 생성되는 방법을 제어할 필요가 있으므로 Clock을 도입해 그것을 Receiver에 전달한다. Clock에 대한 테스트를 만들 경우 테스트는 다음과 같다. 

 

 

@Test public void rejejectsRequestNotWithInTheSameDay() { 
	Receiver receiver = new Receiver(stubClock);
    stubClock.setNextDate(TODAY);
    receiver.acceptRequest(FIRST_REQUEST);
    
    stubClock.setNextDate(TOMORROW);
    assertFalse("too late ...) 
}

 

 

... 

 

이제 별다른 꼼수를 쓰지 않고도 Receiver를 테스트할 수 있다. 하지만 좀 더 중요한 점은 Receiver가 시간에 의존한다는 사실을 좀 더 분명히 한 것이다. 

 

- 264~ 265p 

 

 

코드 구조화 기법 측면에서 객체 지향의 목표 중 하나는 객체의 경계를 명확하게 보이게 하는 것이다. '콘텍스트 독립성'에서 강조한 것처럼 객체는 옺기 지역적이거나(해당 객체의 범위 내에서 생성되고 관리되는) 명시적으로 전달되는 값과 인스턴스를 다뤄야 한다.

- 268p 

 

 

로깅보다는 알림

- 269p 

 

 

 

public class MusicCentreTest { 
	@Test public void startsCdPlayerAtTimeRequested() { 
    	final MutableTime scheduledTime = new MutableTime(); 
        CdPlayer player = new CdPlayer() { 
        	@Override public void scheduledToStartAt(Time startTime) { 
            	scheduledTime.set(startTiome);
            }
        }
        
        MusicCentre centre = new MusicCentre(player);
        centre.startMediaAt(LATER);
        
        assertEquals(LATER, scheduledTime.get());
     }
  }

 

 

이 접근법의 문제는 CdPlayer와 MusicCentre 사이의 관계가 암시적으로 남게된다는 것이다. 지금쯤이면 테스트 '주도' 개발에서 우리 의도가 목 객체를 이용해 객체 간의 관계를 끌어내는 데 있음을 알기 바란다. 하위 클래스를 만든다면 도메인 코드에 그러한 관계를 눈에 보이게끔 할 만한 것(객체상의 메서드)이 아무것도 없다. 이렇게 되면 이 관계를 지원하는 서비스가 다른 곳에서도 의미가 있는지 확인하기 더 어려워지며, 다음 번에 해당 클래스를 이용할 때 다시 분석해야만 할 것이다. 요저을 말하자면 CDPlayer를 당므과 같이 구현하는 것도 가능하다. 

 

 

public class CdPlayer{
	public void scheduleToStartAt(Time startTime) {... }
    public void stop() {}
    ...
}

 

 

... 실제로 필요한 것은 ScheduleDevice다. 로버트 마틴은 '인터페이스 분리 원칙'에서 클라이언트가 사용하지 않는 인터페이스에 의존하게 해서는 안 된다'라면서 그 점을 강조했는데 이것은 바로 우리가 구상 클래스를 대상으로 목 객체를 적용할 때하는 일과 정확히 일치한다. 

 

- 272p 

 

 

 

값(어찌 됐든 불변적이어야 할)에 목 객체를 적용할 이유는 전혀 없다. 인스턴스를 생성해서 쓰기만 하라. 이를테면, 다음 테스트에서 Video는 Show의 일부 세부 사항을 담고 있다. 

 

@Test public void sumsToatalRunningTime() { 
	Show show = new Show();
    Video video1 = context.mock(Video.class); // 이렇게 하지 말라. 
    Video video2 = context.mock(Video.class);
    
    ... 
}

 

여기서는 어느 시간 값이 반환되는지 제어하고자 인터페이스/구현 쌍을 생성할 만한 이유가 없다. 단지 적절한 시간을 담고 있는 인스턴스를 생성해서 사용하기만 하면 된다. 

- 273~274p 

 

값의 인스턴스를 생성하기가 너무 복잡해서 값을 대상으로 목 객체를 적용하고 싶은 마음이 든다면 빌더를 작성하는 방법을 고려해보라. 

- 274p 

 

 

비대한 생성자 

 

public MessageProcessor(MessageUnpacker unpacker, 
AuditTrail auditor, 
CounterPartyFinder counterPartyFinder, 
LocationFinder locationFinder, 
DomesticNotifier domesticNotifier, 
ImportedNotifier importedNotifier);

 

->

public MessageProcessor(
MessageUnpacker unpacker, 
AuditTrail auditor, 
MessageDispatcher dispatcher);

 

- 275p 

 

 

 

 

너무 많은 의존성 

 

public class RacingCar {
  private final Track track;
  private Tires tires;
  private Suspension suspension;
  private Wing frontWing;
  private Wing backWing;
  private double fuelLoad;
  private DrivingStrategy driver;

  public RacingCar(Track track, DrivingStrategy driver...){
    this.track = track;
    this.driver = driver;
    ...
  }
}

 

-> 

 

public class RacingCar {
  private final Track track;
  private DrivingStrategy driver = DriverTypes.borderlineAggressiveDriving();
  private Tires tires = TireTypes.mediumSlicks();
  private Suspension suspension = SuspensionTypes.mediumStiffness();
  private Wing frontWing = WingTypes.mediumDownforce();
  private Wing backWing = WingTypes.mediumDownforce();
  private double fuelLoad = 0.5;

  public RacingCar(Track track){
    this.track = track;
  }

  public void setSuspension(Suspension suspension) { ... }
  public void setTires(Tires tires) { ... }
}

 

 

셍상자 재조합 -> 이러한 이웃 객체들을 흔히 볼 수 있는 기본값으로 초기화했다. 

 

- 278~279p 

 

 

 

테스트가 우리에게 말해주는 것 

- 지식의 초점이 특정 객체에 맞춰진다 

- 뭔가가 명시적이라면 거기에 이름을 부여할 수 있다

- 이름이 더 많다는 것은 도메인 정보가 더 많다는 의미다

- 데이터 대신 행위를 전달하라 

 

- 281~282p 

 

 

 

테스트는 행위를 명확하게 표현하기도 해야한다(테스트는 가독성이 있어야 한다). 코드 가독성이 중요한 것과 같은 이유로 테스트 가독성도 중요하다. 매번 개발자가 테스트가 의미하는 바를 파악하려고 더듬어 나간다면 새로운 기능을 만드는 데 쓸 시간이 부족할 테고 팀 속도는 떨어지고 만다. 

- 285p 

 

 

주의해야할 가독성 문제

- 테스트 이름이 그 테스트 케이스가 의도하는 바를 명확히 설명하지 못하며 다른 테스트 케이스와의 차이점도 드러내지 못함

- 테스트 케이스 하나로 여러 기능을 테스트함

- 테스트 구조가 서로 달라서 코드를 읽는 사람이 테스트를 쭉 훑어보는 것만으로는 테스트 의도를 이해할 수 없음

- 테스트를 준비하고 예외 처리를 하는 코드가 너무 많아서 핵심 로직이 파묻힘

- 리터럴 값(매직 넘버)를 사용하지만 해당 값의 정체가 명확하지 않은 테스트 

 

- 286p 

 

 

구조를 이용해 설명하라

구조를 사용해 공유하라

긍정적인 요소를 강조하라

부수적인 객체에 위임하라

- 293p 

 

 

 

 

복잡한 준비가 필요한 클래스의 경우 생성자의 각 매개변수에 대응되는 필드가 포함된 테스트 데이터 빌더를 만든다. 

public class OrderBuilder {

  public static OrderBuilder anOrder() {
    return new OrderBuilder();
  }

  public OrderBuilder withCustomer(Customer customer) {
    this.custoner = customer;
    return this;
  }

  public OrderBuilder withOrderLines(OrderLines lines) {
    this.lines = lines;
    return this;
  }

  public OrderBuilder withDiscount(BigDecimal discountRate){
    this.discountRate = discountRate;
    return this;
  }

  public Order build() {
    Order order = new Order(customer);
    for(OrderLine line: lines) order.addLine(line);
      order.setDiscountRate(discountRate);
    }
  }
}

 

- 298p 

 

 

 

팩터리 메서드를 이용한 도메인 모델 강조

 

빌더를 생성하는 부분을 팩터리 메서드로 감싸면 테스트 코드에 있는 잡음을 더욱 줄일 수 있다. 

 

Order order = anOrder().fromCustomer(aCustomer().withAddress(anAddress().withNoPostCode())).build();

 

- 302p 

 

 

 

의사소통이 우선이다. 

 

테스트 데이터 빌더를 이용해 중복을 줄이고 테스트 코드를 좀 더 표현력 있게 만들었다. 이것은 우리가 코드라는 언어를 관찰한 바를 반영하는 또 다른 기법에 해당하며, 이 기법은' 코드는 읽으려고 있는 것'이라는 원칙에 의해 주도된다. 

- 306p 

 

 

 

테스트의 핵심은 통과가 아니라 실패에 있다.

- 307p 

 

 

 

우리는 제품 코드가 테스트를 통과하길 바랄 뿐 아니라 테스트가 실제로 존재하는 오류를 감지해 보고하게 하고 싶기도 하다. '실패하는' 테스트는 본연의 역할을 성공적으로 수행해 왔다. 심지어 우리가 다루는 것과 관련이 없는 영역에서 일어난 예상치 못한 테스트 실패조차도 아주 가치가 있을 수 있다. 테스트 실패가 우리가 알아차리지 못했던 코드상의 암시적인 관계를 드러내기 땜ㄴ이다.

- 307p 

 

 

테스트 불안정성의 공통적인 원인은 다음과 같다.

- 테스트가 시스템에서 관련이 없는 부분이나 테스트 대상 객체에 무관한 행위와 너무 긴밀하게 결합돼 있다.

- 테스트가 대상 코드의 예상 행위를 과도하게 기술해서 필요 이상으로 제약한다.

- 여러 테스트에서 동일한 제품 코드의 행위를 시험할 때 중복이 생긴다.

 

- 315p 

 

 

 

일어나야 할 일만 정확하게 기술하고 더는 기술하지 말라

- 316p 

 

 

 

 

영속화 데이터를 테스트 종료 시점이 아닌 테스트 시작 시점에 정리하라 

각 테스트에서는 테스트가 시작할 때 영속성 저장소를 알려진 상태로 초기화해야 한다. 테스트가 개별적으로 실행되면 테스트 실패를 진단하는 데 도움이 되는 데이터를 영속성 저장소에 남길 것이다. 테스트가 테스트 스위트의 일부로 실해오디면 다음 테스트에서는 영속화 상태를 먼저 정리해야 한다. 그래야 테스트끼리 서로 격리될 것이다.

- 335p 

 

 

 

 

동기화에 대한 단위 테스트 

 

기능 관련 관심사와 동기화 관련 관심사를 분리하면 AuctionSearch의 기능과 관련된 행위만을 분리해서 테스트할 수 있다. 이제 동기화를 테스트 주도할 차례다. AuctionSearch 구현을 통해 여러 스레드를 실행하는 부하 테스트를 작성해 동기화 문제가 일어나게 한다. 스레드 스케줄러를 정밀하게 제어하지 않고는 테스트에서 동기화 오류를 찾을 수 없다. 할 수 있는 일은 똑같은 코드를 많은 스레드를 대상으로 여러 번 실행해 테스트가 오류를 감지할 만한 가능성을 주는 것이다.

- 353p 

 

 

비동기적인 테스트에서는 반드시 성공할 때까지 기다리고 시간 제한을 이용해 실패를 감지해야 한다.

- 365p 

 

 

반응형