Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 10장 테스트 #2

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.



10.3.5.2 컨텍스트 관리
각 TestContext는 테스트 인스턴스에 대해 신뢰할 수 있는 컨텍스트 관리와 캐싱지원을 제공한다. 테스트 인스턴스는 설정된 ApplicationContext로의 접근이 자동으로 허용되지 않는다. 하지만 테스트 클래스가 ApplicationContextAware 인터페이스를 구현하면 ApplicationContext의 참조가 테스트 인스턴스에 제공된다. AbstractJUnit4SpringContextTests와 AbstractTestNGSpringContextTests가 ApplicationContextAware를 구현했으므로 ApplicationContext에 대한 접근을 제공한다.

@Autowired ApplicationContext
ApplicationContextAware 인터페이스를 구현하는 대신 필드나 setter 메서드에 @Autowired 어노테이션을 붙혀서 테스트 클래스의 어플리케이션 컨텍스트를 주입할 수 있다. 다음 예를 보자.


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class MyTest {

  @Autowired
  private ApplicationContext applicationContext;

  // class body...
}

@Autowired를 통한 의존성 주입을 기본적으로 설정된 DependencyInjectionTestExecutionListener가 제공한다.(Section 10.3.5.3, “테스트 픽스처의 의존성 주입”)를 참고해라.)

TestContext 프레임워크를 사용하는 테스트 클래스들은 어플리케이션 컨텍스트를 설정하기 위해 어떤 클래스도 상속받을 필요가 없고 특정 인터페이스를 구현할 필요도 없다. 대신 클래스 수준의 @ContextConfiguration 어노테이션을 선언함으로써 설정이 이뤄진다. 테스트 클래스가 어플리케이션 컨텍스트 리소스 locations이나 설정 classes를 명시적으로 선언하지 않았다면 설정된 ContextLoader가 기본 위치나 기본 설정 클래스에서 어떻게 컨텍스트를 로드할 지 결정한다.

다음 섹션에서는 ApplicationContext를 XML 설정 파일이나 스프링의 @ContextConfiguration 어노테이션을 사용한 @Configuration 클래스로 어떻게 설정하는지 설명한다.


XML 리소스를 사용한 컨텍스트 설정
XML 설정파일로 테스트의 ApplicationContext를 로딩하려면 테스트 클래스에 @ContextConfiguration 어노테이션을 붙히고 XML 설정 메타데이터에 리소스 위치를 담고 있는 locations 속성을 가진 배열을 설정해라. 이 평범한(plain) 경로(예를 들면 "context.xml"같은)는 테스트 클래스를 정의한 패키지의 상대적인 클래스패스 리소스로 다뤄질 것이다. 슬래시로 시작하는 경로는 절대경로 클래스패스 위치로 다룬다. 예를 들어 "/org/example/config.xml"같은 것이다. 리소스 URL을 나타내는 경로(classpath:, file:, http: 등의 접두사가 붙은 경로)는 원래 그대로 사용될 것이다. 대신에 자신만의 커스텀 ContextLoader나 고급 사용을 위해 SmartContextLoader를 구현하거나 설정할 수 있다.

@RunWith(SpringJUnit4ClassRunner.class)
// 어플리케이션 컨텍스트는 클래스패스의 루트의 "/app-config.xml"와
// "/test-config.xml"에서 로드될 것이다.
@ContextConfiguration(locations={"/app-config.xml", "/test-config.xml"})
public class MyTest {
  // class body...
}

@ContextConfiguration 는 표준 자바 value 속성으로 locations 속성의 별칭을 지원한다. 그러므로 커스텀 ContextLoader를 설정할 필요가 없다면 locations 속성명의 선언을 생략하고 다음 예제에 나오는 것처럼 약식으로 리소스 위치를 선언할 수 있다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-config.xml"})
public class MyTest {
  // class body...
}

@ContextConfiguration 어노테이션의 locations와 value 속성을 모두 생략하면 TestContext 프레임워크가 기본 XML 리소스 위치를 찾으려고 시도할 것이다. 특히, GenericXmlContextLoader는 테스트 클래스 이름에 기반해서 기본 위치를 찾는다. 클래스 이름이 com.example.MyTest이라면 GenericXmlContextLoader는 "classpath:/com/example/MyTest-context.xml"에서 어플리케이션 컨텍스트를 로드한다.

package com.example;

@RunWith(SpringJUnit4ClassRunner.class)
// ApplicationContext는 "classpath:/com/example/MyTest-context.xml"에서 로드될 것이다.
@ContextConfiguration
public class MyTest {
  // class body...
}


@Configuration 클래스를 사용한 컨텍스트 설정
@Configuration 클래스(Section 4.12, “자바기반의 컨테이너 설정” 참고)를 사용해서 테크트의 ApplicationContext를 로드하려면 테스트 클래스에 @ContextConfiguration 어노테이션을 붙히고 설정 클래스에 대한 찹조를 답고 있는 classes 속성의 배열을 설정해라. 아니면 자신만의 커스텀 ContextLoader나 고급 사용을 위한 SmartContextLoader를 구현하거나 설정할 수 있다.

@RunWith(SpringJUnit4ClassRunner.class)
// ApplicationContext는 AppConfig와 TestConfig에서 로드될 것이다
@ContextConfiguration(classes={AppConfig.class, TestConfig.class})
public class MyTest {
  // class body...
}

@ContextConfiguration 어노테이션에서 classes 속성을 생략하면 TestContext 프레임워크는 기본 설정 클래스의 존재를 찾으려고 시도할 것이다. 특히, AnnotationConfigContextLoader는 @Configuration Javadoc에 나온 것처럼 설정 클래스 구현체의 요구사항을 만족시키는 어노테이션이 붙은 테스트 클래스의 정적 이너클래스를 모두 찾을 것이다. 다음 예제에서 OrderServiceTest 클래스는 Config라는 정적 이너 설정 클래스를 선언했고 테스트 클래스를 위한 ApplicationContext를 로드하는데 자동으로 사용할 것이다. 설정 클래스의 이름은 임의로 사용할 수 있다. 추가적으로 필요하다면 테스트 클래스는 하나이상의 정적 이너 설정클래스를 포함할 수 있다.

package com.example;
 
@RunWith(SpringJUnit4ClassRunner.class)
// ApplicationContext는 정적 이너 Config 크래스에서 로드될 것이다.
@ContextConfiguration
public class OrderServiceTest {
 
  @Configuration
  static class Config {

    // 이 빈은 OrderServiceTest 클래스로 주입될 것이다
    @Bean
    public OrderService orderService() {
      OrderService orderService = new OrderServiceImpl();
      // set 프로퍼티 등등.
      return orderService;
    }
  }

  @Autowired
  private OrderService orderService;

  @Test
  public void testOrderService() {
    // orderService를 테스트한다
  }

}


XML 리소스와 @Configuration 클래스를 섞어서 사용하기
때 로는 테스트의 ApplicationContext를 설정할 때 XML 리소스와 @Configuration 클래스를 섞어서 사용하기를 원할 수 있다. 예를 들어 프로덕션 레벨에서는 XML 설정을 사용하고 테스트의 스프링이 관리하는 특정 컴포넌트를 설정할 때는 @Configuration 클래스를 사용하고자 하거나 그 반대로 사용하는 경우이다. Section 10.3.4.1, “Spring Testing Annotations”에서 얘기했듯이 TestContext 프레임워크는 @ContextConfiguration로 두가지 모두를 선언하게 하지는 않지만 이것이 둘다 사용할 수 없다는 의미는 아니다.

테스트 설정에 XML과 @Configuration 클래스를 사용하려면 진입점으로 하나를 선택하고 진입점이 다른 것을 포함하거나 임포트해야한다. 예를 들어 XML에 컴포넌트 스캔이나 평범한 스프링 빈으로 @Configuration 클래스를 정의해서 @Configuration 클래스들을 포함시킬 수 있다. 반면에, @Configuration 클래스에서는 XML 설정파일을 임포트하기 위해 @ImportResource를 사용할 수 있다. 이 동작은 프로덕션에서 어플리케이션을 구성하는 방법과 의미적으로 동일하다. 프로덕션 설정에서 프로덕션 ApplicationContext가 로딩될 XML 리소스 위치의 세트나 @Configuration 클래스의 세트를 정의할 것이지만 여전히 다른 종류의 설정을 자유롭게 포함하거나 임포트할 수 있다.


컨텍스트 설정의 상속
@ContextConfiguration 은 상속해야 하는 수퍼클래스가 선언한 리소스 위치나 설정 클래스를 나타내는 inheritLocations 불리언 속성을 지원한다. 기본값은 true이다. 즉 어노테이션이 붙은 클래스는 어노테이션이 붙은 수퍼클래스가 선언한 리소스 위치나 설정 클래스를 상속받는다. 특히, 어노테이션이 붙은 테스트 클래스를 위한 리소스 위치나 설정 클래스들은 어노테이션이 붙은 수퍼클래스가 선언한 리소스 위치나 설정클래스의 목록에 추가된다. 그러므로 서브클래스들은 리소스 위치나 설정 클래스의 목록을 확장하는 옵션을 가진다.

@ContextConfiguration 의 inheritLocations 속성을 false로 설정하면 어노테이션이 붙은 클래스의 리소스 위치나 설정 클래스들은 가려지고(shadow) 효과적으로 수퍼클래스가 정의한 리소스 위치나 설정 클래스를 교체한다.

XML 리소스 위치를 사용하는 다음의 예제에서 ExtendedTest의 ApplicationContext는 "base-config.xml"와 "extended-config.xml"에서 그 순서대로 로드될 것이다. 그러므로 "extended-config.xml"에서 정의한 빈은 "base-config.xml"에서 정의한 빈으로 오버라이드(또는 교체) 될 것이다.

@RunWith(SpringJUnit4ClassRunner.class)
// ApplicationContext는 클래스패스 루트의 "/base-config.xml"에서 로드될 것이다.
@ContextConfiguration("/base-config.xml")
public class BaseTest {
  // class body...
}

// ApplicationContext는 클래스 패스 루트의 "/base-config.xml"와 "/extended-config.xml"에서 
// 로드될 것이다
@ContextConfiguration("/extended-config.xml")
public class ExtendedTest extends BaseTest {
  // class body...
}

비 슷하게 설정 클래스를 사용하는 다음 예제는 ExtendedTest의 ApplicationContext는 BaseConfig 와 ExtendedConfig 설정 클래스에서 그 순서대로 로드된다. 그러므로 ExtendedConfig에서 정의한 빈은 BaseConfig에서 정의한 빈으로 오버라이드(교체)된다.

@RunWith(SpringJUnit4ClassRunner.class)
// ApplicationContext는 BaseConfig에서 로드될 것이다
@ContextConfiguration(classes=BaseConfig.class)
public class BaseTest {
  // class body...
}

// ApplicationContext는 BaseConfig와 ExtendedConfig에서 로드될 것이다
@ContextConfiguration(classes=ExtendedConfig.class)
public class ExtendedTest extends BaseTest {
  // class body...
}


환경 프로파일을 사용하는 컨텍스트 설정
스 프링 3.1은 환경과 프로파일(또는 빈 정의 프로파일(bean definition profiles))의 개념에 대해 프레임워크수준의 최고급 지원을 도입했고 통합 테스트는 이제 다양한 테스트 시나리오에 따라 특정 빈 정의 프로파일을 활성화하도록 설정될 수 있다. 이는 테스트 클래스에 새로운 @ActiveProfiles 어노테이션을 붙히고 테스트의 ApplicationContext를 로드할 때 활성화 되어야 하는 프로파일의 목록을 제공함으로써 이룰 수 있다.

Note
@ActiveProfiles는 새로운 SmartContextLoader SPI의 어떤 구현체와도 사용할 수 있지만 @ActiveProfiles는 구식 ContextLoader SPI의 구현체는 지원하지 않는다.

XML 설정과 @Configuration 클래스를 가진 몇가지 예제를 살펴보자.

<!-- app-config.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:jdbc="http://www.springframework.org/schema/jdbc"
  xmlns:jee="http://www.springframework.org/schema/jee"
  xsi:schemaLocation="...">

  <bean id="transferService" class="com.bank.service.internal.DefaultTransferService">
    <constructor-arg ref="accountRepository"/>
    <constructor-arg ref="feePolicy"/>
  </bean>

  <bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository">
    <constructor-arg ref="dataSource"/>
  </bean>

  <bean id="feePolicy" class="com.bank.service.internal.ZeroFeePolicy"/>

  <beans profile="dev">
    <jdbc:embedded-database id="dataSource">
      <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
      <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
    </jdbc:embedded-database>
  </beans>

  <beans profile="production">
    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
  </beans>
</beans>


package com.bank.service;

@RunWith(SpringJUnit4ClassRunner.class)
// ApplicationContext는 "classpath:/app-config.xml"에서 로드된다
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
public class TransferServiceTest {

  @Autowired
  private TransferService transferService;

  @Test
  public void testTransferService() {
    // transferService를 테스트한다
  }
}

TransferServiceTest 를 실행할 때 TransferServiceTest의 ApplicationContext는 클래스패스 루트의 app-config.xml 설정 파일에서 로드될 것이다. app-config.xml를 검사하면 accountRepository 빈이 dataSource 빈에 의존성을 가진다는 것을 알아차릴 것이다. 하지만 dataSource는 최상위 빈으로 정의되지 않았다. 대신, dataSource는 두번 정의됐다. 한번은 production 프로파일이고 한번은 dev 프로파일이다.

TransferServiceTest 에 @ActiveProfiles("dev") 어노테이션을 붙혀서 스프링 TestContext 프레임워크가 현재 프로파일을 {"dev"}로 설정해서 ApplicationContext를 로드하도록 한다. 그 결과 내장된 데이터베이스를 생성하고 accountRepository 빈을 개발 DataSource에 대한 참조와 연결할 것이다. 이것이 통합테스트에서 하려고 하는 것이다.

다음 코드 목록은 XML 대신 @Configuration 클래스를 사용해서 어떻게 같은 설정과 통합테스트를 구현하는지 보여준다.

@Configuration
@Profile("dev")
public class StandaloneDataConfig {

  @Bean
  public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("classpath:com/bank/config/sql/schema.sql")
        .addScript("classpath:com/bank/config/sql/test-data.sql")
        .build();
  }
}


@Configuration
@Profile("production")
public class JndiDataConfig {

  @Bean
  public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
  }
}


@Configuration
public class TransferServiceConfig {

  @Autowired DataSource dataSource;

  @Bean
  public TransferService transferService() {
    return new DefaultTransferService(accountRepository(), feePolicy());
  }

  @Bean
  public AccountRepository accountRepository() {
    return new JdbcAccountRepository(dataSource);
  }

  @Bean
  public FeePolicy feePolicy() {
    return new ZeroFeePolicy();
  }

}


package com.bank.service;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  classes={
    TransferServiceConfig.class,
    StandaloneDataConfig.class,
    JndiDataConfig.class})
@ActiveProfiles("dev")
public class TransferServiceTest {

  @Autowired
  private TransferService transferService;

  @Test
  public void testTransferService() {
    // transferService를 테스트한다
  }
}

이번 변경에서는 XML 설정을 3개의 독립적인 @Configuration 클래스로 분리했다.

TransferServiceConfig: @Autowired를 사용한 의존성 주입으로 dataSource를 얻는다.
StandaloneDataConfig: 개발자 테스트에 적합한 내장 데이터베이스의 dataSource를 정의한다.
JndiDataConfig: 프로덕션 환경의 JNDI에서 획득한 dataSource를 정의한다.

XML 에 기반한 설정 예제처럼 TransferServiceTest에 여전히 @ActiveProfiles("dev") 어노테이션을 붙혔다. 하지만 이번에는 @ContextConfiguration 어노테이션으로 3개의 설정 클래스를 모두 지정했다. 테스트 클래스 자체의 바디는 전혀 변경하지 않고 놔두었다.


컨텍스트 캐싱
TestContext 프레임워크가 테스트에 대한 ApplicationContext를 일단 로드하면 이 컨텍스트는 캐싱될 것이고 같은 테스트슈트(test suite)내에서 유일한(unique) 컨텍스트 설정을 같이 선언한 이어진 모든 테스트에서 재사용할 것이다. 캐싱이 어떻게 동작하는지 이해하려면 유일하다(unique)와 테스트슈트(test suite)의 의미를 이해하는 것이 중요하다.

ApplicationContext 를 어플리케이션 컨텍스트를 로드할 때 사용한 설정 파라미터들의 조합으로 유일하게 구분할 수 있다. 따라서 설정 파라미터들의 유일한 조합을 캐싱된 컨텍스트의 키를 생성하는데 사용한다. TestContext 프레임워크는 컨텍스트 캐시 키를 구성할 때 다음의 설정 파라미터들을 사용한다.

  • locations ( @ContextConfiguration에서)
  • classes ( @ContextConfiguration에서)
  • contextLoader ( @ContextConfiguration에서)
  • activeProfiles ( @ActiveProfiles에서)
예 를 들어, TestClassA가 @ContextConfiguration의 locations (또는 value) 속성에 {"app-config.xml", "test-config.xml"}를 지정했다면 TestContext 프레임워크가 대응되는 ApplicationContext를 로드하고 이러한 위치(location)에 단독으로 기반하는 키 아래있는 static 컨텍스트 캐시에 이 ApplicationContext를 저장한다. 그래서 TestClassB도 위치(location)에 {"app-config.xml", "test-config.xml"}를 정의하고 다른 ContextLoader나 다른 활성화된 프로파일을 정의하지 않았다면 두 테스트 클래스가 같은 ApplicationContext를 공유할 것이다. 즉, 어플리케이션 컨텍스트를 로딩하는 설정 비용은 딱 한번만 발생하고(테스트슈트마다) 이어지는 테스트 실행은 훨씬 더 빨라진다.

테스트 슈트와 포크된(forked) 프로세스
스 프링 TestContext 프레임워크는 static 캐시에 어플리케이션 컨텍스트를 저장한다. 이 말은 컨텍스트가 static 변수에 리터럴하게 저자오딘다는 의미이다. 다시 말하자면 분리된 프로세스에서 테스트를 실행할 때 정적 캐시는 각 테스트 실행간에 비워질 것이고 이는 효율적으로 캐싱 메카니즘을 사용안하도록 한다.

캐싱 메카니즘의 이득을 얻으려면 모든 테스트가 같은 프로세스나 같은 테스트슈트에서 돌아가야 한다. 이는 IDE에서 하나의 그룹으로 모든 테스트를 실행해서 이룰 수 있다. 유사하게 Ant나 Maven같은 빌드 프레임워크로 테스트를 실행할 때 빌드 프레임워크가 테스트들 사이에서 포크(fork)하지 않도록 하는 것이 중요하다. 예를 들어 Maven Surefire 플러그인에서 forkMode를 always나 pertest로 설정하면 TestContext 프레임워크는 테스트 클래스들 사이에서 어플리케이션 컨텍스트를 캐시할 수 없게 되어 빌드 과정이 현저하게 느려질 것이다.

테스트가 어플리케이션 컨텍스트를 망가뜨려서 리로딩을 해야 하는 흔치않은 경우에(예를 들어 빈 정의나 어플리케이션 객체의 상태를 수정해서) 테스트 클래스나 테스트 메서드에 @DirtiesContext 어노테이션을 붙힐 수 있다. (Section 10.3.4.1, “Spring Testing Annotations”에서 @DirtiesContext에 관한 논의를 참고해라.) 이 어노테이션은 스프링이 캐시에서 컨텍스트를 제거하고 다음 테스트를 실행하기 전에 어플리케이션 컨텍스트를 다시 구성하도록 지시한다. @DirtiesContext에 대한 지원은 기본적으로 사용가능한 DirtiesContextTestExecutionListener에서 제공한다.


10.3.5.3 테스트 픽스처의 의존성 주입
기 본적으로 설정되는 DependencyInjectionTestExecutionListener를 사용할 때 테스트 인스턴스의 의존성을 @ContextConfiguration로 설정한 어플리케이션 컨텍스트의 빈에서 주입한다. setter 메서드나 필드에 어떤 어노테이션을 붙히는 지에 따라 setter 주입이나 필드 주입, 또는 둘다를 사용할 것이다. 스프링 2.5와 3.0에서 도입된 어노테이션 지원의 일관성을 위해서 스프링의 @Autowired 어노테이션이나 JSR 330의 @Inject를 사용할 수 있다.

Tip
TestContext 프레임워크는 테스트 인스턴스를 인스턴스하는 방법으로 구성하지 않는다. 그러므로 생성자에 @Autowired나 @Inject를 사용해서 테스트 클래스에는 효과가 없다.

@Autowired 는 타입으로 자동연결하기 때문에 같은 타입의 빈 정의를 여러 개 가지고 있다면 이러한 빈들에는 이 접근을 사용할 수 없다. 이러한 경우에는 @Qualifier와 @Autowired를 함께 사용할 수 있다. 스프링 3.0에서는 @Inject를 @Named와 함께 사용할 수도 있다. 아니면 테스트 클래스가 자신의 ApplicationContext에 접근한다면 (예를 들어) applicationContext.getBean("titleRepository")를 호출해서 명시적으로 검색할 수 있다.

테 스트 인스턴스에 의존성 주입을 하고 싶지 않다면 그냥 필드나 setter 메서드에 @Autowired나 @Inject 어노테이션을 붙히지 말아라. 아니면 명시적으로 클래스를 @TestExecutionListeners로 설정하고 리스너 목록에서 DependencyInjectionTestExecutionListener.class를 생략해서 의존성 주입은 전혀 사용하지 않게 할 수 있다.

목표 섹션에서 간략히 설명했듯이 HibernateTitleRepository 클래스를 테스트하는 시나리오를 생각해 보자. 다음의 두 코드는 필드와 setter 메서드에 @Autowired를 사용하는 방법을 보여준다. 어플리케이션 컨텍스트 설정은 모든 샘플코드 뒤에 나와있다.

Note
다음 코드목록에서 의존성 주입을 JUnit으로 지정하지 않았다. 동일한 DI 기술을 어떤 테스트 프레임워크와도 사용할 수 있다.

다 음의 예제들은 Assert를 앞에 붙히지 않고 assertNotNull()같은 정적 단언문 메서드를 호출한다. 이러한 호출은 예제에는 나와있지 않지만 import static 선언으로 해당 메서드들이 제대로 임포트되었다고 가정한 것이다.

첫 예제코드는 필드 주입으로 @Autowired를 사용한 테스트 클래스의 JUnit 기반 구현체를 보여준다.

@RunWith(SpringJUnit4ClassRunner.class)
// 이 테스트 픽스처를 로드하도록 스프링 설정을 지정한다
@ContextConfiguration("repository-config.xml")
public class HibernateTitleRepositoryTests {

  // 이 인스턴스는 타입으로 
  의존성 주입될 것이다.
  @Autowired    
  private HibernateTitleRepository titleRepository;

  @Test
  public void findById() {
    Title title = titleRepository.findById(new Long(10));
    assertNotNull(title);
  }
}

아니면 아래에서 보듯이 setter 주입으로 @Autowired를 사용하도록 클래스를 설정할 수 있다.

@RunWith(SpringJUnit4ClassRunner.class)
// 이 테스트 픽스처를 로드하도록 스프링 설정을 지정한다
@ContextConfiguration("repository-config.xml")
public class HibernateTitleRepositoryTests {

  // 이 인스턴스는 타입으로 
  의존성 주입될 것이다.
  private HibernateTitleRepository titleRepository;

  @Autowired
  public void setTitleRepository(HibernateTitleRepository titleRepository) {
    this.titleRepository = titleRepository;
  }

  @Test
  public void findById() {
    Title title = titleRepository.findById(new Long(10));
    assertNotNull(title);
  }
}

앞의 코드들은 @ContextConfiguration 어노테이션으로 참조되는 다음과 같은 같은 XML 컨텍스트 파일을 사용한다.(즉 repository-config.xml이다.)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

  <!-- 이 빈은 HibernateTitleRepositoryTests 클래스로 주입된다 -->
  <bean id="titleRepository" class="com.foo.repository.hibernate.HibernateTitleRepository">
    <property name="sessionFactory" ref="sessionFactory"/>
  </bean>
  
  <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
      <!-- 간결함을 위해 설정을 생략했다 -->
  </bean>
</beans>

Note
setter 메서드에 @Autowired를 사용해서 스프링이 제공하는 테스트 기반 클래스를 확장한다면 영향을 받는 어플리케이션 컨텍스트에 정의한 타입의 여러가지 빈을 가질 것이다. 예를 들어 여러 DataSource 빈이다. 이러한 경우 setter 메서드를 오버라이드 할 수 있고 다음과 같이 지정한 대상 빈을 나타내기 위해 @Qualifier 어노테이션을 사용할 수 있지만 수퍼클래스의 오버라이드된 메서드에 위임하도록 해야한다.

// ...

  @Autowired
  @Override
  public void setDataSource(@Qualifier("myDataSource") DataSource dataSource) {
    super.setDataSource(dataSource);
  }

// ...

DataSource 빈을 나타내서 지정한 빈과 일치하는 타입의 세트로 제한한다. 제한자의 값은 대응되는 <bean> 정의내의 <qualifier> 선언과 일치한다. 빈 이름을 제한자 값의 대비책(fallback)으로 사용하므로 효과적으로 이름으로 특정빈을 가르킬 수도 있다.(위에서 보았듯이 "myDataSource"가 빈 id라고 가정한다.)



10.3.5.4 트랜잭션 관리
TestContext 프레임워크에서 트랜잭션은 TransactionalTestExecutionListener가 관리한다. 테스트 클래스에 명시적으로 @TestExecutionListeners를 선언하지 않더라도 TransactionalTestExecutionListener은 기본적으로 설정된다. 하지만 트랜잭션 지원을 활성화하려면 @ContextConfiguration의 의미로 로드하는 어플리케이션 컨텍스트에 PlatformTransactionManager를 제공해야 한다. 추가적으로 테스크 클래스에 클래스 수준 혹은 메서드 수준 @Transactional를 선언해야 한다.

클래스 수준의 트랜잭션 설정은(예시. 트랜잭션 관리자와 기본 롤백 플래그에 빈 이름을 설정) 어노테이션 지원 섹션의 @TransactionConfiguration 부분을 봐라.

트 랜잭션을 테스트 클래스 전체에 적용하지 않으면 메서드에 명시적으로 @Transactional 어노테이션을 붙힐 수 없다. 특정 테스트 메서드는 커밋이 되도록 트랜잭션을 제어하려면 클래스 수준의 기본 롤백 설정을 오버라이드하도록 @Rollback 어노테이션을 사용할 수 있다.

클래스 수준의 트랜잭션 지원을 위해서 AbstractTransactionalJUnit4SpringContextTests 와 AbstractTransactionalTestNGSpringContextTests가 미리 설정된다.

때 때로 트랜잭션이 적용된 컨텍스트 밖에서 트랜잭션이 적용된 테스트 메서드 이전이나 이후에 특정 코드를 실행해야할 필요가 있다. 예를 들면 테스트 실행에 앞서 초기 데이터베이스의 상태를 확인하거나 테스트 실행 이후에 예상한 트랜잭션 커밋 작업(테스트가 트랜잭션을 롤백하도록 설정하지 않았다면)을 확인하는 등이다. TransactionalTestExecutionListener는 정확히 이러한 시나리오를 위해서 @BeforeTransaction와 @AfterTransaction를 지원한다. 테스트 클래스의 어떤 public void 메서드라도 그냥 이러한 어노테이션 중에 하나를 붙히면 TransactionalTestExecutionListener가 절적한 시기에 before transaction method나 after transaction method의 실행을 보장한다.

Tip
모 든 before methods(JUnit의 @Before 어노테이션이 붙은 메서드처럼)와 after methods(JUnit의 @After 어노테이션이 붙은 메서드처럼)는 트랜잭션내에서 실행된다. 추가적으로 @BeforeTransaction나 @AfterTransaction 어노테이션이 붙은 메서드들은 @NotTransactional 어노테이션이 붙은 테스트에는 자연스럽게 실행하지 않는다. 하지만 @NotTransactional는 스프링 3.0부터는 폐기되었다.

다음의 JUnit 기반 예제는 트랜잭션과 관련된 여러 어노테이션을 강조하는 가상의 통합 테스트 시나리오를 보여준다. 더 자세한 내용과 설정 예제는 어노테이션 지원 섹션을 참고해라.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@TransactionConfiguration(transactionManager="txMgr", defaultRollback=false)
@Transactional
public class FictitiousTransactionalTest {

  @BeforeTransaction
  public void verifyInitialDatabaseState() {
    // 트랜잭셩을 시작하기 전에 초기상태를 검증하는 로직
  }

  @Before
  public void setUpTestDataWithinTransaction() {
    // 트랜잭션내에서 테스트 데이터 구성
  }

  @Test
  // 클래스수준의 defaultRollback 설정을 오버라이드한다
  @Rollback(true)
  public void modifyDatabaseWithinTransaction() {
    // 테스트 데이터를 사용하고 데이터베이스의 상태를 수정하는 로직
  }

  @After
  public void tearDownWithinTransaction() {
    // 트랜잭션내의 "tear down" 로직을 실행한다
  }

  @AfterTransaction
  public void verifyFinalDatabaseState() {
    // 트랜잭션이 롤백된 후에 최종 상태를 검증하는 로직
  }
}

ORM 코드를 테스트할 때 false positive 피하기
하 이버네이트 세션의 상태를 조작하는 어플리케이션 코드를 테스트할 때 해당 코드를 실행하는 테스트 메서드내에서 의존하는 세션을 플러시(flush)하도록 해야한다. 기반 세션을 플러시하는 것이 실패하면 false positive (역주: 오류가 아닌데 오류라고 판단하는 경우를 의미한다)가 된다. 테스트는 통과할 것이지만 동일한 코드가 프로덕션 환경에서는 예외를 던질 것이다. 다음의 하이버네이트 기반 예제 테스트 케이스에서 한 메서드는 false positive를 보여주고 다른 메서드는 세션 플러시의 결과를 제대로 노출한다. 이는 내장 메모리(in-memory) 작업의 단위를 유지하는 JPA와 다른 ORM 프레임워크에도 적용된다.


// ...

@Autowired
private SessionFactory sessionFactory;

@Test // 예상되는 예외는 없다!
public void falsePositive() {
  updateEntityInHibernateSession();
  // False positive: 예외가 던져지고 세션을 결국 플러시 될 것이다.
  // (예: 프로덕션 모드에서)
}

@Test(expected = GenericJDBCException.class)
public void updateWithSessionFlush() {
  updateEntityInHibernateSession();
  // 테스트에서 false positive를 피하려면 수동으로 플러시해야 한다
  sessionFactory.getCurrentSession().flush();
}

// ..



10.3.5.5 TestContext 지원 클래스

JUnit 지원 클래스
org.springframework.test.context.junit4 패키지는 JUnit 4.5+ 기반 테스트 케이스를 지원하는 클래스를 제공한다.

  • AbstractJUnit4SpringContextTests: JUnit 4.5+ 환경에서 명백한 ApplicationContext 테스트 지원과 Spring TestContext Framework와 통합된 추상 기반(base) 테스트 클래스

    AbstractJUnit4SpringContextTests를 확장할 때 다음의 protected 인스턴스 변수에 접근할 수 있다.

    • applicationContext: 명시적인 빈 검색을 수행하거나 컨텍스트의 상태를 전체적으로 테스트할 때 이 변수를 사용해라.
  • AbstractTransactionalJUnit4SpringContextTests: JDBC 접근에 다소 편리한 기능도 추가하는 AbstractJUnit4SpringContextTests의 추상 transactional 확장. javax.sql.DataSource 빈과 PlatformTransactionManager이 ApplicationContext에 정의될 것이다. AbstractTransactionalJUnit4SpringContextTests를 확장할 때 다음의 protected 인스턴스 변수에 접근할 수 있다.

    • applicationContext: AbstractJUnit4SpringContextTests 수퍼클래스를 상속받았다. 명시적인 빈 검색을 수행하거나 컨텍스트의 상태를 전체적으로 테스트할 때 이 변수를 사용해라.
    • simpleJdbcTemplate: 디비 쿼리를 하는 SQL 문을 실행할 때 이 변수를 사용해라. 이러한 쿼리는 데이터베이스와 관련된 어플리케이션 코드를 실행하기 이전과 이후 모두에 데이터베이스 상태를 확인하는데 사용할 수 있고 스프링이 이러한 쿼리가 어플리케이션 코드와 같은 트랜잭션의 범위로 실행된다는 것을 보장해준다. ORM 도구와 함께 사용할 때는 false positives를 피해야 한다.
Tip
이 러한 클래스들은 확장하기 쉽다. 스프링에 특화된 클래스 계층(예를 들어 테스트하는 클래스를 직접 확장하고자 한다면)에 묶인 테스트 클래스를 원하지 않는다면 @RunWith(SpringJUnit4ClassRunner.class), @ContextConfiguration, @TestExecutionListeners 등을 사용해서 자신만의 커스텀 테스트 클래스를 설정할 수 있다.


Spring JUnit Runner
Spring TestContext Framework는 커스텀 러너 (JUnit 4.5 ? 4.9에서 테스트되었다)로 JUnit 4.5+와 완전한 통합을 제공한다. 개발자들이 테스트 클래스에 @RunWith(SpringJUnit4ClassRunner.class) 어노테이션을 붙혀서 JUnit에 기반한 유닛테스트와 통합테스트를 구현할 수 있고 동시에 어플리케이션 컨텍스트 로딩, 테스트 인스턴스의 의존성 주입, 테스트 메서드 실행의 트랜잭션 적용 등의 TestContext 프레임워크의 이점을 얻을 수 있다. 다음의 코드 목록은 테스트 클래스를 커스텀 스프링 러너로 실행하도록 설정하기 위한 최소 요구사항을 보여준다. @TestExecutionListeners는 기본 리스너를 사용안하도록 비어있는 리스트로 설정했다.비어있는 리스트로 설정하지 않으면 ApplicationContext를 @ContextConfiguration로 설정해야 한다.

@RunWith(SpringJUnit4ClassRunner.class)
@TestExecutionListeners({})
public class SimpleTest {

  @Test
  public void testMethod() {
    // 테스트 로직 실행...
  }
}


TestNG 지원 클래스
org.springframework.test.context.testng 패키지는 TestNG 기반의 테스트 클래스를 지원하는 클래스를 제공한다.

  • AbstractTestNGSpringContextTests: TestNG 환경에서 명시적인 ApplicationContext 테스트 지원과 함께 Spring TestContext Framework와 통합하는 추상 기반(base) 테스트 클래스.

    AbstractTestNGSpringContextTests를 확장할 때 다음의 protected 인스턴스 변수에 접근할 수 있다.

    • applicationContext: 명시적인 빈 검색을 수행하거나 컨텍스트의 상태를 전체적으로 테스트할 때 이 변수를 사용해라.
  • AbstractTransactionalTestNGSpringContextTests: JDBC 접근을 위한 다소 편리한 기능을 추가한 AbstractTestNGSpringContextTests의 추상 transactional 확장이다. javax.sql.DataSource 빈과 PlatformTransactionManager이 ApplicationContext에 정의될 것이다. AbstractTransactionalTestNGSpringContextTests를 확장할 때 다음의 protected 인스턴스 변수에 접근할 수 있다.

    • applicationContext: AbstractTestNGSpringContextTests 수퍼클래스를 상속받는다. 명시적인 빈 검색을 수행하거나 컨텍스트의 상태를 전체적으로 테스트할 때 이 변수를 사용해라.
    • simpleJdbcTemplate: 데이터베이스 쿼리를 하는 SQL 문을 실행하려면 이 변수를 사용해라. 이러한 쿼리는 데이터베이스와 관련된 어플리케이션 코드의 실행 이전과 이후 모두에 데이터베이스 상태를 확인하는 데 사용할 수 있고 스프링이 이러한 쿼리가 어플리케이션 코드와 같은 트랜잭션의 범위로 실행된다는 것을 보장한다. ORM 도구와 함게 사용할 때는 false positives를 피하도록 해야한다.
Tip
이 러한 클래스들은 확장하기 쉽다. 테스트 클래스가 스프링에 특화된 클래스 계층에 묶이길 원하지 않는다면(예를 들어 테스트하는 클래스를 직접 확장하고자 한다면) @ContextConfiguration, @TestExecutionListeners 등을 사용하거나 수동으로 테스트 클래스를 TestContextManager로 구성해서 자신만의 커스텀 테스트 클래스를 설정할 수 있다. 어떻게 테스트 클래스를 구성하는지에 대한 예제는 AbstractTestNGSpringContextTests의 소스코드를 참고해라.



10.3.6 PetClinic 예제
샘 플 저장소에 있는 PetClinic 어플리케이션은 JUnit 4.5+ 환경에서 Spring TestContext Framework의 여러가지 기능을 설명한다. 대부분의 테스트 기능은 AbstractClinicTests에 포함되어 있고 일부 기능이 다음 목록에 나와있다.

import static org.junit.Assert.assertEquals;
// import ...

@ContextConfiguration
public abstract class AbstractClinicTests extends AbstractTransactionalJUnit4SpringContextTests {

  @Autowired
  protected Clinic clinic;

  @Test
  public void getVets() {
    Collection<Vet> vets = this.clinic.getVets();
    assertEquals("JDBC query must show the same number of vets", super.countRowsInTable("VETS"), vets.size());
    Vet v1 = EntityUtils.getById(vets, Vet.class, 2);
    assertEquals("Leary", v1.getLastName());
    assertEquals(1, v1.getNrOfSpecialties());
    assertEquals("radiology", (v1.getSpecialties().get(0)).getName());
    // ...
  }

  // ...
}

Notes:

  • 이 테스트 케이스는 AbstractTransactionalJUnit4SpringContextTests 클래스를 확장한다. 의존성 주입 (DependencyInjectionTestExecutionListener를 통해서)과 트랜잭션 동작(TransactionalTestExecutionListener를 통해서)을 위해서 설정을 상속받는다.
  • clinic 인스턴스 변수(어플리케이션 객체는 테스트되었다.)는 @Autowired를 통해 의존성 주입으로 설정된다.
  • testGetVets() 메서드는 주어진 테이블의 열(row) 갯수를 쉽게 검증해서 어플리케이션 코드의 올바른 동작을 검증하는 상속받은 countRowsInTable() 메서드를 어떻게 사용할 수 있는지 설명한다. 이를 통해 테스트는 더 강력해지고 정확한 테스트 데이터에 대한 의존성은 줄어든다. 예를 들어 테스트를 깨뜨리지 않고 데이터베이스의 추가적인 열을 추가할 수 있다.
  • 데 이터베이스를 사용하는 수많은 통합 테스트처럼 AbstractClinicTests의 테스트 대부분은 테스트 케이스 실행 이전에 이미 데이터베이스에 있는 데이터의 최소 양에 의존한다. 아니면 테스트 클래스의 테스트 픽스처 구성과정에서 데이터베이스의 존재하는 데이터를 선택할 수 있다.(다시 말하지만 테스트와 같은 트랜잭션에 있다.)
PetClinic 어플리케이션은 JDBC, Hibernate, JPA의 세가지 데이터 접근 기술을 지원한다. 리소스 위치를 지정하지 않고 @ContextConfiguration를 선언함으로써 AbstractClinicTests는 기본 위치인 AbstractClinicTests-context.xml에서 로드한 어플리케이션 컨텍스트를 가질 것이다. AbstractClinicTests-context.xml는 공통 DataSource를 선언한다. 서브클래스들은 PlatformTransactionManager와 Clinic의 구현체를 반드시 선언하는 부가적인 컨텍스트 위치를 지정한다.

예 를 들어 PetClinic 테스트의 하이버네이트 구현체는 다음의 구현체를 가진다. 이 예제에서 HibernateClinicTests는 한 줄의 코드도 가지고 있지 않다. @ContextConfiguration만 선언하고 테스트들은 AbstractClinicTests를 상속받는다. @ContextConfiguration를 어떤 리소스 위치도 지정하지 않고 선언했기 때문에 스프링 TestContext 프레임워크는 AbstractClinicTests-context.xml(예시, 상속받은 위치)와 HibernateClinicTests-context.xml에 정의된 모든 빈에서 어플리케이션 컨텍스트를 로드한다. HibernateClinicTests-context.xml는 AbstractClinicTests-context.xml에 정의한 빈을 오버라이드 할 수 있다.

@ContextConfiguration
public class HibernateClinicTests extends AbstractClinicTests { }

대 규모 어플리케이션에서 스프링 설정은 종종 여러 파일로 나뉜다. 따라서 설정 위치는 모든 어플리케이션에 한정된 통합 테스트의 공통 기반 클래스에 보통 지정한다. 이러한 기반 클래스는 하이버네이트를 사용하는 어플리케이션의 경우 SessionFactory같은 유용한 인스턴스 변수(자연히 의존성 주입으로 존재한다.)를 추가할 수도 있다.

가능한한 통합테스트에서 배포된 환경과 정확히 같은 스프링 설정을 가져야 한다. 다를 수도 있는 부분은 데이터베이스 연결 풀링과 트랜잭션 인프라에 관련된 것들이다. 완전한 어플리케이션 서버에 배포한다면 아마도 어플리케이션 서버의 연결풀(JNDI로 사용가능한)과 JTA 구현체를 사용할 것이다. 그러므로 프로덕션에서는 DataSource와 JtaTransactionManager에 JndiObjectFactoryBean나 <jee:jndi-lookup>를 사용할 것이다. JNDI와 JTA는 통합테스트 컨테이너 밖에서는 사용할 수 없으므로 이를 위해서는 Commons DBCP BasicDataSource와 DataSourceTransactionManager 또는 HibernateTransactionManager 같은 조합을 사용해야 한다. 테스트와 프로덕션 환경사이에 다르지 않은 설정들과 분리한 어플리케이션 서버설정과 '로컬' 설정사이에 선택할 수 있는 하나의 XML 파일에 이 다양한 동작을 넣을 수 있다. 추가적으로 연결 설정에 프로퍼티 파일을 사용하는 것은 바람직하다. 예제는 PetClinic 어플리케이션을 참고해라.


10.4 추가 자료
테스트에 관한 더 자세한 정보는 다음의 자료들을 참고해라.

  • JUnit: “프로그래머 지향 자바 테스트 프레임워크”. 스프링 프레임워크가 테스트 슈트에 사용하고 있다.
  • TestNG: JUnit에서 영감을 받아 자바 5 어노테이션, 테스트 그룹, 데이터 주도 테스트, 분산 테스트등을 추가한 테스트 프레임워크
  • MockObjects.com: 테스트 주도 개발에서 코드의 설계를 향상시키는 기술인 목 객체를 위한 웹 사이트.
  • "목 객체": 위키피디아의 글.
  • EasyMock: “자바의 프록시 메카니즘을 사용해서 인터페이스에 대한 목 객체를 생성해서 제공하는 ”자바 라이브러리. 스프링 프레임워크가 테스트 슈트에 사용하고 있다.
  • JMock: 목 객체로 자바 코드의 테스트 주도 개발을 지원하는 라이브러리
  • Mockito: 테스트 스파이 패턴에 기반한 자바 목 라이브러리
  • DbUnit: 테스트 실행간에 알고 있는 상태를 데이터베이스에 두는(다른 것들도 있지만) 데이터베이스 주도 프로젝트를 대상으로 하는 JUnit 익스텐션(Ant와 메이븐과도 사용할 수 있다.)
  • Grinder: 자바 부하 테스트 프레임워크.

2012/11/04 01:37 2012/11/04 01:37