Chapter 21. Testing

21.1. 단위 테스팅

이 매뉴얼은 스프링 기반 어플리케이션을 위한 효과적인 단위 테스트를 작성하는 데는 별 도움을 주지 못할 것이다.

의존성 주입(Dependency Injection)의 주된 이익들 중 하나는 당신의 코드가 전통적인 J2EE 개발방법에 비해 컨테이너에 덜 의존적이라는 점이다.

당신의 어플리케이션을 구성하는 POJO는 Spring 혹은 어떠한 다른 컨테이너 없이 new(생성자) 실행을 사용하여 간단하게 초기화된 객체들을 가지고, JUnit 테스트에서 테스트 가능해야만 한다. 당신은 당신의 코드를 독립적으로 테스트하기 위해, 가짜(mock) 객체들 혹은 많은 다른 가치있는 테스팅 기법들을 사용할 수 있다. 만약 당신이 Spring을 둘러싼 구조적인 장점--예를 들어, EJB 없는 J2EE에서의 장점--을 따라가고자 한다면, 당신은 완벽한 계층화 만들기가 또한 테스팅을 굉장히 쉽게 해준다는 사실을 알게될 것이다. 예를 들어, 당신은 단위 테스트 중에 퍼시스턴트 데이터들에 억세스할 필요없이, DAO 인터페이스들을 stub 혹은 mock 함으로써 서비스 계층 객체들을 테스트할 수 있을 것이다.

진정한 단위 테스트는 굉장히 빨리 실행될 것이다. 왜냐하면, 어플리케이션 서버, 데이터베이스, ORM 툴 등 셋업해야 할 실행시 하부구조가 아무것도 없기 때문이다. 따라서 진정한 단위 테스트는 당신의 생산성을 높혀줄 것이다.

21.2. 통합 테스팅

그러나, 당신의 어플리케이션 서버에 대한 개발없이 몇몇 통합 테스팅을 실행할 수 있는 것은 매우 중요하다. 이것은 다음과 같은 것들을 테스트할 것이다.

  • 당신의 Spring 컨텍스트들을 올바르게 연결하기

  • JDBC 혹은 ORM 툴을 사용한 데이터 접근--정확한 SQL문. 예를 들어, 당신은 DAO 구현 클래스들을 테스트할 수 있다.

그래서 Spring은 spring-mock.jar.에서 통합 테스팅을 위한 훌륭한 지원을 제공한다. 이것은 Cactus와 같은 툴을 사용한 컨테이너 내부 테스팅에 비해 훨씬 뛰어난 대안으로 여겨질 수 있다.

org.springframework.test 패키지는 Spring 컨테이너를 사용하지만 어플리케이션 서버 또한 다른 배포된 환경에 종속되지 않는, 통합 테스팅을 위한 훌륭한 상위 클래스들을 제공한다. 그런 테스트들은 다른 특별한 배포단계 없이 JUnit-심지어 IDE 내에서도--에서 실행될 수 있다. 그것들은 unit 테스트보다는 늦게 실행되겠지만, Cacus 테스트 또는 어플리케이션 서버 배포판에 의존적인 원격 테스트보다는 훨씬 빠를 것이다.

이 패키지의 상위 클래스들은 다음의 기능들을 제공한다:

  • 컨텍스트 캐슁

  • 테스트 클래스들에 대한 의존성 주입

  • 테스트에 적합한 트랜잭션 관리

  • 테스팅에 유용한 상속받은 인스턴스 변수들

2004년 말이래 많은 수의 인터페이스와 다른 프로젝트들은 이러한 접근방식의 강력함과 유용함을 보여왔다. 기능에 있어서의 몇가지 중요한 부분들을 상세하게 살펴보도록 하자.

21.2.1. 컨텍스트 관리와 캐슁

org.springframework.test 패키지는 Spring 컨텍스트의 일관된 로딩과 로딩된 컨텍스트를 캐슁을 제공한다. 후자는 매우 중요한데, 왜냐하면 만약 당신이 큰 프로젝트에서 일을하고 있다면, 스타트업 시간은 이슈가 될 것이기 때문이다.--이것은 Spring 그 자체의 오버헤드때문이 아니라 Spring 컨테이너에 의해 초기화되는 객체들이 그 자체로 초기화에 시간이 걸리기 때문이다. 예를 들어, 50-100개의 하이버네이트 매핑 파일들을 가진 프로젝트는 그것들을 로드하는데 10-20초 가량 소요되고, 매 테스트 케이스를 실행하기 전의 비용은 굉장한 생산성 감소를 초래하게 될 것이다.

따라서, AbstractDependencyInjectionSpringContextTests 는 컨텍스트들의 위치를 제공하는 하위 클래스들이 반드시 구현해야 하는 abstract protected method를 가진다.

protected abstract String[] getConfigLocations();

이것은 어플리케이션을 설정하기 위해 사용되는 컨텍스트 위치들--전형적으로 클래스패쓰에 위치하는--의 리스트를 제공한다. 이것은 web.xml 혹은 다른 배포 설정에서 기술되는 설정 위치들의 리스트와 동일하거나 거의 비슷할 것이다.

디폴트로, 한번 로드되면, 설정들의 세트는 매 테스트 케이스를 위해 재사용될 것이다. 그래서, 셋업 비용은 단지 한번만 발생되고 뒤이은 테스트 실행은 보다 빠를 것이다.

테스트가 설정 위치를 지저분하게하는 정말 드문 경우, --예를 들어, 빈 정의 혹은 어플리케이션 객체의 상태를 수정 등으로 인해-- 리로딩을 요청하게 될 때, 당신은 AbstractDependencyInjectionSpringContextTests 에 있는 setDirty() 메써드를 호출할 수 있다. 이것은 다음 테스트 케이스를 실행하기 전에 설정을 리로드하고 어플리케이션 컨텍스트를 재생성하게 한다.

21.2.2. 테스트 클래스 인스턴스들의 의존성 주입

AbstractDependencyInjectionSpringContextTests (와 하위클래스들)가 당신의 어플리케이션 컨텍스트를 로드할 때, 그것들은 선택적으로 당신의 테스트 클래스들의 인스턴스들을 세터 주입을 통해 설정할 수 있다. 당신이 해야 할 일은 인스턴스 변수들과 그에 일치하는 세터들을 정의하는 것 뿐이다. AbstractDependencyInjectionSpringContextTestsgetConfigLocations() 메써드에서 기술된 설정 파일들의 세트에서 일치되는 객체들을 자동으로 위치시킬 것이다.

상위 클래스들은 타입에 의한 자동묶기를 사용한다. 그래서 만약 당신이 동일한 타입의 빈 정의들을 여러개 가진다면, 당신은 그러한 특정한 빈들을 위해 이 접근방식을 사용할 수 없을 것이다. 이런 경우, 당신은 상속된 applicationContext 인스턴스 변수를 사용할 수 있으며, getBean()을 사용하여 명백하게 룩업할 수 있을 것이다.

만약 당신이 당신의 테스트 케이스들에 적용된 세터 주입을 원하지 않는다면, 세터들을 아무것도 정의하지 않거나 org.springframework.test 패키지의 클래스 계층의 root인, AbstractSpringContextTests를 상속받도록 하라. 이 클래스는 단지 Spring 컨텍스트들을 로드하기 위핸 편의 메써드들만을 가지고 있으며 아무런 의존성 주입도 실행하지 않는다.

21.2.3. 트랜잭션 관리

실제 데이터베이스에 접근하는 테스트에 있어 한 가지 일반적인 문제점은 데이터베이스 상태에 테스트가 영향을 끼친다는 점이다. 심지어 당신이 개발 데이터베이스를 사용할 때에도, 상태변화는 이후의 테스트들에 영향을 끼칠지 모른다.

또한, 데이터 삽입, 수정 등 많은 동작들은 트랜잭션 외부에서 이루어지거나 검증될 수 없을 것이다.

org.springframework.test.AbstractTransactionalDataSourceSpringContextTests 상위 클래스(와 그 하위 클래스들)는 이러한 필요를 충적시키기 위해 존재한다. 기본적으로, 이 클래스들은 개별 테스트 케이스를 위해 트랜잭션을 생성하고 롤백한다. 당신은 간단하게 트랜잭션의 존재를 확인할 수 있는 코드를 쓰기만 하면 된다. 만약 당신이 트랜잭션적으로 프락시된 객체를 당신의 테스트 내에서 호출한다면, 그 객체들은 그들의 트랜잭션적인 문법에 따라 올바르게 동작할 것이다.

AbstractTransactionalSpringContextTests 는 어플리케이션 컨텍스트 내에 정의된 PlatformTransactionManager 빈에 의존한다. 타입에 의해 자동으로 묶어주기 때문에 이름은 중요한 것이 아니다.

일반적으로 당신은 하위 클래스인 AbstractTransactionalDataSourceSpringContextTests를 상속할 것이다. 이것은 또한 DataSource 빈 정의--이것 역시 아무 이름이어도 상관없다.--가 설정들 내에 존재해야만 한다. 이것은 편리한 질의를 위해 유용한 JdbcTemplate 인스턴스를 생성하고, 선택된 테이블들의 내용들을 삭제하기에 편리한 메써드들을 제공한다. (트랜잭션은 기본적으로 롤백된다는 점을 기억하라. 왜냐하면 그것이 안전하기 때문이다.)

만약 당신이 트랜잭션이 커밋되기를 원한다면,--드물지만, 예를 들어 만약 당신이 데이터베이스에 데이터를 입력시키는 특별한 테스트를 원한다면 유용할 것이다.-- 당신은 AbstractTransactionalSpringContextTests로부터 상속받은 setComplete() 메써드를 호출할 수 있다. 이것은 롤백 대신에 트랜잭션이 커밋되도록 할 것이다.

또한 테스트 케이스가 끝나기 전에 트랜잭션을 종료시키는 편리한 기능이 있는데, endTransaction() 메써드를 호출하면 된다. 이것은 기본적으로 트랜잭션을 롤백시키며, 오로지 이전에 setComplete() 가 호출되었을 때만 커밋시킨다. 이 기능은 만약 당신이 이를테면, 웹 혹은 트랜잭션 외부의 원격티어에서 사용될 하이버네이트 매핑된 객체들과 같이, 연결이 끊어진 데이터 객체들의 동작을 테스트하고자 할 때 유용하다. 종종, lazy 로딩 에러는 UI 테스팅을 통해서만 발견되는데; 만약 당신이 endTransaction() 를 호출한다면, 당신은 JUnit 테스트 수트를 통해 UI의 올바른 동작을 검증할 수 있을 것이다.

이러한 테스트 지원 클래스들은 하나의 데이터베이스와 함께 동작하는 것으로 설계되었다.

21.2.4. 편리한 변수들

당신이 org.springframework.test 패키지를 상속한다면, 당신은 다음의 protected 인스턴스 변수들에 접근할 수 있을 것이다.

  • applicationContext (ConfigurableApplicationContext): AbstractDependencyInjectionSpringContextTests 로부터 상속받았다. 명시적인 빈 룩업을 수행하거나 총괄적으로 컨텍스트의 상태를 테스트할 때 이것을 사용하라.

  • jdbcTemplate: AbstractTransactionalDataSourceSpringContextTests 로부터 상속받았다. 상태를 확인하기 위해 질의를 할 때 유용하다. 예를 들어, 당신이 객체를 생성하고, 그것을 ORM 툴을 사용하여 저장하는 어플리케이션 코드에 대한 테스팅 이전/이후에, 그 데이터가 데이터베이스에 나타나는지를 검증하기 위해 질의할 수 있을 것이다. (Spring은 그 쿼리가 동일 트랜잭션 범위에서 실행되는 것을 보증할 것이다.) 당신은 이것이 올바르게 작동되기 위해, ORM 툴이 그것의 변화들을 flush하도록 호출할 필요가 있을 것이다. 예를 들어, 하이버네이트 Session 인터페이스의 flush() 메써드를 사용해서 말이다.

종종 당신은 통합 테스트를 위해 많은 테스트들에서 사용되는 보다 유용한 변수들을 제공하는 어플리케이션-포괄 상위 클래스를 제공할 것이다.

21.2.5. 예시

Spring 배포판에 포함된 PetClinic 샘플 어플리케이션은 이러한 테스트 상위 클래스들의 사용을 설명해준다.(Spring 1.1.5 이상 버전)

대부분의 테스트 기능은 AbstractClinicTests에 포함되었고, 부분적인 내역들은 아래에서 보이는 대로이다.

public abstract class AbstractClinicTests extends AbstractTransactionalDataSourceSpringContextTests {

   protected Clinic clinic;

   public void setClinic(Clinic clinic) {
      this.clinic = clinic;
   }

   public void testGetVets() {
      Collection vets = this.clinic.getVets();
      assertEquals("JDBC query must show the same number of vets",
         jdbcTemplate.queryForInt("SELECT COUNT(0) FROM VETS"), 
         vets.size());
      Vet v1 = (Vet) EntityUtils.getById(vets, Vet.class, 2);
      assertEquals("Leary", v1.getLastName());
      assertEquals(1, v1.getNrOfSpecialties());
      assertEquals("radiology", ((Specialty) v1.getSpecialties().get(0)).getName());
      Vet v2 = (Vet) EntityUtils.getById(vets, Vet.class, 3);
      assertEquals("Douglas", v2.getLastName());
      assertEquals(2, v2.getNrOfSpecialties());
      assertEquals("dentistry", ((Specialty) v2.getSpecialties().get(0)).getName());
      assertEquals("surgery", ((Specialty) v2.getSpecialties().get(1)).getName());
}

노트:

  • 이 테스트 케이스는 이것은 의존성 주입과 트랜잭션적인 동작을 위해 org.springframework.AbstractTransactionalDataSourceSpringContextTests 를 상속받았다.

  • clinic 인스턴스 변수--테스트될 어플리케이션 객체--는 setClinic() 메써드를 통해 의존성 주입에 의해 세팅된다.

  • testGetVets() 메써드는 테스트될 어플리케이션 코드의 올바른 동작을 검증하기 위해 JdbcTemplate 변수를 사용하는 방법을 보여준다. 이것은 더욱 강력한 테스트들을 가능하게 하고 정확한 테스트 데이터에 대한 의존성을 줄여준다. 예를 들어, 당신은 테스트들을 중단하지 않고도 데이터베이스에 부가적인 row들을 추가할 수 있다.

  • 데이터베이스를 사용하는 많은 통합 테스트들처럼, AbstractClinicTests 에서의 테스트들의 대부분은 테스트 케이스들이 실행되기 전 데이터베이스 내에 이미 존재하는 최소량의 데이터베이스에 의존한다. 그러나, 당신은 또한 당신의 테스트 케이스들 내에서 --물론, 하나의 트랜잭션 내에서-- 데이터베이스에 입력하는 것을 선택할 수도 있다.

PetClinic 어플리케이션은 3가지 데이터 접근 기술들을 제공한다.--JDBC, 하이버네이트, 그리고 아파치 OJB. 그래서 AbstractClinicTests 는 컨텍스트 위치들을 기술하지 않는다. 이것은 AbstractDependencyInjectionSpringContextTests의 필수적인 protected abstract 메써드를 구현하는 하위 클래스들에 따라 다르다..

예를 들어, PetClinic 테스트의 JDBC 구현은 다음의 메써드를 포함한다.

public class HibernateClinicTests extends AbstractClinicTests {

   protected String[] getConfigLocations() {
      return new String[] { 
         "/org/springframework/samples/petclinic/hibernate/applicationContext-hibernate.xml" 
      };
   }
}

PetClinic은 매우 간단한 어플리케이션이기 때문에, 단 하나의 Spring 설정 파일만이 존재한다. 물론, 보다 복잡한 어플리케이션들은 일반적으로 Spring 설정을 여러 개의 파일들로 쪼갤 것이다.

최하위 클래스에서 정의되는 대신, 설정 위치들은 모든 어플리케이션-특화 통합 테스트를 위한 일반적인 베이스 클래스에서 종종 기술된다. 이것은 또한 유용한 --자연스럽게 의존성 주입에 의해 제공되는--하이버네이트를 사용하는 어플리케이션의 경우의 HibernateTemplate와 같은인스턴스 변수들을 추가할 것이다.

가능한 한, 당신은 배포될 환경에서와 동일한 Spring 설정 파일들을 통합 테스트에서도 가져야만 한다. 유일하게 다른점이라면 데이터베이스 커넥션 풀링과 트랜잭션 하부구조와 관련된 부분들 뿐이다. 만약 당신이 고성능(? full-blown) 어플리케이션 서버에 배포할 것이라면, 당신은 아마도 그것의 (JNDI를 통해 가능한) 커넥션 풀과 JTA 구현을 사용할 것이다. 따라서 제품 내에서, 당신은 DataSourceJtaTransactionManager를 위해 JndiObjectFactoryBean을 사용할 것이다. JNDI와 JTA는 컨테이너 외부 통합 테스트에서는 가능하지 않을 것이다. 따라서, 당신은 반드시 Commons DBCP BasicDataSourceDataSourceTransactionManager 혹은 HibernateTransactionManager와 같은 조합을 사용해야 할 것이다. 당신은 어플리케이션 서버와 로컬 설정 사이의 선택을 가지는 이러한 다양한 동작들을, 테스트와 제품 환경들 사이에 변화가 없는 모든 다른 설정들로부터 분리하여, 하나의 XML 파일에 분해할 수 있다.

21.2.6. 통합 테스트 실행하기

통합 테스트는 자연적으로 평범한 유닛 테스트들에 비해 보다 환경적인 의존성을 가진다.그런 통합 테스팅은 유닛 테스팅의 대체물이 아니라 테스팅의 부가적인 형태이다.

주된 의존성은 전형적으로 어플리케이션에 의해 사용되는 완전한 스키마를 포함하는 개발 데이터베이스에 대한 것이다. 이것은 아마도 또한, DBUnit같은 툴에 의해 셋업되거나 데이터베이스 툴 셋을 사용하여 임포트된, 테스트 데이타를 포함한다.