[Test] 테스트 주도 개발 (25 ~ 27장)

DaeHoon·2022년 11월 19일
0

TDD

목록 보기
7/9

25. 테스트 주도 개발 패턴

'테스트할 시간이 없다.' 죽음의 나선

  • 스트레스를 많이 받으면 테스트를 점점 뜸하게 한다. 테스트를 뜸하게 하면 에러가 점점 많아지고, 에러가 많아지면 더욱 많은 스트레스를 받게 된다.
  • 테스트는 중간에 기능을 추가하거나 기능을 수정할 때의 두려움을 지루함으로 바꿔주는 효험이 있다.
  • 즉 테스트를 실행해서 계속 초록 막대 상태면 작업 중에 에러를 낼 확률도 적어지고 이로 인한 스트레스도 줄어든다.
  • 결론은 '시간 없다고 테스트를 거르지 말자'가 된다.

격리된 테스트

  • 각자의 테스트들은 서로 독립적이어야 한다. 순서를 바꿔도 이상없이 잘 실행되어야 한다.
  • 즉 테스트 간 응집도는 높히고 결합도는 낮춰야 한다.

테스트 목록

  • 요구 사항을 토대로 테스트 할 목록을 뽑아낸 다음, TDD를 위해 테스트 코드를 작성해라.

테스트 우선

  • 코드를 작성하기 전 테스트를 먼저 작성해라.

단언 우선

  • 테스트 코드를 작성할 때 단언(assert)를 가장 먼저 쓴 다음 시작해라.

테스트 데이터

  • 테스트할 때 어떤 데이터를 사용해야 하는가? 테스트를 읽을 때 쉽고 따라하기 좋을 만한 데이터를 사용해라.

명백한 데이터

  • 데이터의 의도를 어떻게 표현할 것인가? 테스트 자체에 예상되는 값과 실제 값을 포함하고 이 둘 사이의 관계를 드러내기 위해 노력해라.
  • 예를 들어, 한 통화를 다른 통화로 환전하려고 하는데, 이 거래에는 수수료가 1.5% 붙는다. USD에서 GBP로 교환하는 환율이 2:1이고 %100를 환전하려면 50GBP - 1.5% = 49.25GBP여야 한다.
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(49.25, "GBP"). result);
  • 위의 예를 이런 테스트로 표현할 수 있다. 위의 테스트에서는 입력으로 사용된 숫자와 예상되는 결과 사이의 관계를 명확히 읽어낼 수 있다.

26. 빨간 막대 패턴

  • 아래의 패턴들은 테스트를 언제 어디에 작성할 것인지, 테스트 작성을 언제 멈출지에 대한 것이다.

시작 테스트

  • 오퍼레이션이 아무 일도 하지 않는 경우를 먼저 테스트할 것.

    • 이 오퍼레이션을 어디에 두어야 하나?
    • 적절한 입력 값은 무엇인가?
      -이 입력들이 주어졌을 때 적절한 출력은 무엇인가?
  • 당신에게 뭔가를 가르쳐 줄 수 있으면서도 빠르게 구현할 수 있는 테스트를 선택하라.

설명 테스트

  • 자동화된 테스트가 더 널리 쓰이게 하기 위해 테스트를 통해 설명을 요청하고 테스트를 통해 설명하라.
  • 단순한 시작법은 테스트를 이용하여 묻고, 테스트를 이용하여 설명하는 것이다.
  • 예를 들어 "Foo를 이런 식으로 설정하고 Bar를 이런 식으로 설정하면 76이 나와야 됩니다. Foo가 이렇고 Bar가 이러면 답은 67이 되겠죠" 이런 식으로 설명할 수 있다.

학습 테스트

  • 외부에서 만든 소프트웨어에 대한 테스트를 작성해야 할 때 바로 사용하는 대신 API가 우리 예상대로 실행된다는 것을 확인해줄 만한 작은 테스트를 만들어 보자
public void setUp(){
  store = RecordStore.openRecordStore("testing", true);
}

public void tearDown(){
  Recordstore = RecordStore.deleteRecordStore("testing");
}

public void testStore(){
  int id = store.addRecord(new byte[] {5, 6}, 0, 2);
  assertEquals(2, store.getRecordSize(id));
  byte [] buffer = new byte[2];
  assertEquals(2, store.getRecord(id, buffer, 0);
  assertEquals(5, buffer[0]);
  assertEquals(6, buffer[1]);
}
  • 만약 우리가 API를 제대로 이해했다면 이 테스트는 한번에 통과할 것이다.

또 다른 테스트

  • 주제와 무관한 아이디어 떠오르면 이에 대한 테스트를 할일 목록에 적어놓고 다시 주제로 돌아오자
  • 새 아이디어가 떠오르면 존중하고 맞이하되 그것이 내 주의를 흩뜨리지 않게 한다. 그 아이디어를 리스트에 적어놓고는 하던 일로 다시 돌아간다.

회귀 테스트

  • 시스템 장애가 보고될 때 무슨 일을 제일 먼저 하는가? 그 장애로 인하여 실패하는 테스트, 그리고 통과할 경우엔 장애가 수정되었다고 볼 수 있는 테스트를 가장 간단하게 작성해라.
  • 회귀 테스트(regression test)란, 처음 코딩할 때 작성했어야 하는 테스트다. 회귀 테스트를 작성할 때는 이 테스트를 작성해야 한다는 사실을 어떻게 하면 애초에 알 수 있었을지 항상 생각해보라. 애플리케이션 차원의 회귀 테스트는 시스템의 사용자들이 여러분에게 정확히 무엇을 기대했으며 무엇이 잘못되었는지 말할 기회를 준다.

27. 테스팅 패턴

  • 이 패턴들은 더 상세한 테스트 작성법에 대한 것이다.

자식 테스트

  • 지나치게 큰 테스트에서 깨지는 부분에 대해 작은 케이스로 작성하고, 그 작은 케이스가 실행이 되면 다시 원래의 큰 테스트 케이스를 추가해라

모의 객체 (Mock Object)

  • 비용이 많이 들거나 복잡한 리소스에 의존하는 객체를 테스트 하기 위해 모의 객체를 만들자.
  • 고전적인 예로는 DB다. DB는 시작 시간이 오래 걸리고, 깨끗한 상태로 유지하기가 어렵다. 그리고 만약 DB가 원격 서버에 있다면 이로 인해 테스트 성공 여부가 네트워크 상의 물리적 위치에 영향을 받게 된다.
  • 해법은 모의 DB를 사용하는 것이다.
public void testOrderLookUp(){
  Database db = new MockDatabase();
  db.expectQuery("select order_no from Order where cust_no is 123");
  db.returnResult(new String[] {"Order 2", "Order 3"};
}
  • Mock DB는 예상된 쿼리를 얻지 못하면 예외를 던질 것이다. 만약 쿼리가 맞다면 MockDataBase는 상수 문자열에서 마치 결과 집합 (result st)처럼 보이는 뭔가를 생성해서 반환한다.
  • 모의 객체는 당신이 모든 객체의 가시성(visibility)에 대해 고민하도록 하여 설계에서 커플링이 감소하도록 한다.
  • 모의 객체를 사용하면 프로젝트에 위험 요소가 하나 추가된다. 모의 객체가 진짜 객체와 동일하게 동작하지 않은 경우가 그 위험 요소다. 이는 모의 개겣용 테스트 집합을 진짜 객체가 사용 가능해질 때 그대로 적용해서 이러한 위험을 줄일 수 있다.

셀프 션트

  • 한 객체가 다른 객체와 올바르게 통신하고 있는 것을 확인하기 위함을 다른 객체가 아닌 테스트 케이스에서 판별하도록 테스트를 수행한다. (자기가 보낸 것이 다시 자신에게 돌아오는지 확인하는 루프백 테스트와 유사)

  • 셀프 션트 패턴은 테스트 케이스가 구현할 인터페이스를 얻기 위해 인터페이스 추출을 해야 한다. 인터페이스를 추출하는 것이 더 쉬운지, 존재하는 클래스를 블랙 박스로 테스트하는 것이 더 쉬운지 결정해서 판단해야 한다.

  • 자바의 경우, 셀프 션트로 사용한 결과로 인터페이스 안의 메서드들을 다 구현해야한다. 인터페이스에 대한 구현은 또한 적절한 값을 되돌리거나 부적절한 오퍼레이션이 호출된 경우 예외를 던지게끔 만들어야 할 것이다.

로그 문자열

  • 메시지의 호출 순서가 올바른지를 검사하려면 어떻게 해야할까? 로그 문자열을 가지고 있다가 메시지가 호출될 때마다 그 문자열에 추가되도록 한다.
  • 로그 문자열은 특히 옵저버를 구현하고, 이벤트 통보가 원하는 순서대로 발생하는지를 확인하고자 할 때 유용하다.

크래시 테스트 더미

  • 호출되지 않을 것 같은 에러 코드(발생하기 힘든 에러 상황)를 어떻게 테스트할 것인가? 실제 작업을 수행하는 대신 그냥 예외를 발생시키기만 하는 특수한 객체를 만들어서 이를 호출한다.
public void testFileSystemError() {
	File f = new File("foo") {
		public boolean createNewFile() throws IOException {
			throw new IOException();
		}
	};
	try {
		saveAs(f);
		fail(); 
	} catch (IOException e) {
		 doExceptionEvent() 
	}
}
  • 객체 전체를 흉내낼 필요가 없다는 점을 제외하면 크래시 테스트 더미는 모의 객체와 유사하다. 우리가 테스트하기 원하는 적절한 메서드만이 오류를 발생시키게끔 하기 위해 유용하게 쓰인다.

깨진 테스트

  • 혼자서 프로그래밍할 때 프로그래밍 세션을 어떤 상태로 끝마치는 게 좋을까? 마지막 테스트가 깨진 상태로 끝마치는 것이 좋다.
  • 나중에 다시 코딩하기 위해 돌아왔을 때, 어느 작업부터 시작할 것인지 명백히 알 수 있다. 전에 하고 있던 생각에 대한 명확하고 구체적인 책갈피를 가지게 되는 것이다.

깨끗한 체크인

  • 팀 프로그래밍을 할 때 프로그래밍 세션을 어떤 상태로 끝마치는 게 좋을까? 모든 테스트가 성공한 상태로 끝마치는 것이 좋다.

  • 코드를 체크인하기 전에 항상 모든 테스트가 돌아가는 상태로 만들어 두어야 한다. 그러면 통합 테스트 슈트에서 테스트가 실패하는 경우도 있는데 그럴 땐 어떻게 해야 할까?

  • 가장 단순한 규칙은 그동안 작업한 코드를 날려버리고 다시 하는 것이다. 실패한 테스트는 프로그램을 내가 완전히 이해하지 못했음을 알려주는 강력한 증거다. 만약 다른 사람이 이 규칙을 따른다면 체크인을 더 자주하려는 경향이 생길 것이다. 왜냐하면 제일 먼저 체크인하는 사람은 작업을 날릴 위험이 없을 테니까.

profile
평범한 백엔드 개발자

0개의 댓글