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

Shiba·2023년 12월 1일
0

🍀 스프링 정리

목록 보기
20/21
post-thumbnail

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

스프링의 POJO와 DI를 이용한 프로그래밍 모델이 가져온 가장 큰 혜택 중 하나는 바로 테스트다. IoC와 DI로 인해, 서버에 배치하지 않고도 스프링 컨테이너만으로 DB까지 참여하는 이상적인 통합 테스트가 가능하다.
6장에서는 스프링이 지원하는 통합 테스트 방식인 컨텍스트 테스트의 특징을 자세히 살펴보고, 컨텍스트 테스트를 최적화된 방식으로 가능하게 해주는 컨텍스트 테스트 프레임워크의 사용 방법을 살펴본다.

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

스프링은 테스트에 사용되는 애플리케이션 컨텍스트를 생성하고 관리하고 테스트에 적용해주는 기능을 가진 테스트 프레임워크를 제공
- 이를 테스트 컨텍스트 프레임워크라고 부른다

테스트 컨텍스트 프레임워크를 사용해 테스트를 만드는 방법과 그 특징에 대해서는 Vol.1에서 설명했다. 여기서는 다양한 설정 방법과 확장 방법, 그리고 테스트를 위해 지원되는 기능을 좀 더 살펴보자.

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

자바에서 가장 많이 사용되는 테스트 프레임워크로는 JUnitTestNG가 있다
여기서는 가장 대표적이며 가장 많이 사용되는 JUnit 4를 기준으로 설명하겠다.

◼ 테스트용 애플리케이션 컨텍스트 캐싱과 설정파일

JUnit4에서는 특정 클래스를 상속하지 않아도 테스트 코드를 작성할 수 있다.
테스트 메소드에 @Test 애노테이션만 붙여주면 하나의 독립적인 테스트가 된다.

문제는 테스트가 독립적이라고 해서 매번 스프링 컨텍스트, 즉 컨테이너를 새로 만드는 건 매우 비효율적인 방법이라는 점이다.
- 빈 오브젝트 초기화 작업에 적지 않은 시간이 소모됨. 이런 초기화 작업이 테스트마다 반복되면 테스트에 많은 부담을 줌.

=> 스프링은 테스트가 사용하는 컨텍스트를 캐싱해서 여러 테스트에서 하나의 컨텍스트를 공유할 수 있는 방법을 제공

동일한 컨텍스트 구성을 갖는 테스트끼리는 같은 컨텍스트를 공유
- 동일한 테스트용 컨텍스트 구성을 갖는다면 테스트가 수천 개라도 하나의 애플리케이션 컨텍스트만 만들어서 사용 가능

테스트에 테스트 컨텍스트 프레임워크를 적용하려면 테스트 클래스에 두 가지 애노테이션을 부여해줘야 한다.

  • @RunWith 애노테이션을 이용해서 JUnit 테스트를 실행하는 러너를 스프링이 제공하는 것으로 변경
    - JUnit의 확장 포인트를 이용한 방법
  • @ContextConfiguration을 통해 컨텍스트 설정파일을 지정
    - 설정파일이 동일하다면 같은 테스트용 컨텍스트를 공유한다. 따라서 새로운 애플리케이션 컨텍스트가 만들어지지 않는다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext")
public class Test1 {
	@Test public void testMethod1() { ... }
    @Test public void testMethod2() { ... }
    ...
}
@RunWith(SpringJUnit4ClassRunner.class)
//Test1과 같은 설정파일을 사용 - Test1에서 만들어진 테스트용 컨텍스트 공유
@ContextConfiguration("/test-applicationContext")
public class Test2 { ... }

@ContextConfiguration에는 여러 개의 XML 설정파일을 지정할 수도 있다.
- {}를 사용해 배열로 만들면 된다
- 설정파일이 여러 개인 경우, 설정파일의 구성이 동일한 테스트끼리만 공유

@RunWith(SpringJUnit4ClassRunner.class)
//Test1,2와 같은 설정파일이 있지만, 다른 설정파일이 없으므로 새로운 컨텍스트 적용 
@ContextConfiguration({"/test-applicationContext", "/subContext.xml"})
public class Test3 { ... }

@ContextConfiguration에서 설정파일 이름을 생략하면 어떻게 될까?

현재 클래스 이름 + '-context.xml'이 붙은 파일이 디폴트 설정파일 이름으로 사용

// 테스트 클래스의 패키지가 com.epril.myproject.test인 경우
@RunWith(SpringJUnit4ClassRunner.class)
// com/epril/myproject/test/Test4-context.xml인 설정파일을 가진 것으로 간주
@ContextConfiguration 
public class Test4 { ... }

일반적으로 디폴트 설정파일 이름을 사용하는 것은 좋은 방법은 아니다.

◼ 컨텍스트 설정의 상속과 컨텍스트 로더

JUnit4의 장점은 테스트 클래스가 특정 클래스를 상속하도록 강제하지 않는다는 것
- 필요하다면 상속구조를 활용할 수도 있다

슈퍼클래스와 서브클래스 모두 @ContextConfiguration을 이용해 컨텍스트 파일을 지정
- 컨텍스트 파일 정보는 상속됨. 따라서 서브 클래스는 슈퍼클래스의 컨텍스트 파일 정보도 포함

@ContextConfiguration("common-context.xml")
public class SuperTest { ... }

@ContextConfiguration("sub-context.xml")
public class SubTest extends SuperTest { ... } //common-context.xml 파일도 포함됨

슈퍼클래스의 컨텍스트 파일 설정을 무시하고 새롭게 정의하고 싶다면 @ContextConfiguration의 inheritLocations를 false로 변경해주면 된다.

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

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

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

테스트 클래스는 테스트 컨텍스트로부터 애플리케이션 컨텍스트와 그에 담긴 빈을 제공받아 테스트 코드에서 사용
- DI를 사용해 애플리케이션 컨텍스트를 받음
- @Autowired, @Resource등을 사용

일반 빈 클래스와 마찬가지로 테스트 클래스에서도 테스트 클래스에 할당된 애플리케이션 컨텍스트를 @Autowired를 사용해 전달받을 수 있다.

@Autowired ApplicationContext context;

애플리케이션 컨텍스트 외에도 모든 컨텍스트 내의 원하는 빈을 제공받을 수 있다

◼ 공유 컨텍스트 사용 시 주의할 점

하나의 컨텍스트를 여러 테스트가 공유할 수 있다는건 독점하고 있는 것이 아니므로 내부 정보나 구성을 함부로 변경해서는 안된다. 가능하면 어떠한 변경도 못하게 하는 것이 좋다.

테스트는 그 실행 순서와 환경에 영향을 받지 않아야 한다.
- 테스트를 순서를 바꿔서 실행하거나 앞의 테스트의 성공유무에 영향을 받아선 안된다.

컨텍스트 내의 빈 오브젝트는 아니지만 외부 리소스를 변경하는 경우도 주의해야 한다.
컨텍스트 내의 빈을 테스트하는 중에 특정 파일을 생성하는 기능을 테스트했다면 테스트가 끝난 후에 파일을 제거해주는 것이 좋다.

그럼에도 어쩔 수 없이 컨텍스트의 빈 오브젝트를 조작하고 수정하는 작업이 꼭 필요하는 테스트가 있을 수 있다.
=> 이런 경우에는 테스트 메소드에 @DirtiesContext 애노테이션을 붙여주면 된다.
- 해당 애노테이션이 붙으면 테스트가 수행된 후, 현재 테스트 컨텍스트를 강제로 제거한다. (더 이상 사용하지 못하게 하기 위함)

@Test
@DirtiesContext
public void test1() { ... }

📖 트랜잭션 지원 테스트

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

테스트에서 DB를 사용하는 기능을 테스트하는 것은 간단함.
하지만 DB를 사용하는 테스트에서는 단순히 컨텍스트에 구성된 빈의 메소드를 호출하는 것으로는 충분하지 않다
- 테스트에서 트랜잭션을 조작하거나 지원하는 기능이 필요한 경우가 있기 때문

테스트에서 트랜잭션 지원이 필요한 이유는 여러가지이지만 대표적으로 다음 두 가지 문제점이 존재한다

◼ DAO 단독 테스트

DAO를 개발한 후에 서비스 계층을 거치지 않고 직접 DAO만 테스트해야 할 때가 있음.
- 스프링의 데이터 액세스 기술로 만든 DAO는 기본적으로 트랜잭션 동기화가 필요
=> 트랜잭션을 시작해주는 AOP가 있는 서비스계층을 통해 접근하지 않으면 DAO가 실행이 안됨

◼ 롤백 테스트

테스트는 어떤 순서로 실행되든 그 결과가 항상 일정해야 하고 외부 리소스나 환경에 영향을 받아서는 안됨.
- 테스트에서 DB를 수정하는 경우, 트랜잭션 롤백을 통해 모든 정보를 롤백시키는 롤백 테스트를 수행해야 한다.

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

◼ 트랜잭션 매니저

스프링의 모든 트랜잭션은 트랜잭션 매니저를 이용해 생성, 관리
- 트랜잭션 매니저를 이용할 수 있다면 트랜잭션 제어도 가능

테스트 클래스에서 트랜잭션 매니저 빈을 DI 받기

@Autowired PlatformTransactionManager transactionManager;

테스트에서는 TransactionTemplate와 TransactionCallback을 이용해 트랜잭션 경계를 설정한 후에 DB를 사용하는 빈을 호출해서 테스트를 진행

@Autowired

@Test
public void txTest() {
	new TransactionTemplate(transactionManager).execute(
    	new TransactionCallback<Object>() {
        	public Object doInTransaction(TransactionStatus status) {
				//예외가 발생하지 않아도 트랜잭션이 끝날 때 트랜잭션이 롤백되도록 설정
				status.setRollbackOnly(); 
            	
                // execute() 메소드에 의해 시작된 트랜잭션 안에서 모든 DB 작업이 진행
            	dao.deleteAll();
                dao.add(new Member(10, "Spring", 7.8));
            	assertThat(dao.count(), is(1));
                
            	return null;  }});
}

setRollbackOnly() 메소드를 실행하면 해당 트랜잭션은 무조건 롤백된다.
- 강제로 롤백을 시켜서 롤백 테스트를 만드는 경우 유용함

트랜잭션의 속성을 디폴트와 다르게 하려면 트랜잭션 템플릿을 만들 때 트랜잭션 매니저와 함께 TransactionDefinition 타입 오브젝트를 생성자에 전달해주면 된다.

◼ @Transactional 테스트

테스트에서 트랜잭션 매니저를 DI 받아 트랜잭션 템플릿과 함께 사용하는 방법은 테스트 코드가 지저분해진다는 단점이 있다.
- 테스트에서는 AOP를 적용하는 것이 불가능함.
-- 스프링 AOP의 적용 대상은 컨텍스트에 등록된 빈 오브젝트 뿐이기 때문

스프링의 테스트 컨텍스트 프레임워크는 마치 AOP를 적용한 것과 유사한 방식으로 트랜잭션 기능을 테스트 메소드에 적용할 수 있게 해준다.
- @Transactional 애노테이션을 메소드에 부여해주면 된다.

@Test
@Transactional
public void txTest() {
	dao.deleteAll();
    dao.add(new Member(10,"Spring", 7.8));
    assertThat(dao.count(), is(1));
}

서비스 계층에서 사용한 @Transactional과 다른 점은 강제롤백 옵션이 설정된 트랜잭션으로 만들어진다는 점이다. (@Transactional == setRollbackOnly())

트랜잭션을 일부러 커밋하고 싶은 경우, @Rollback 애노테이션을 이용해 롤백이 적용되지 않도록 만들어주면 예외가 발생하지 않는다면 커밋된다.

@Test
@Transactional
@Rollback(false)
public void tsTest() { ... }

@Before나 @After 메소드 역시 트랜잭션 안에서 진행되는데, 트랜잭션이 시작되기 전이나 완전히 종료된 후 해야 할 작업이 있는 경우가 있을 수도 있다
- 스프링에서 제공하는 @BeforeTransaction과 @AfterTransaction이 붙은 메소드를 사용

//위에서부터 순차적으로 메소드가 실행됨
@BeforeTransaction
public setUpBeforeTx() { ... }

@Before
public void setUpInTx() { ... }

@Test
@Transactional
public void test() { ... }

@After
public void tearDownInTx() { ... }

@AfterTransaction
public void tearDownAfterTx() { ... }

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

하이버네이트나 JPA를 사용하는 롤백 테스트를 만들 때는 주의할 점이 있다.
ORM은 모든 작업결과를 바로 DB에 반영하지 않고, 필요한 시점에 DB에 반영한다.

이 때문에 잘못된 테스트가 작성될 수 있다

//user 엔티티를 저장해서 비교하는 테스트
@Test
@Transactional
public void multiAdd() {
	User user = new User(...);
    hibernateDao.add(user);
    assertThat(user, is(hibernateDao.get(user.getId())));
}
// 바로 DB에 반영하지 않아서 DB에 아무것도 전달되지 않은 채로 테스트가 종료됨

ORM은 캐시에 INSERT문을 보관해두다가 get() 메소드를 실행하면 캐시에서 엔티티를 찾는다. 이 상태에서 테스트가 종료되면 스프링은 트랜잭션을 롤백시켜버린다.
결국 한 번도 SQL이 만들어져 DB에 전달되지 않고 테스트가 종료되는 것이다.
=> 강제로 flush() 메소드를 호출하는 방법을 사용해야 한다.

User user = new User(...);
hibernateDao.add(user);
SessionFactory.getCurrentSession().flush();

assertThat(user, is(hibernateDao.get(user.getId())));

DAO를 직접 테스트하지 않고 서비스 계층 오브젝트를 거치는 경우에도 이런 문제를 염두에 둬야 한다.

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

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

여기서는 스프링의 @Transactional 테스트에 DBUnit을 함께 사용하는 방법을 설명하겠다

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

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

@Autowired DataSource dataSource;

@Test
@Transactional
public void userQuery() {
	//현재 트랜잭션이 사용하는 DB커넥션을 가져와 DBUnit용으로 변경
	IDatabaseConnection connection =
    	new Database(DataSourceUtils.getConnection(this.dataSource));
    
    // XML로 만들어진 테스트 데이터 파일을 지정. 엑셀, CSV로도 만들 수 있다
    IDataSet dataset = new FlatXmlDataSet(...);
    
    //테스트 데이터 삽입. 테스트가 끝나면 모두 롤백
    DatabaseOperation.CLEAN_INSERT.execute(connection, dataset);
    
    //테스트 데이터를 이용해 테스트 진행
    List<User> users = dao.findUsersByCondition(...);
    assertThat(...);
}

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

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

◼ @Configuration 클래스 테스트

스프링 3.0에서는 XML만 사용 가능
스프링 3.1에서는 XML대신 @Configuration 클래스도 사용 가능

다음과 같이 Appconfig 클래스에 테스트에 이용한 컨텍스트 정보가 저장되어 있을 때,

@Configuration
@EnableTransactionManagement
@ComponentScan("myproject")
public class AppConfig {
	...
}

스프링 3.0에서는 XML을 거쳐서 사용해야 했지만, 스프링 3.1이라면 @ContextConfiguration을 이용해 지정해서 사용이 가능하다

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=AppConfig.class)
@Transactional
public class DaoTest {
	@Autowired UserDao userDao;
    ...
    @Test
    public void userDaoTest() {
    	//
    }
}

XML과 @Configuration 클래스를 동시에 사용할 수 없다. 동시에 지정하면 에러 발생

◼ @ActiveProfile

테스트는 서버가 아니라 독립 실행 환경으로 동작하므로 표준 환경 오브젝트가 지원하는 환경변수와 시스템 프로퍼티를 이용해 활성 프로파일을 지정할 수 있을 것이다.

하지만 환경변수나 시스템 프로퍼티 설정은 번거로움
- 테스트를 실행하는 별도의 스크립트를 따로 만들지 않는다면 환경이 바뀔 때마다 테스트용 활성 프로파일 설정을 다시 해줘야 한다.
- 각기 다른 활성 프로파일을 적용하고 싶을 때, 일괄 적용할 수 있는 방법이 없다.

스프링 3.1은 간편하게 테스트용 활성 프로파일을 지정할 수 있는 @ActiveProfiles를 제공

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=AppConfig.class)
@ActiveProfiles("dev")
public class MyAppTest {
profile
모르는 것 정리하기

0개의 댓글