2부 다채로운 테스트 전략


5장 테스트 커버리지와 개발

테스트 커버리지

테스트가 수행될 때 어떤 코드를 훑고 가는지 정확하게 알 필요가 있다. 이 때 측정 지표가 되는 것이 커버리지이며, 이상적으로는 애플리케이션 코드의 100%가 커버되어야한다.

블랙박스 테스트

블랙박스 테스트 방식을 취할 경우, 애플리케이션의 공개 API를 다루는 테스트를 작성할 수 있다. 가이드 문서만 주어지므로 내부의 특수한 파라미터 값을 사용해야만 발생하는 상황을 검사하는 테스트는 작성할 수 없다.

화이트박스 테스트

단위 메소드 구현에 대한 자세한 지식을 활용해 단위 테스트를 작성할 수 있다. 더 많은 메소드에 접근 가능하고 입력과 보조 객체(스텁, 목 등)의 동작까지 제어할 수 있으므로 높은 코드 커버리지를 달성할 수 있다.

Cobertura

JUnit과 통합된 코드 커버리지 툴이다. 무료 오픈소스이며, Ant, Maven과 통합되어 있고 콘솔에서도 사용 가능하다. HTML/XML 형태의 보고서를 생성해주고 테스트된 코드 라인과 분기의 비율을 클래스/패키지/전체 수준에서 보여준다.

[참고] Cobertura Coverage Report of JUnit itself


테스트 가능한 코드(testable code)

기존 코드를 리팩터링해서 테스트하기 쉽게 고치는 것보다, 아예 처음부터 테스트하기 쉬운 코드를 작성하는 것이 항상 더 쉽다는 것을 잊지 말자.

1. 공개 API는 계약이다.

공개 메소드는 어플리케이션의 컴포넌트와 오픈소스 프로젝트, 존재조차 인식할 수 없는 상용 제품들을 이어주는 연결 고리이다. 함부로 변경해서는 안되고 반드시 모든 공개 메소드를 테스트 해야하는 이유이다.
"공개(public) API의 시그니처는 절대 변경하지 않는다."

2. 종속성을 줄여라.

단위 테스트는 코드를 고립시켜 독립적으로 검증해야 한다. 테스트 케이스는 단순 명료해야 한다. 따라서 테스트 가능한 코드를 작성하기 위해서는 종속성을 가능한 최소화시켜야 한다.

class Vehicle {
  Driver d = new Driver();   //이부분을
  boolean hasDriver = true;
  private void setHasDriver(boolean hasDriver){
    this.hasDriver = hasDriver;
  }
}

종속성을 줄이기 위해 코드에서 팩토리(객체 생성 담당 메소드)와 로직 수행을 처리하는 메소드를 분리한다.
Vehicle 객체는 자신이 생성될 때마다 Driver 객체도 함께 생성한다. 개념이 섞인 경우이다.

⇒ Driver 인터페이스를 Vehicle 클래스에 건네주는 방식으로 수정한다.

class Vehicle {
  Driver d;
  boolean hasDriver = true;
  Vehicle(Driver d) {   //이런 식으로
    this.d = d;
  }
  private void setHasDriver(boolean hasDriver) {
    this.hasDriver = hasDriver;
  }
}

이제 mock Driver 객체를 만들어 건내줄 수 있고, 종료에 상관없이 JuniorDriver, SeniorDriver 등 모든 Driver 구현체의 mock 객체를 만들어 Vehicle 클래스에 건넬 수 있다.

3. 생성자는 간단하게 만들어라.

모든 테스트 케이스는
테스트 하려는 클래스를 생성한다 > 생성한 클래스를 특정 상태가 되도록 설정한다 > 클래스의 최종 상태를 확인한다
와 같은 과정을 거친다.
인스턴스 변수에 값을 할당하지 않고 생성자에서 작업을 수행하게 되면 앞의 두 작업이 섞여버린다. 이는 아키텍처 관점에서도 좋지 않다.

4. 최소 지식의 원칙을 따르라.

디미터의 법칙(The Law of Demeter)
최소 지식의 원칙. 클래스는 반드시 자신에게 꼭 필요한 만큼만 알아야 한다.

class Car {
  /*
  private Driver driver;
  Car(Context context) {
    this.driver = context.getDriver();   //Context 객체가 getDriver 메소드를 갖고있음을 알아야만 한다.
  }
  */
  Car(Driver driver) {
    this.driver = driver;
  }
}

⇒ 객체를 요구하되, 객체를 검색하지는 말라. 그리고 어플리케이션에서 꼭 필요한 객체만 요청하라

5. 숨겨진 종속성과 전역 상태를 피하라.

숨겨진 종속성

public void reserve() {
  DBManager manager = new DBManager();
  manager.initDatabase();
  //Reservation r = new Reservation();   이렇게 작성하면 종속성을 숨기는 결과를 낳는다.
  Reservation r = new Reservation(manager);   //객체 생성시에 명시적으로 받고있다.
  r.reserve();
}

수정된 코드와 같이 작성을 해야 Reservation 객체가 DBManager가 설정되었을 때만 동작할 수 있다는 것을 알 수 있다.

부족한 캡슐화
추상화의 구성원 하나 이상에서 선언된 접근 가능성이 실제로 필요한 것 보다 더 관대할 때 악취(bad smell)가 발생한다.
이는 특히 소프트웨어 시스템 전반에서 모든 추상화에 접근 가능한 전역 상태(전역 변수, 전역 자료구조 등)가 있을 때 악취의 극단적인 형태가 드러난다. 시스템에 있는 모든 객체가 전역 상태에 접근하여 변경할 수 있고, 서로 직접 의존해서는 안되는 두 객체 사이에 은밀한 통신 채널이 생성 될 수 있기 때문이다.
이렇듯 전역으로 접근 가능한 변수와 자료구조는 소프트웨어 시스템의 이해 가능성, 안정성, 테스트 가능성에 심각한 영향을 미칠 수 있다. 전역 객체에 접근해야 할 때에는 그 객체 뿐 아니라 그 객체가 참고하는 다른 모든 객체도 함께 공유해야 한다.
"... 모두가 일부의 친구만 공개하고, 다른 친구들은 비밀로 하고 있는 사회를 상상해보자. (중략) 만약 여러분이 관계를 만들어낸(코딩한) 사람이라면, 여러분은 모든 종속성을 정확히 알고 있지만, 여러분 이후에 합류한 사람들은 모두 당황하게 될 것이다. 친구라고 선언된 사람들만이 유일한 친구도 아닐 뿐더러, 알 수 없는 비밀 경로로 정보가 전달되기 때문이다. 거짓으로 가득한 사회에 살게 된 것이다."

6. 싱글톤의 장단점을 파악하고 주의해서 사용하라.

싱글톤 패턴
프로젝트 진행시 어플리케이션 전 영역에 걸쳐 하나의 클래스의 단 하나의 인스턴스만을 생성하는 것을 말한다.
예를 하나 들어 보면, ㅇㅇ문고에서 책을 관리하기 위해서는 관리대장이나 관리를 위한 프로그램이 필요할 것이다. 관리대장을 만든다고 가정할 때 관리대장에는 책이름, 지은이, 출판사 등 책에 대한 정보를 적게 될 것이다.
그러면 관리대장은 몇 권이 필요할까?
만약 한권의 관리대장이 ㅇㅇ문고에 있는 책정보를 모두 담을 수 있다면 정답은 당연히 한 권이 될 것이다. 책 100권이 있다고 관리대장도 100권을 만드는 사람은 없을 것이다.
책을 관리하는 관리대장을 싱글톤이라고 생각하면 된다. 책은 100권, 1000권이 있어도 이를 관리하는 관리대장은 하나의 인스턴스면 된다.
이를 위해 프로그램 종료시점까지 하나의 인스턴스만을 생성하고 관리하는 것이 싱글톤의 개념이다.

public class Singleton {
    private static Singleton INSTANCE;
    private Singleton() {}
    public static Singleton getInstance() {
        if(INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

싱글톤 패턴의 단점
싱글톤 디자인 패턴은 객체가 단 한 번만 인스턴스화됨을 보장해야 한다. 이를 위해 생성자를 private로 만들어 외부로부터 숨겨버린다. 그렇기 때문에 직접 호출할 수도, 테스트할 수도 없다. 싱글톤은 어플리케이션에 전역 상태를 만들어 낸다는 명백한 취약점이 존재한다. 따라서 반드시 주의해서 사용해야 한다.

[참고] 싱글톤 패턴 (Singleton Pattern)
http://itdp1024.tistory.com/22

7. 제네릭 메소드를 애용하라.

단위 테스트는 고립된 상태에서의 테스트이다. 고립된 상태로 만들기 위해선 코드 상에 연결점을 준비하여 필요시에 쉽게 테스트 코드로 대체할 수 있도록 만들어야 한다. 연결점들은 다형성 - polymorphism. 객체가 다른 객체인 것처럼 보이게 하는 능력 - 을 활용한다.
정적 메소드를 사용하고 다형성 활용이 불가하다면 어플리케이션과 테스트 코드 모두를 재활용할 수 없다는 뜻이다. 이는 코드 중복 생성으로까지 이어질 수 있다.

[참고] Generic - 원천(Raw)타입을 사용하지 맙시다.
http://ojava.tistory.com/27
[참고] Generic - 제네릭 메소드를 애용하자.
http://ojava.tistory.com/37

8. 상속보다 컴포지션을 활용하라.

런타임에 최대한 유연한 코드를 만드는 것이 목표이다. 상속보다는 컴포지션 방식이 확실히 객체의 상태 변경이나 테스트에 더 적합한 코드를 만들어 준다.

[참고] 가급적 상속(Inheritance) 보다는 컴포지션(composition)을 사용하자.
http://aroundck.tistory.com/617

9. 조건 분기보다 다형성을 활용하라.

클래스가 너무 복잡하다면 생성하는 것 자체만으로 골머리를 앓게 되기도 한다. 긴 switch나 if문의 사용을 피해서 복잡도를 감소시키자.

[참고] 실습 동영상
"Replace Conditional with Polymorphism"
https://www.youtube.com/watch?v=NCsoEEz_Ta0
"Replace Conditionals with Polymorphism TDD + Java + JUnit"
https://www.youtube.com/watch?v=6a_USwOEXq4


테스트 주도 개발(Test-driven Development, TDD)

테스트 주도 개발은 자동화 테스트가 실패했을 때와 코드 중복을 제거하려 할 때에만 새로운 코드를 작성하도록 권하는 프로그래밍 실천법이다. TDD의 목표는 '작동하는 깨끗한 코드(clean code that works)'이다.
TDD를 제대로 알고 싶다면, 켄트 백이 저술한 "테스트 주도 개발"을 읽어보기 바란다.


6장 스텁을 활용한 포괄적인 테스트

7장 목 객체를 활용한 테스트

[참고]
Mock을 이용한 단위 테스트 http://egloos.zum.com/kingori/v/4169398
Stub과 Mock을 사용해서 테스트 짜기 http://okjungsoo.tistory.com/entry/Stub과-Mock의-차이

테스트 더블

테스트 작성 시 테스트 대상 코드와 상호작용하는 객체 . 스텁 객체, 페이크 객체, 스파이 객체, 목 객체 등이 있다.

목 객체

Mock Object 는 검사하고자 하는 코드와 맞물려 동작하는 객체들을 대신하여 동작하기 위해 만들어진 객체이다. 목 객체는 가짜 객체를 이용해 테스트 대상 객체가 올바른 방식으로 상호작용하였는지 검증한다. 대게 테스트당 하나의 목 객체만 사용한다.
검사하고자 하는 코드는 Mock Object 의 메서드를 부를 수 있고, 이 때 Mock Object는 미리 정의된 결과 값을 전달한다. Mock Object는 자신에게 전달된 인자를 검사할 수 있으며, 이를 테스트 코드로 전달할 수도 있다.

스텁 객체

시스템 상의 외부 의존물을 대신하기 위해 쓰이는 제어 가능한 대체물이다. 스텁을 사용하면 외부 의존 관계를 직접 다루지 않고도 코드를 테스트해 볼 수 있다. Stub 은 테스트 과정에서 일어나는 호출에 대해 지정된 답변을 제공하고, 그 밖의 테스트를 위해 별도로 프로그래밍 되지 않은 질의에 대해서는 대게 아무런 대응을 하지 않는다. 또한 Stub은 email gateway stub 이 '보낸' 메시지를 기억하거나, '보낸' 메일 개수를 저장하는 것과 같이, 호출된 내용에 대한 정보를 기록할 수 있다.

목과 스텁의 차이

그 차이는 매우 작거나 없는 것 처럼 보일 수 있다. 기본적인 차이점은 스텁은 테스트가 실패하게 할 수 없지만, 목은 가능하다.
스텁으로는 절대로 테스트가 실패하지 못한다는 사실에 주목한다. assert는 테스트 대상 클래스에 의해서 이루어진다.

반면 목은 테스트 실패 여부를 검증하기 위해서이다. assert는 목을 대상으로 수행한다.


다시 말하면, 스텁은 테스트가 잘 돌아가도록 돕기 위해서 사용되며 목은 테스트가 실패할 수 있을 정도의 영향을 미친다.

State-based testing과 Result-driven testing

Mock Object 활용을 통한 테스트 방식의 전환. 상태 기반에서 행위 기반으로

Mock Object 및 Library 적용을 통한 변화 중 가장 큰 것이 테스트 방식의 전환이 아닐까 생각한다.

Stub 을 활용한 "상태 기반" 테스트 방식은 Stub 만들고, 테스트 대상 메서드 등을 실행하고, 마지막에 원하는 상태가 되었는 지를 검증하는 구조이다. 즉, "영숙이가 철수한테 1000만원을 이체를 한다" 라면 "영숙이 잔고 2000만원, 철수 잔고 500만원" 이라는 초기 상태를 설정하고, 뭔가 중간에 쿵짝쿵짝 한 다음, 마지막에 "영숙이 잔고 1000만원, 철수 잔고 1500만원" 이라는 최종 상태가 우리가 원하는 상태인지를 확인하는 것이다.

이와 달리 "행위 기반" 테스트 방식은 "영숙이가 철수한테 1000만원을 이체를 한다" 라면 "영숙이 계좌에서 1000만원 빼" , "철수 계좌에 1000만원 넣어" 라는 중간 행위가 제대로 수행되었는지를 확인한다. 이 차이는 테스트 작성의 개념을 완전히 바꿔 놓는 것이다.

results matching ""

    No results matching ""