2022. 6. 30. 23:22ㆍ토비의 스터디
기존의 테스트코드에서 @Before 메소드가 테스트 메소드 개수만큼 반복된다.
@Before
public void setUp(){
Application Context ac = new GenericXmlApplicationContext("applicationContext.xml");
this.dao = ac.getBean("userDao", UserDao.class);
}
현재 @Before 메소드는 애플리케이션 컨텍스트를 생성하는 코드를 수행하는데, 애플리케이션 컨텍스트는 생성될 때 모든 싱글톤 빈 오브젝트를 초기화한다.
어떤 빈은 오브젝트가 생성될 때 자체적인 초기화 작업을 진행하는 경우도 있다. 이 경우 빈 오브젝트 초기화에 많은 시간이 할애된다.
또, 애플리케이션 컨텍스트가 초기화될 때 어떤 빈은 독자적으로 많은 리소스를 할당하거나, 독립적인 스레드를 띄우기도 한다.
이 경우 매 테스트를 마치고, 애플리케이션 컨텍스트 내의 빈에 할당한 리소스를 정리하지 않는다면 다음 테스트에서 생성한 애플리케이션 컨텍스트가 만들어지는 과정에서 문제를 야기한다.
이전 장들에서 설명했듯 각 테스트는 일관성있는 결과를 보장해야 하며 독립적이어야 한다.
JUnit은 매번 테스트를 진행할 때마다 테스트 클래스의 오브젝트를 생성한다는 것을 확인했다.
따라서 여러 테스트가 함께 참조하는 애플리케이션 컨텍스트를 스ㅐ틱 필드에 저장하면 편리하게 사용할 수 있다.
2.4.1 테스트를 위한 애플리케이션 컨텍스트
스프링 테스트 컨텍스트 프레임워크 적용
테스트 컨텍스트의 지원을 받으면 애노테이션 설정으로 애플리케이션 컨텍스트를 생성하여 모든 테스트가 공유할 수 있다.
@Before 메소드에서 애플리케이션 컨텍스트를 생성하였는데, 이를 제거하고 ApplicationContext 타입의 변수를 인스턴스 레벨에서 선언한다. 이후 @Autowired로 주입받는다.
@Runwith(SpringJUnit4ClassRunner.class) // 스프링의 테스트 컨텍스트
//프레임워크의 JUnit 확장기능 지정
@ContextConfiguration(locations="/applicationContext.xml")
//테스트 컨텍스트가 자동으로 만들어줄 애플리케이션 컨텍스트의 위치 지정
public class UserDaoTest{
@Autowired
private ApplicationContext ac;
...
@Before
public void setUp(){
this.dao = ac.getBean("userDao", UserDao.class);
...
}
}
현재 인스턴스 변수인 ac는 초기화해주는 코드가 없어도 NullPointerException이 발생하지 않는다. ac 변수에는 애플리케이션 컨텍스트가 들어있기 때문이다.
그것이 가능한 이유는 어노테이션에서 찾을 수 있다.
@RunWith는 JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용한다.
그 중 SpringJUnit4ClassRunner라는 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면 JUnit은 테스트가 사용할 애플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해준다.
테스트 메소드의 컨텍스트 공유
setUp() 메소드를 다음과 같이 변경하고 실행해보자.
@Before
public void setUp(){
System.out.println(this.ac);
System.out.println(this);
}
출력된 ac와 this의 값을 살펴보면,
ac는 모두 동일하다. ==> 하나의 애플리케이션 컨텍스트가 만들어지고 모든 테스트 메소드에서 사용되고 있다.
반면 UserDaoTest(위 코드에서 this)의 오브젝트는 매번 주소 값이 다르다. ==> JUnit은 테스트 메소드를 실행할 때마다 새로운 테스트 오브젝트를 만들기 때문이다.
이제 스프링이 애플리케이션 컨텍스트를 테스트 개수에 상관없이 한 번만 만들기 때문에 테스트 수행 속도는 빨라진다.
@AutoWired
@AutoWired는 스프링의 DI에 사용되는 애노테이션이다. @AutoWired가 붙은 인스턴스 변수가 있으면, 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다. 이후 타입이 일치하는 빈이 있으면 인스턴스 변수에 주입한다.
참고로 스프링 애플리케이션 컨텍스트는 초기화할 때 자기 자신도 빈으로 등록한다. 따라서 애플리케이션 컨텍스트에는 ApplicationContext 타입의 빈이 존재하며 당연히 DI도 가능하다.
@AutoWired는 생성자, 수정자 등의 메소드 없이 주입이 가능하다. 이런 방법을 자동 와이어링이라고 한다.
단, @AutoWired는 타입을 기준으로 어떤 빈을 가져올지 결정한다. 만약 DataSource를 주입받으려고 하는데, DataSource 타입의 빈이 두개 이상 등록되어 있다면 문제가 발생한다. 만약 타입이 중복되는 경우에는 이름으로 빈을 검색한다.
예를들어
@Autowired
DataSource dataSource;
이 경우 DataSource 타입으로 등록된 빈이 두개 이상인 경우, DataSource라는 이름을 가진 빈을 주입한다.
2.4.2 DI와 테스트
UserDao와 커넥션을 생성하는 클래스 사이에 DataSource라는 인터페이스를 두었다. 그래서 UserDao는 자신이 사용할 구체적인 클래스가 무엇인지 알 필요가 없다. 또한 DI를 통해 외부에서 오브젝트를 주입받으므로 변경에 유리하다.
하지만 "DataSource의 구현클래스를 절대로 바꾸지 않을것인데도 굳이 DataSource 인터페이스를 사용하고 DI를 통해 주입해주는 방식을 이용해야 하는 것인가?" 하고 반문할 수 있다.
이에 대답은 "인터페이스를 두고 DI 방식을 사용해야 한다" 라고 할 수 있다.
그 이유는
1) 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없다.
2) 클래스 구현 방식은 바뀌지 않더라도 인터페이스를 두고 DI를 적용하는 것은 다른 차원의 서비스 기능을 도입할 수 있다. 부가기능 추가가 자연스러워진다.
3) 테스트를 손쉽게 만들 수 있다.
다음으로 3번 테스트에 DI를 이용하는 방법을 몇 가지 살펴보자.
테스트 코드에 의한 DI
애플리케이션이 사용하는 applicationContext.xml에 정의된 DataSource 빈은 서버의 DB 풀 서비스와 연결해서 운영용 DB 커넥션을 돌려주도록 만들어져 있다고 해보자.
테스트 코드에서 이 DataSource를 이용해서는 안 될 것이다. 운영용 DB가 위협받는다.
이 경우 테스트 테스트용 DB에 연결해주는 DataSource를 테스트 내에서 직접 만들어서 사용하면 된다.
아래 예제에서는 스프링에 제공하는 DataSource인 SingleConnectionDataSource를 사용한다. DB 커넥션을 하나만 만들어두고 사용하므로 매우 빠르다.
@DirtiesContext
public class UserDaoTest {
@Autowired
UserDao dao;
@Before
public void setUp() {
// 테스트에서 UserDao가 사용할 DataSource를 직접 생성
DataSource dataSource = new SingleConnectionDataSource(
"jdbc:mysql://localhost/testdb", "spiring", "book", true);
// 코드에 의한 수동 DI
dao.setDataSource(dataSource);
...
}
이 방법을 통해 XML 설정파일 수정 없이 오브젝트 관계를 재구성 할 수 있다.
하지만 applicationContext.xml 파일의 설정 정보를 따라 구성한 오브젝트를 가져온 다음, 의존 관계를 강제로 변경했다.
애플리케이션 컨텍스트는 테스트 중 한 개만 만들어지고 모든 테스트에서 공유해서 사용한다. 따라서 애플리케이션 컨텍스트의 구성이나 상태를 테스트 내에서 변경하지 않는 것이 원칙이다.
하지만 위의 테스트는 UserDao 빈의 의존관계를 강제로 변경한다.
따라서 @DirtiesContext라는 애노테이션을 추가했다. 이 애노테이션이 붙은 테스트 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않는다. 즉, 테스트 메소드를 수행하고 나면 매번 새로운 애플리케이션 컨텍스트를 만들어서 다음 테스트가 사용하게 해준다.
테스트를 위한 별도의 DI 설정
방금처럼 테스트에서 수동 DI를 하면, 애플리케이션 컨텍스트도 매번 새로 만들어야 하는 등 번거롭다.
테스트에서 사용할 DataSource 클래스가 빈으로 정의된 테스트 전용 설정파일을 따로 만드는 방법을 소개한다.
xml 설정파일을 한개 더 생성한 다음 하나에는 운영동 DB를 사용하는 DataSource를 빈으로 등록하고, 다른 하나에는 테스트에서 사용할 DataSource가 빈으로 등록되게 만드는 것이다.
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver" />
//testdb 등록
<property name="url" value="jdbc:mysql://localhost/testdb?characterEncoding=UTF-8" />
<property name="username" value="spring" />
<property name="password" value="book" />
</bean>
이후 설정정보 파일의 위치를 지정하는 locations 값을 테스트용 설정 파일로 변경한다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/test-applicationContext.xml")
public class UserDaoTest {
...
}
컨테이너 없는 DI 테스트
마지막으로는 아예 스프링 컨테이너를 사용하지 않고 테스트를 만드는 방법을 살펴본다.
public class UserDaoTest {
UserDao dao; //@AutoWired가 없다.
@Before
public void setUp() {
...
dao = new UserDao();
DataSource dataSource = new SingleConnectionDataSource(
"jdbc:mysql://localhost/testdb", "spiring", "book", true);
// 오브젝트 생성, 주입을 모두 직접한다.
dao.setDataSource(dataSource);
}
DataSource를 직접 만드는 번거로움은 있으나 애플리케이션 컨텍스트를 아예 사용하지 않으니 코드는 단순해졌다.
DI를 테스트에 이용하는 방법을 세 가지 살펴보았다.
항상 스프링 컨테이너 없이 테스트 할 수 있는 방법을 우선적으로 고려하자(마지막에 소개한 방법)
가장 빠르고 간결하다.
여러 오브젝트와 복잡한 의존관계를 갖는 오브젝트를 테스트 할때는 스프링의 설정을 이용한 DI 방식의 테스트를 이용하면 편리할 것이다.
'토비의 스터디' 카테고리의 다른 글
[토비의 스프링 3.1] 3.1 다시 보는 초난감 DAO (0) | 2022.07.05 |
---|---|
[토비의 스프링 3.1] 2.5 학습 테스트로 배우는 스프링 (0) | 2022.07.04 |
[토비의 스프링 3.1] 2.2 UserDaoTest 개선 (0) | 2022.06.09 |
[토비의 스프링 3.1] 2.3 개발자를 위한 테스팅 프레임워크 JUnit (0) | 2022.06.03 |
[토비의 스프링 3.1] 2.1 UserDaoTest 다시 보기 (0) | 2022.06.01 |