1. 테스트 컨텍스트 프레임워크의 개념
- 컨텍스트 테스트는 Spring이 지원하는 통합 테스트 방식이다.
- 테스트 컨텍스트 프레임워크는 테스트에 사용되는 애플리케이션 컨텍스트를 생성하고 관리해주는 테스트 프레임워크로, 서버에서와 거의 동일한 구성으로 동작하는 통합 테스트를 쉽게 만들 수 있도록 한다.
- Spring은 JUnit, TestNG 등 여러 테스트 프레임워크를 지원하는데, Spring의 컨텍스트 테스트 엔진은 테스트 프레임워크의 종류에 독립적으로 작성되었다.
- Spring은 테스트에서 사용되는 컨텍스트를 캐싱해서 여러 테스트(메소드, 클래스)에서 해당 컨텍스트를 공유할 수 있도록 해준다. 테스트 메소드의 개수만큼 테스트 클래스의 오브젝트는 반복적으로 만들어지지만 컨텍스트는 반복적으로 만들어지지 않는다. 이로써 실행 시간도 빨라지고 bean 오브젝트의 초기화 작업에 대한 부담도 훨씬 줄어든다. 2개 이상의 컨텍스트도 캐싱해서 사용 가능하다.
※ 테스트를 최대한 고립해서 동작시키기 위해 테스트 메소드마다 테스트 클래스의 오브젝트를 만드는 것이다.
- 대표적으로 TestNG, JUnit3, JUnit4 등이 있다.
2. JUnit4의 적용
1) @RunWith, @ContextConfiguration
- 아래와 같이 테스트 클래스에 @RunWith 어노테이션을 이용해서 JUnit4용 러너를 등록해주고 @ContextConfiguration 어노테이션을 이용해서 테스트에서 사용할 컨텍스트 설정 파일을 지정해주면 된다. xml 설정 파일의 경우에는 locations 속성에 경로를 지정하면 되며, 클래스패스 루트로부터 지정할 때는 /로 시작하면 된다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/test-application.xml") // 여러 개라면 {}로 묶으면 된다.
public class UserDaoTest {...}
다른 테스트 클래스에서 @ContextConfiguration를 사용했더라도 동일한 컨텍스트 설정 파일을 locations으로 지정했다면 하나의 컨텍스트만 생성된다. 단, locations에 여러 설정 파일이 {} 배열로 지정된 경우에는 locations 배열이 동일한 테스트끼리만 공유된다는 사실에 유의해야 한다. 결론적으로, locations가 완전히 일치해야 공유된다.
그리고 @Configuration 설정 클래스의 경우에는 classes 속성에 지정하면 된다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=AppConfig.class) // 여러 개라면 {}로 묶으면 된다.
public class UserDaoTest {...}
xml 설정정보와 클래스 설정정보는 동시에 사용될 수 없는 점에 유의하자. 즉, locations 속성과 classes 속성은 같이 쓰면 안된다.
- @ContextConfiguration에 아무런 정보도 넣지 않고 단순히 @ContextConfiguration만 사용하면 디폴트 설정정보가 있는지 확인하고 이를 사용한다. xml 파일의 경우에는 클래스 이름에 -context.xml이 붙은 파일을 찾고, @Configuration 클래스의 경우에는 테스트 클래스의 static 멤버 클래스들 중에서 @Configuration가 붙은 클래스를 찾는다. 디폴트 설정정보로 사용될 중첩 클래스는 private이거나 final로 선언되면 안된다.
2) 컨텍스트 설정의 상속
- JUnit4 에서는 테스트 클래스를 구성할 때 상속 구조를 활용할 수 있다. 슈퍼 클래스에서 지정한 @ContextConfiguration에 지정한 컨텍스트 파일 정보는 서브 클래스에 상속된다. 아래 예시에서 SubTest에 적용된 컨텍스트 설정 파일은 common-context.xml과 sub-context.xml 두 개다.
@ContextConfiguration("common-context.xml")
public class SuperTest {...}
@ContextConfiguration("sub-context.xml")
public class SubTest extends SuperTest {...}
- 슈퍼 클래스의 컨텍스트 파일 정보를 상속하고 싶지 않다면 서브 클래스의 @ContextConfiguration.inheritLocations 속성값을 false로 두면 된다.
3) 컨텍스트 로더 설정
- 컨텍스트를 로딩하는 방식을 변경하고 싶다면 컨텍스트 로더를 변경하면 된다. 디폴트로는 GenericXmlContextLoader이 적용되어 있다.
- ContextLoader의 구현체를 @ContextConfiguration.loader 속성에 지정해주면 된다.
3. 테스트 코드의 테스트 컨텍스트 활용
- 테스트 클래스 그 자체는 bean이 아니지만 @Autowired, @Resource 등으로 애플리케이션 컨텍스트의 bean을 DI 받을 수 있다.
- 테스트 메소드마다 테스트 클래스의 오브젝트가 만들어지기 때문에, 특정 테스트 메소드에서만 많은 bean을 사용해야 한다면 bean을 클래스의 필드 레벨에 정의해주기보다는 메소드 내에서 ApplicationContext를 DI 받고 직접 getBean() 메소드로 bean을 가져오는 것이 좋다.
- JPA를 사용하는 경우에는 @PersistenceContext와 @PersistenceUnit도 사용 가능하다.
- 테스트가 끝났다면 테스트에 사용되었던 DB, 리소스, bean의 상태는 원래대로 돌려놓도록 하자. 테스트의 독립성은 반드시 보장되어야 한다.
- @DirtiesContext 어노테이션을 클래스 또는 메소드에 추가해줌으로써, 실제 환경에 영향이 가지 않도록 해당 클래스 또는 메소드에는 ApplicationContext 공유를 허용하지 않는 방법도 있다. @DirtiesContext 어노테이션은 일부러 예외적인 상황을 만들거나 설정파일의 DI 구조를 강제로 바꿔가면서 테스트해야 할 때 사용된다.
※ @DirtiesContext의 classMode 속성을 ClassMode.AFTER_EACH_TEST_METHOD으로 바꿔주면 모든 테스트 메소드에 @DirtiesContext를 적용한 것처럼 동작한다.
4. 트랜잭션을 지원하는 테스트
- 테스트에서 트랜잭션을 조작하거나 지원하는 기능이 필요하기도 하다.
1) 테스트의 트랜잭션 지원 필요성
(1) DAO를 단독으로 테스트하는 경우
- 서비스 계층을 거치지 않고 DAO만 테스트하는 경우다. JPA와 같이 트랜잭션을 필요로 하는 기술로 만든 DAO라면 테스트에서 트랜잭션을 지원해줘야 한다.
(2) 롤백 테스트
- 각 테스트의 독립성을 지키기 위해 테스트에 사용했던 DB는 테스트가 끝났을 때 초기 상태로 되돌려놓아야 한다고 언급했다. 이 때 트랜잭션의 롤백을 이용하는 것이 좋다.
2) 트랜잭션 지원 테스트 작성
(1) 트랜잭션 매니저
- 테스트에서도 PlatformTransactionManager 타입의 트랜잭션 매니저 bean을 DI 받아서 사용할 수 있다.
@Autowired PlatformTransactionManager transactionManager;
(2) @Transactional 테스트
- 테스트 오브젝트에는 AOP를 적용할 수 없지만, @Transactional 어노테이션을 사용하면 AOP를 적용한 것과 유사한 방법으로 트랜잭션 기능을 테스트 메소드에 적용할 수 있다.
@Test
@Transactional
public void test() {...}
- @Transactional을 클래스 레벨에 부여하면 해당 클래스 내의 모든 @Test 메소드에 트랜잭션이 적용된다. 메소드 단위에서 트랜잭션 속성을 다르게 적용하고 싶다면 메소드 레벨에 다시 @Transactional을 사용하면 된다.
- @Transactional을 사용하면 기본적으로 transactionManager라는 이름의 트랜잭션 매니저 bean을 가져와서 사용한다.
- @Transactional 테스트에서 트랜잭션이 적용된 서비스 계층의 메소드를 호출하면 트랜잭션 전파 방식의 적용을 받는다.
- @Transactional을 테스트에서 사용하면 테스트가 끝났을 때 자동으로 롤백을 수행해준다. 트랜잭션을 커밋하고 싶다면 다음과 같이 @Rollback 어노테이션을 사용해서 롤백이 적용되지 않도록 설정해주면 된다.
@Test
@Transactional
@Rollback(false)
public void test() {...}
이 때는 롤백을 일으키는 예외가 발생하지 않는 한 커밋된다.
- JUnit의 @After, @Before 메소드는 트랜잭션 안에서 실행된다. 트랜잭션이 시작되기 전이나 트랜잭션이 종료된 후에 해야 할 작업은 각각 org.springframework.test.context.transaction의 @AfterTransaction, @BeforeTransation가 붙은 메소드에서 수행해주면 된다.
- 이 방법 외에도 TransactionTemplate과 TransactionCallback을 이용해 트랜잭션 경계를 설정하고 DB 테스트를 수행하는 방법도 있지만 테스트 코드가 지저분해진다는 단점이 있기 때문에 따로 다루지는 않았다.
(3) ORM 트랜잭션 테스트에서 주의할 점
- ORM은 기본적으로 작업 결과를 바로 DB에 반영하지 않고 영속성 컨텍스트같은 캐시에 변동 사항을 저장해두었다가 트랜잭션이 커밋되거나, JPQL 등을 사용하여 SELECT 쿼리를 직접 사용하거나, 트랜잭션을 강제로 플러시하면 작업 결과를 플러시해서 DB에 반영해준다. 그래서 ORM 트랜잭션 테스트시 DB를 한 번도 거치지 않는 테스트가 만들어지기 쉽다. 이 때는 테스트 모드 내부에서 강제로 플러시를 해줄 필요가 있다.
3) DBUnit
- XML이나 엑셀 파일에 준비해놓은 테스트 데이터를 미리 DB에 저장해둘 수 있도록 도와주는 테스트 라이브러리다.
- SQL를 사용하는 대신 미리 준비해둔 데이터를 사용할 수 있기 때문에 테스트 데이터의 준비가 간편하다.
- 트랜잭션 안에서 DBUnit을 통해 저장된 테스트 데이터는 데스트가 끝나고 나면 롤백된다.
예시
@Test
@Transactional
public void test() {
IDatabaseConnection con = new Database(DataSourceUtils.getConnection(this.dataSource)); // @Transactional에 의해 시작된 트랜잭션이 사용 중인 DB 커넥션을 사용
IDataSet dataset = new FlatXmlDataSet(...); // 생성자 파라미터로 java.io의 File, InputStream, Reader, URL 타입이 들어갈 수 있다.
DatabaseOperation.CLEAN_INSERT.execute(con, dataset);
// 테스트 진행
}
5. @ActiveProfiles
- 활성화할 프로파일들을 지정할 수 있는 테스트용 어노테이션이다. 활성화해서 사용할 프로파일을 애플리케이션 컨텍스트에게 알려주는 것이다.
@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles("dev")
@ContextConfiguration(classes=TestApplicationContext.class)
public class ATest {...}
● 참고 자료 : 토비의 스프링 3.1 Vol.2
'Spring Series > Spring Framework' 카테고리의 다른 글
[Spring] 리모팅과 EJB (0) | 2022.10.25 |
---|---|
[Spring] DelegatingDataSource (0) | 2022.10.23 |
[Spring] Spring의 로드타임 위버 (0) | 2022.10.17 |
[Spring] AspectJ로 bean이 아닌 오브젝트에 DI 적용하기 (0) | 2022.10.13 |
[Spring] AOP 포인트컷 표현식 (0) | 2022.10.12 |
댓글