토비의 스프링 Vol.2 - 6장 테스트 컨텍스트 프레임워크

2020-11-04

테스트 컨텍스트 프레임워크

스프링의 POJO와 DI를 이용한 프로그래밍 모델이 가져온 가장 큰 혜택 중 하나는 바로 테스트다. POJO 프로그래밍 모델은 단위 테스트를 손쉽게 작성할 수 있는 환경을 제공해주고, 기술에 종속적이지 않은 코드를 작성하게 해준다. IoC와 DI로 인해, 서버에 배치하지 않고도 스프링 컨테이너만으로 DB까지 참여하는 이상적인 통합테스트가 가능해진다.


테스트 컨텍스트 프레임워크

스프링은 테스트에 사용되는 애플리케이션 컨텍스트를 생성하고 관리하고 테스트에 적용해주는 기능을 가진 테스트 프레임워크를 제공한다. 이를 테스트 컨텍스트 프레임워크라고 부른다. 테스트 컨텍스트 프레임워크는 스프링의 IoC/DI를 지원하는 애플리케이션 컨텍스트를 테스트에서 효과적으로 사용하게 해준다. 테스트 컨텍스트 프레임워크를 이용하면, 서버에서와 거의 동일한 구성으로 동작하는 통합 테스트를 손쉽게 만들 수 있다.

테스트 프레임워크와 컨텍스트 테스트

자바에서 가장 많이 사용되는 테스트 프레임워크로는 JUnit과 TestNG가 있다. JUnit은 기존의 JUnit 3와 애노테이션 방식으로 새롭게 만들어진 JUnit 4로 다시 구분할 수 있다. 스프링 컨텍스트 테스트는 이 세 가지 테스트 프레임워크를 모두 지원한다.

테스트용 애플리케이션 컨텍스트 캐싱과 설정파일
  • JUnit 4에서는 특정 클래스를 상속하지 않아도 테스트 코드를 작성할 수 있다. 테스트 메소드에 @Test라는 애노테이션만 붙여주면 메소드가 속한 클래스는 테스트 클래스가 되며, @Test가 붙은 모든 메소드가 각각 하나의 독립적인 테스트가 된다.
  • 테스트가 독립적이라고 해서 매번 스프링 컨텍스트, 즉 컨테이너를 새로 만드는 건 매우 비효율적인 방법이기에, 스프링은 테스트가 사용하는 컨텍스트를 캐싱해서 여러 테스트에서 하나의 컨텍스트를 공유할 수 있는 방법을 제공한다. 동일한 컨텍스트 구성을 갖는 테스트끼리는 같은 컨텍스트를 공유하는 것이다.
  • 테스트 컨텍스트 프레임워크가 적용된 테스트 예
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration("/test-applicationContext")
    public class Test1 {
    @Test public void testMethod1() {...}
    @Test public void testMethod2() {...}
    }
    
    컨텍스트 설정의 상속과 컨텍스트 로더
  • JUnit 4의 장점은 테스트 클래스가 특정 클래스를 상속하도록 강제하지 않는다는 것이다. 따라서 테스트 클래스를 구성할 때 필요하면 상속구조를 활용할 수도 있다. 슈퍼클래스와 서브클래스에서 모두 @ContextConfiguration을 이용해 컨텍스트 파일을 지정했다면 컨텍스트 파일 정보는 상속된다. 따라서 서브클래스의 컨텍스트 파일 정보는 슈퍼클래스에서 정의된 것까지 포함된다. 슈퍼클래스의 컨텍스트 파일 설정을 무시하고 새롭게 정의하고 싶다면 @ContextConfiguration의 inheritLocations를 false로 바꿔주면 된다.
  • 테스트 컨텍스트를 로딩하는 방식을 변경하려면 ContextLoader 인터페이스를 구현한 클래스를 만들고 이를 @ContextConfiguration의 loader 엘리먼트에 지정해주면 된다.

    테스트 코드의 테스트 컨텍스트 활용

    ‘테스트 컨텍스트 프레임워크’의 ‘컨텍스트’는 애플리케이션 컨텍스트가 아니다. 테스트에서 사용되는 애플리케이션 컨텍스트를 생성하고 관리해주는 오브젝트를 가리키는 용어다.

    테스트 컨텍스트로부터 DI 받기

    테스트 클래스는 테스트 컨텍스트로부터 애플리케이션 컨텍스트와 그에 담긴 빈을 제공받아 테스트 코드에서 사용한다. 애플리케이션 컨텍스트를 제공받는 방법은 스프링답게 DI를 사용한다. @Autowired ApplicationContext context; 애플리케이션 컨텍스트 외에도 모든 컨텍스트 내의 원하는 빈을 제공받을 수 있다.

    공유 컨텍스트 사용 시 주의할 점
  • 캐싱 기법을 통해 하나의 컨텍스트를 여러 테스트가 공유할 수 있다는 건 테스트 컨텍스트 프레임워크의 장점이지만, 단점도 있다. 컨텍스트를 공유하는 테스트 메소드는 컨텍스트가 자신이 독점하는 것이 아니므로 그 구성이나 내부 정보를 함부로 변경해서는 안 된다.
  • 테스트는 모두 고립돼서 동작해야 하고 테스트 결과도 자신이 검증하는 코드가 바뀌지 않는 한 항상 일정해야 한다.

    @DirtiesContext 애노테이션이 붙은 테스트가 수행되고 나면 스프링은 현 테스트 컨텍스트를 강제로 제거한다. 이후에 설정파일이 같은 컨텍스트를 사용하는 테스트가 진행된다면, 이때는 새로운 컨텍스트가 만들어진다. 따라서 다른 테스트에는 영향을 주지 않고 안전하게 컨텍스트를 조작하는 테스트를 만들 수 있다.


트랜잭션 지원 테스트

테스트의 트랜잭션 지원 필요성

테스트에서 트랜잭션 지원이 필요한 이유

  1. DAO 단독 테스트
    • DAO를 개발한 후에 서비스 계층을 거치지 않고 직접 DAO만 테스트해야 할 때, 스프링의 데이터 액세스 기술로 만든 DAO는 기본적으로 트랜잭션 동기화를 필요로 한다.
  2. 롤백 테스트
    • 테스트에서 진행되는 모든 DB작업을 하나의 트랜잭션으로 묶어서 진행하고, 테스트를 마칠 때 트랜잭션을 모두 롤백시키는 롤백 테스트를 진행할 때

      트랜잭션 지원 테스트 작성 방법

      트랜잭션 매니저

      스프링의 모든 트랜잭션은 트랜잭션 매니저를 이용해 만들어지고 관리된다. 따라서 트랜잭션 매니저를 이용할 수 있으면 트랜잭션도 제어할 수 있다.

      @Transactional 테스트

      테스트에서 트랜잭션 매니저를 DI 받아서 트랜잭션 템플릿과 함께 사용하는 방법은 DB를 이용하는 테스트의 요구조건을 모두 만족하긴 하지만, 테스트 코드가 지저분해진다는 단점이 있다. 템플릿/콜백 방식에서 나타나는 장황한 코드로 인해 테스트 코드를 이해하기도 조금 불편해진다. 그래서 스프링의 테스트 컨텍스트 프레임워크는 마치 AOP를 적용한 것과 유사항 방식으로 트랜잭션 기능을 테스트 메소드에 적용할 수 있게 해준다. 트랜잭션을 적용하고 싶은 메소드가 있으면 다음과 같이 @Transactioanl 애노테이션을 메소드에 부여해주면 된다.

      ORM 롤백 트랜잭션 테스트의 주의사항

      하이버네이트나 JPA를 사용하는 롤백 테스트를 만들 때는 주의할 점이 있다. ORM은 기본적으로 모든 작업 결과를 바로 DB에 반영하지 않는다. 대신 가능한 한 오랫동안 메모리에 변경사항을 저장하고 있다가 꼭 필요한 시점에서 DB에 반영한다. 최적화를 위한 트랜잭션 내의 캐싱 기법이라고 볼 수 있다. ORM의 엔티티 오브젝트를 이용한 작업을 SQL로 만들어 DB로 보내는 것을 플러시: flush라고 한다. 기본적으로 ORM은 자동플러시 모드로 동작한다. 자동플러시 모드에서는 트랜잭션이 커밋되거나, 캐시에 저장해둔 정보가 반영되는 SELECT 쿼리를 실행해야 하거나, 코드에서 flush() 메소드를 실행해서 강제로 플러시하도록 만들 때만 트랜잭션 내의 캐시에 저장해뒀던 ORM 작업 결과를 SQL로 만들어 DB에서 실행시킨다. flush()와 내부 캐시를 강제로 비우도록 clear() 메소드를 잘 사용해야 한다.

      트랜잭션 지원 테스트에 DBUnit 이용하기

      DBUnit은 DB가 사용되는 테스트를 만들 때 유용하게 쓸 수 있는 지원 라이브러리다. XML이나 엑셀 파일에 준비해둔 테스트 데이터를 DBUnit의 명령을 이용해 DB에 삽입해주는 것이다.

DBUnit을 통해 테스트 데이터를 주입하려면 먼저 DB 커넥션을 준비한 후에 IDatabaseConnection 타입 오브젝트로 만들어줘야 한다. 스프링의 트랜잭션 지원 테스트에서는 트랜잭션 매니저를 통해 내부적으로 DB 커넥션과 트랜잭션을 만들기 때문에 트랜잭션에 DBUnit을 참여시키려면 현재 트랜잭션이 사용하는 DB 커넥션을 가져와야 한다.

현재 진행 중인 트랜잭션이 사용하는 DB 커넥션을 가져올 때는 DataSourceUtils의 getConnection() 메소드를 사용하면 된다. 먼저 DB 커넥션을 가져올 DataSource를 DI 받아 두고 getConnection() 메소드를 호출하면, 현재 진행 중인 트랜잭션이 사용하는 DB 커넥션을 돌려준다. 이를 사용해서 DBUnit의 IDatabaseConnection을 만들고 DBUnit의 테스트 데이터 등록 API를 사용하면, 테스트 코드와 같은 트랜잭션 안에 테스트 데이터를 삽입할 수 있다. 이렇게 준비된 데이터를 사용해서 테스트가 동작하고 트랜잭션이 끝나면, DBUnit의 테스트 데이터 등록 작업을 포함해서 모든 DB 작업이 롤백될 것이다.


스프링 3.1의 컨텍스트 테스트 프레임워크

자바 코드 설정정보와 프로파일 활용

@Configuration 클래스 테스트
  • 스프링 3.0의 컨텍스트 테스트 프레임워크는 테스트에서 사용할 애플리케이션 컨텍스트의 설정정보로 XML만 사용할 수 있었지만, 스프링 3.1에서는 XML 대신 @Configuration 클래스도 사용할 수 있다.
  • 스프링 3.0에서라면 XML을 거치지 않으면 AppConfig 설정을 이용한 테스트를 할 수 없었다. 하지만 스프링 3.1이라면 @ContextConfiguration의 classes 엘리먼트를 이용해 @Configuration 클래스를 바로 지정해서 사용할 수 있다.
  • @Configuration 클래스의 경우는 테스트 클래스의 스태틱 멤버 클래스 중에서 @Configuration이 붙은 것을 모두 디폴트 설정정보로 사용한다.
  • 중첩 스태틱 클래스로 정의하는 디폴트 설정 클래스들은 private이거나 final로 선언되면 안 된다. 중첩 스태틱이면서 final이 아니고, private도 아닌 클래스만 적용된다.
  • XML에도 디폴트 파일이 있고 @Configuration 클래스도 디폴트로 사용 가능한 클래스가 동시에 정의되어 있다면 에러가 난다.
    @ActiveProfile

    테스트는 서버가 아니라 독립 실행환경으로 동작하므로 표준 환경 오브젝트가 지원하는 환경변수와 시스템 프로퍼티를 이용해 활성 프로파일을 지정할 수 있다. 하지만 환경변수나 시스템 프로퍼티 설정은 번거롭다. 테스트를 실행하는 별도의 스크립트로 따로 만들지 않는다면 환경이 바뀔 때마다 테스트용 활성 프로파일 설정을 다시 해줘야 한다. 또, 한 번에 실행하는 테스트 클래스가 여러 개인데 각기 다른 활성 프로파일을 적용하고 싶다면 환경변수나 시스템 프로퍼티로 일괄 적용하는 방법을 사용할 수 없다.

    그래서 스프링 3.1은 간편하게 테스트용 활성 프로파일을 지정할 수 있는 @ActiveProfiles를 제공한다. 다음 테스트 클래스는 설정정보 중에서 dev 프로파일만 활성화해서 테스트를 수행한다.

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes=AppConfig.class)
    @ActiveProfiles("dev")
    public class MyAppTest {
    

정리

  • 테스트 컨텍스트의 도움을 받으면 테스트가 애플리케이션 컨텍스트를 공유할 수 있어서 테스트의 성능을 극대화 할 수 있다. 단, 다른 테스트에 영향을 주지 않도록 주의해야 한다.
  • 테스트 컨텍스트 프레임워크를 사용하면 컨텍스트 캐싱을 통해 테스트 성능을 향상시킬 수 있다.
  • 테스트 컨텍스트 정보는 상속도 가능하다.
  • 테스트 오브젝트는 빈은 아니지만 테스트 컨텍스트의 도움으로 애플리케이션 컨텍스트로부터 테스트에 필요한 빈을 주입받을 수 있다. 주요 DI용 애노테이션을 활용할 수 있다.
  • DAO를 단독으로 테스트하거나 테스트 작업이 다른 테스트에 영향을 주지 않기 위해서는 롤백 테스트로 만들어야 한다.
  • @Transactional은 테스트에서는 롤백 테스트를 만들 때 사용한다.
  • ORM에 대한 테스트는 ORM의 캐시가 동작하는 특징을 잘 이해하고 사용해야 한다.
  • DBUnit을 트랜잭션 테스트에 활용하면 테스트 데이터를 손쉽게 등록할 수 있다.

출처 : https://github.com/Masssidev/toby-vol2