Home 8장 TDD에 대한 다양한 시각
Post
Cancel

8장 TDD에 대한 다양한 시각

8.1 TDD와 소프트웨어 디자인

TDD가 주는 설계상 이점

TDD로 개발하면 TDD 자체가 단위 단위의 작은 설계를 만들어낸다. 입력과 출력, 해당 모듈이 동작하기 위해 필요한 요소 파악 등이 사전에 확실하게 고려되고 테스트된다. 이러한 모습을 ‘강형 마이크로 디자인’이라고 한다. 이는 모듈에 대한 의존성이 상대적으로 적고, 각 모듈에 대한 테스트를 독립적으로 수행할 수 있도록 만들기 때문에 자기 완결성이 높은 모듈이 된다. 그래서 소프트웨어의 복잡도가 낮아지고 개발자가 좀 더 일반화되고 추상화된 형태의 디자인에 더 집중할 수 있게 도와준다.

TDD와 객체 지향 프로그래밍

OOP의 근간이 되는 기초 원칙 중에는 모듈은 높은 응집도를 유지하고 낮은 결합도를 갖도록 만들어야 한다 는 원칙이 있다. TDD로 작성을 하게 되면 기능과 객체의 관계를 스스로 먼저 고민하게 된다.

테스트 케이스 코드는 입력과 출력이 명확하고, 사용되는 재료가 적을수록 작성하기가 쉽다. 즉 의존관계가 많은 코드는 테스트 코드 자체를 만들기가 어렵다. TDD는 자신의 모듈에 필요한게 무엇인지를 미리미리 고민하게 만들고 기능위주로 테스트를 하기 때문에 불필요한 속성/필드가 무엇인지도 초반부터 밝혀진다. 따라서 테스트를 작성하다 보면 어떠한 기능이 어떤 클래스에 의존하게 되는지 미리 알 수 있고, 해당 기능이 어디에 있어야할지 생각할 수 있다. 이것이 응집도를 높이고 결합도를 낮추는 모습이다.

유연한 코드

흔히 변화에 쉽게 적응할 수 있는 코드를 유연한 코드라 부른다. 소프트웨어의 변경이 쉬워지려면 코드가 유연해야하는데, 소프트웨어 공학에서 추구하는 유연함이란 때로 개발비용 증가의 또 다른 요소가 되기도 한다.

계약에 의한 설계

계약이란 로직의 선/후행 조건 같은 단순한 개념에서부터, 개발에 필요한 업무규칙, 고객의 요구사항에 이르기까지의 다양한 경우를 지칭한다.

예) 메소드 진행 전 인자로 받은 객체의 null 여부 확인(선행조건), 통장 개설시 최소한 100원 이상을 입금해야 한다.(업무규칙), 회원가입 시 반드시 유효한 이메일 주소를 입력해야한다.(요구사항)

1
2
3
4
public void testAccout() {
  wrongAccount = new Account(-10000);
  assertTrue("계좌는 반드시 양수 금액으로 생성되어야 함", wrongAccount.getBalance() > 0);
}

위 처럼 작성하게 되면 Account 클래스의 생성자를 테스트해주는 것도 아니고, 그렇다고 getBalance() 메소드를 테스트하는 것도 아니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void testAccout() {
  try{
    wrongAccount = new Account(-10000);
    assertTrue("계좌는 반드시 양수 금액으로 생성되어야 함", false);
  }catch (IllegalArgumentException e) {
    assertTrue(true);
  }
}

public Account(int money) {
  if(money < 0) {
    throw new IllegalArgumentException("계좌 생성시 money는 양수여야 함, 현재 " + money);
  }
  this.balance = money;
}

8.2 TDD 유의사항

1.테스트 케이스는 이름이 중요한다

보통 테스트가 실패하면 실패한 메소드의 이름이 테스트의 기준이 된다. 따라서 테스트의 이름이 잘 작성되어 있어야 실패에 대한 대응을 빠르게 할 수 잇다. 또 경우에 따라서는 테스트 메소드의 이름에 한글을 사용하는 것도 도움이 된다. 로직을 살펴보지 않고 메소드 이름만으로도 충분히 의미를 전달할 수 있을 정도로 작성하는 것이 중요하다.

2.더 이상 제대로 동작하지 않는 테스트 케이스는 제거한다

3.TDD는 자동화된 테스트를 만드는 것이 최종 목표가 아니다

TDD는 개발의 목표 지점을 미리 정하기 위해 단위 테스트 케이스를 만들고, 목표 상태 도달 여부를 빨리 확인하기 위해 단위 테스트 케이스를 자동화 시킨다. 자동화된 단위 테스트 케이스들은 TDD의 부산물이다. 즉 개발과 설계를 위한 보조 도구이지 목적은 아니다.

4.모든 상황에 대한 테스트 케이스를 만들 필요는 없다

요구사항에 맞는 현재 필요한 기능에 대한 테스트만 만든다.

5.여러 개의 실패하는 테스트 케이스를 한 번에 만들지 않는다

개발 중에는 작성하고 있는 하나의 클래스에 대해서는 하나의 실패하는 테스트만 유지한다. 해당 실패를 성공시킨 다음에 다음 실패 케이스를 작성한다.

6.하나의 테스트 케이스는 하나만 테스트하도록 작성한다

7.전통적인 테스트 기법을 배워두자

8. 테스트 케이스는 최대한 고립시킨다

단위 테스트는 다른 모듈이나 시스템에 최대한 독립적이고 고립된 형태로 작성될수록 단단한 테스트 케이스가 될 수 있다. 가급적 다음과 같은 것들이 테스트에 들어가지 않도록 작성하면 더 좋다.

  • 테스트 케이스가 작성되어 있지 않은 다른 모듈
  • 데이터베이스 연동
  • 외부 시스템
  • 콘솔 출력
  • 네트워크

최대한 의존관계를 줄이고, 경우에 따라서는 Mock 객체를 이용해서라도 독립성을 보장해줘야 특정 테스트 실패에 따른 실패 전파현상을 최소한으로 줄일 수 있다.

8.3 TDD와 리팩토링

TDD에서 리팩토링은 중요한 부분을 차지한다. 리팩토링은 설계를 개선하고 유지하는 것이 목표다. TDD를 할 때 하나의 단계가 끝날 때마다 리팩토링을 하면 대상 코드뿐 아니라 자동화된 테스트 케이스 자체도 사용하기 좋은 코드로 정련이 된다.

TDD에서 리팩토링은 매우 중요하다.

8.4 TDD와 짝 프로그래밍

TDD와 짝 프로그래밍은 잘 어울린다. 보통 사용자 스토리를 완성하기 위한 세부 ToDo 리스트가 만들어진 다음에 짝 프로그래밍으로 TDD를 적용해보면 효과가 더 높다.

  • 팀원 간의 많은 대화로 목표 시스템에 대한 이해 증가
  • 제품에 대한 공동설계와 공동 소유
  • 개발에 효율을 높일 수 있는 작업 방식의 공유
  • 개발 스킬 향상

8.5 TDD와 심리학

TDD를 실천하기 위해 팀 전체에 도입하고자 할 때 몇 가지 방법이 있다.

  1. 앞으로 TDD는 필수다 라고 강제력 있는 발언을 한다. - 개발 자체를 부담스러운 과정으로 만들 위험이 있다. 따라서 좀 더 긍정적이면서 개발과 별개가 아닌 일부라는 인식을 가질 수 있도록 유도할 필요가 있다.
  2. TDD로 개발하라고 말한 다음 자율에 맡긴다. - 십중팔구 흐지부지 된다.
  3. 테스트 커버리지를 강제화 한다. 매일 체크해서 관리지표로 삼는다. - 꽤 괜찮은 효과를 볼 수 있다.

TDD를 어렵게 만드는 요인

환경적 요인

새로운 것을 배우면 적응돼서 효과를 볼 때까지 소요되는 학습 곡선의 마이너스 영역을 가질 여유가 없고, TDD로 개발하면 단순히 자판을 누르면서 진행해나가는 시행착오 반복 식의 개발보다는 초반에 고민해야 하는 요소가 더 많다. 그렇기 때문에 피로는 누적되고 일정은 기다려주지 않는다.

산만한 아키텍쳐

TDD에서 집중하는건 단위 기능이지만 객체지향 언어에서는 대부분 오브젝트(클래스)로 구성된다. 이는 권한, 위임, 책임등을 갖고 외부와 통신한다. TDD를 통해 만들어진 자동화된 단위 테스트는 그 영역을 커버하지 못한다. 그러다보니 단위 테스트들이 한 방향으로 치우치거나 산만하게 흩어져 있는 모습이 될 수 있다.

의존성 전파로 인한 연쇄적인 테스트 실패들

1
2
3
4
5
6
7
8
9
@Test
public void testAddAccount() { ... }

@Test
public void testUpdateAccount() {
  testAddAccount()

  //......
}

이런 경우 testAddAccount()가 실패하게 되면 이를 호출한 테스트 케이스들도 실패하게 된다.

8.7 행위 주도 개발

TDD는 프로그램의 가장 하위 부분인 단위 메소드의 기능에 집중한다. 즉 시스템 안쪽에서부터 시작해서 사용자에 가까운 바깥쪽으로 만들어나가는 방식이다. 그래서 테스트를 어느 정도 작성하면 될 것인가, 어떤것을 테스트하고 어떤 것은 테스트를 안 해도 될까에 대한 적절한 판단도 함께 필요하다.

BDD의 정의와 목표

책임관계자의 관점에서 보는 애플리케이션의 행위(동작) 중 가치 있는 기능부터 개발하는 방식이다. 애플리케이션의 로직 중에서 특히 업무규칙과 관련된 부분을 밖으로 노출하는 데 집중한다.

BDD의 특징

  1. BDD는 사용자에 좀 더 가까운 고수준의 기능영역을 우선적으로 다룬다.
  2. 무엇을 테스트할 것인가?에 좀 더 초첨을 맞추고 있다. -> 가장 먼저 구현돼야 하는 기능은 무엇인가?
  3. 테스트 메소드 작성에 집중할 수 있는 문장적인 템플릿을 제공한다.
GivenWhenThen
주어진 상황이나 조건기능 수행예상 결과

BDD 접근 전략

개발자는 반드시 알아야 하고 견뎌내야하는 로직을 다룰 때는 TDD가 좋고, 고객 관점에서 이야기하거나 대상 시스템의 최종 형태를 파악하며 개발해야 할 때, BDD를 우선적으로 사용한다.

BDD와 TDD

TDD가 예제에 의한 개발과 단위 테스트 케이스를 지향한다면 BDD는 사용자 시나리오를 통해 사용자 테스트와 회귀 테스트를 지향한다. 또 TDD가 개발자를 위한 방식이라면 BDD는 책임관계자와 함께 하기 위한 방식이다.

BDD 정리

BDD는 TDD에서 사용되는 일종의 개발 전략이고 어떤 면에서는 발전형이라고 볼 수도 있다. 또 핵심 업무도메인을 대하는 일종의 대화법이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LoginSteps extends Steps {
  //....

  @Given("로그인하지 않은 상태라고 가정하고")
  public void logOut() {
    page.click("logout");
  }

  @When("$username과 패스워드 $password로 로그인 했을 때")
  public void logIn(String username, String password) {
    page.click("login");
  }

  @Then("다음과 같은 메시지가 보여야한다. \"$message\"")
  public void checkMessage(String message) {
    ensureThat(page, containsMessage(message));
  }
}
This post is licensed under CC BY 4.0 by the author.

7장 개발 영역에 따른 TDD 작성 패턴

9장 자주 접하게 되는 질문들, FQA