2.4) 스프링 테스트 적용
테스트를 위한 애플리케이션 컨텍스트 관리
- JUnit은 매번 테스트 클래스의 오브젝트를 새로 만든다.
- @Before 메소드는 테스트 메소드 개수만큼 반복되고 그에 따라 애플리케이션 컨텍스트도 개수만큼 만들어진다.
- 물론 테스트는 가능한 한 독립적으로 매번 새로운 오브젝트를 만들어서 사용하는 것이 원칙이다.
- 하지만 애플리케이션 컨텍스트처럼 생성에 많은 시간과 자원이 소모되는 경우에는
테스트 전체가 공유하는 오브젝트를 만들기도 한다.
이때도 테스트는 일관성 있는 실행 결과를 보장해야 하고, 테스트의 실행 순서가 결과에 영향을 미치지 않아야 한다. - JUnit은 테스트 클래스 전체에 걸쳐 딱 한 번만 실행되는 @BeforeClass 스태틱 메소드를 지원한다.
이 메소드에서 애플리케이션 컨텍스트를 만들어 스태틱 변수에 저장해두고 테스트 메소드에서 사용하게 할 수 있다. - 하지만 이보다는 스프링이 직접 제공하는 애플리케이션 컨텍스트 테스트 지원 기능이 더 편리하다.
- 스프링은 JUnit을 이용한 테스트 컨텍스트 프레임워크를 제공한다.
- 이를 통해 간단한 어노테이션 설정만으로 테스트에서 필요로 하는 애플리케이션 컨텍스트를 만들어 공유하게 할 수 있다.
- 먼저 @Before 메소드에서 애플리케이션 컨텍스트를 생성하는 코드를 제거한 후
ApplicationContext 타입의 인스턴스 변수를 선언하고 @Autowired 어노테이션을 붙여준다.
마지막으로 클래스 레벨에 @RunWith와 @ContextConfiguration 어노테이션을 추가해준다. - 이때 @RunWith는 JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 어노테이션으로
SpringJUnit4ClassRunner라는 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면
JUnit이 테스트를 진행하는 중에 테스트가 사용할 애플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해준다.
@ContextConfiguration은 자동으로 만들어줄 애플리케이션 컨텍스트의 설정파일 위치를 지정한 것이다. - 그러므로 context를 사용하려고 할 때
JUnit의 확장 기능을 통해 애플리케이션 컨텍스트가 들어 있어 NullPointerException이 발생하지 않는 것이다.
/**
* 스프링 테스트 컨텍스트를 적용한 UserDaoTest
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
@Autowired
private ApplicationContext context;
private UserDao userDao;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// XML 애플리케이션 컨텍스트로 UserDao 오브젝트를 받아온다.
this.userDao = this.context.getBean("userDao", UserDao.class);
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
@Test
public void addAndGet() throws SQLException {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDao.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDao.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDao.add(user1);
userDao.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDao.add(user1);
assertThat(userDao.getCount(), is(1));
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
userDao.add(user3);
assertThat(userDao.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDao.get("unknown_id"); // 예외가 발생한다.
}
}
- @Before를 사용해 인스턴스 변수 context와 테스트 오브젝트 자신인 this를 출력해보자.
- context는 세 번 모두 동일하므로
하나의 애플리케이션 컨텍스트가 만들어져 모든 테스트 메소드에서 사용되고 있음을 알 수 있다. - 반면에 UserDaoTest의 오브젝트는 매번 주소 값이 다르므로
JUnit은 테스트 메소드를 실행할 때마다 새로운 테스트 오브젝트를 만들어 내는 것을 알 수 있다.
- context는 세 번 모두 동일하므로
/**
* 스프링 테스트 컨텍스트를 적용한 UserDaoTest
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
@Autowired
private ApplicationContext context;
private UserDao userDao;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// XML 애플리케이션 컨텍스트로 UserDao 오브젝트를 받아온다.
this.userDao = this.context.getBean("userDao", UserDao.class);
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
System.out.println(this.context);
System.out.println(this);
}
@Test
public void addAndGet() throws SQLException {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDao.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDao.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDao.add(user1);
userDao.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDao.add(user1);
assertThat(userDao.getCount(), is(1));
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
userDao.add(user3);
assertThat(userDao.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDao.get("unknown_id"); // 예외가 발생한다.
}
}
- 그렇다면 어떻게 context 변수에 애플리케이션 컨텍스트가 들어 있는 것일까?
- 스프링의 JUnit 확장 기능은 테스트가 실행되기 전에 딱 한 번만 애플리케이션 컨텍스트를 만들어두고
- 테스트 오브젝트가 만들어질 때마다 특별한 방법을 이용해
애플리케이션 컨텍스트 자신을 테스트 오브젝트의 특정 필드에 주입해주는 것이다.
- 스프링의 JUnit 확장 기능은 테스트가 실행되기 전에 딱 한 번만 애플리케이션 컨텍스트를 만들어두고
- 스프링 테스트 컨텍스트 프레임워크는 하나의 테스트 클래스를 넘어
여러 개의 테스트 클래스에 애플리케이션 컨텍스트를 공유하게 해준다.
- 여러 개의 테스트 클래스가 있는데 모두 같은 설정파일을 가진 애플리케이션 컨텍스트를 사용한다면,
스프링은 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해준다. - 만약 두 개의 테스트 클래스가 같은 설정파일을 사용하는 경우에는
테스트 수행 중에 단 한 개의 애플리케이션 컨텍스트가 만들어지며
두 테스트 클래스의 모든 메소드가 하나의 애플리케이션 컨텍스트를 공유하게 된다. - 이 덕분에 테스트 성능이 대폭 향상된다.
- 여러 개의 테스트 클래스가 있는데 모두 같은 설정파일을 가진 애플리케이션 컨텍스트를 사용한다면,
- 스프링의 DI에 사용되는 특별한 어노테이션인 @Autowired?
- @Autowired가 붙은 인스턴스 변수가 있으면,
테스트 컨텍스트 프레임워크는 변수 타입가 일치하는 컨텍스트 내의 빈을 찾고,
타입이 일치하는 빈이 있으면 인스턴스 변수에 주입해준다. - 별도의 DI 설정 없이 필드의 타입정보를 이용해 빈을 자동으로 가져오므로 타입에 의한 자동와이어링이라고 한다.
- 스프링 애플리케이션 컨텍스트는 초기화할 때 자기 자신도 빈으로 등록하기 때문에
앞서 ApplicationContext 타입의 빈이 애플리케이션 컨텍스트에 존재하는 셈이고 DI도 가능했던 것이다.
- @Autowired가 붙은 인스턴스 변수가 있으면,
- 그렇다면 컨텍스트가 아니라 아예 UserDao 빈을 직접 DI 받아오자.
- @Autowired를 이용해 애플리케이션이 갖고 있는 빈을 DI 받을 수 있다면
컨텍스트를 가져와 getBean()을 사용하는 것이 아니라, 아예 UserDao 빈을 직접 DI 받아 코드를 더 깔끔히 할 수 있다. - 이외에도 @Autowired를 지정하기만 하면
변수에 할당 가능한 타입을 가진 빈을 자동으로 찾아주므로 어떤 빈이든 다 가져올 수 있다. - 단, @Autowired는 같은 타입의 빈이 두 개 이상 있는 경우에는 타입만으로는 어떤 빈을 가져올지 결정할 수 없다.
이 경우 변수의 이름과 같은 이름의 빈이 있는지 확인한다.
- @Autowired를 이용해 애플리케이션이 갖고 있는 빈을 DI 받을 수 있다면
/**
* 스프링 테스트 컨텍스트를 적용한 UserDaoTest
*/
@RunWith(SpringJUnit4ClassRunner.class)
public class UserDaoTest {
// 컨텍스트가 아니라 아예 UserDao 빈을 직접 DI 받아온다.
@Autowired
private UserDao userDao;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
@Test
public void addAndGet() throws SQLException {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDao.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDao.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDao.add(user1);
userDao.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDao.add(user1);
assertThat(userDao.getCount(), is(1));
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
userDao.add(user3);
assertThat(userDao.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDao.get("unknown_id"); // 예외가 발생한다.
}
}
DI와 테스트
- 절대로 DataSource의 구현 클래스를 바꾸지 않더라도
UserDao와 DB 커넥션 생성 클래스 사이에 DataSource라는 인터페이스를 둬야만 할까?
- DataSource 인터페이스를 UserDao와 DB 커넥션 생성 클래스 사이에 두게 되면
UserDao는 자신이 사용하는 오브젝트의 클래스가 무엇인지 알 필요가 없고,
DI를 통해 외부에서 사용할 오브젝트를 주입받기 때문에 오브젝트 생성에 대한 부담을 지지 않아도 된다.
또한 코드의 수정 없이도 얼마든지 의존 오브젝트를 바꿔가면 사용할 수 있다. - 하지만 절대로 DataSource의 구현 클래스를 바꾸지 않을 것일 때도
굳이 DataSource 인터페이스를 사용하고 DI를 통해 주입해주는 방식을 이용해야 하는가?
그냥 UserDao에서 직접 생성하고 사용하면 안 될까? - 그래도 인터페이스를 두고 DI를 적용해야 한다.
- 소프트웨어 개발에서는 절대로 바뀌지 않는 것을 없으므로 당장에는 클래스를 바꿔서 사용할 계획이 전혀 없더라도,
언젠가 변경이 필요한 상황이 닥쳤을 때 수정에 들어가는 시간과 비용의 부담을 줄여줄 수 있다. - 클래스의 구현 방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI를 적용할 경우에는
DB 커넥션의 개수를 카운팅하는 부가 기능 등의 다른 차원의 서비스 기능을 도입할 수 있어진다. - 마지막으로 효율적인 테스트를 손쉽게 만들기 위해서는 DI를 적용해야 한다.
테스트를 잘 활용하려면 자동으로 실행 가능하며 빠르게 동작하도록 테스트를 만들어야 하는데
그러기 위해서는 가능한 한 작은 단위의 대상에 국한해서 테스트를 해야하는데
DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하는데 중요한 역할을 한다.
- DataSource 인터페이스를 UserDao와 DB 커넥션 생성 클래스 사이에 두게 되면
- DI는 애플리케이션 컨텍스트 같은 스프링 컨테이너에서만 할 수 있는 작업이 아니다.
- 프레임의 도움 없이 오브젝트 팩토리인 DaoFactory를 만들어 직접 수동으로 DI를 적용해볼 수도 있다.
- UserDao에는 DI 컨테이너가 의존관계 주입에 사용하도록 수정자 메소드를 이전에 만들어뒀다.
이 수정자 메소드는 테스트 코드에서 얼마든지 호출해서 사용할 수 있으므로
이를 이용해 UserDao가 사용할 DataSource 오브젝트를 테스트 코드에서 변경할 수 있다. - 그러므로 테스트를 할 때는 운영용 DB 커넥션가 아니라
테스트 코드에 의한 DI를 이용해서 테스트 중에 DAO가 사용할 DataSource 오브젝트를 바꿔주는 방법을 사용할 수 있다. - 이 방법은 XML 설정파일을 수정하지 않고도 테스트 코드를 통해 오브젝트 관계를 재구성할 수 있다.
- 하지만 이미 애플리케이션 컨텍스트에서 파일의 설정정보에 따라 구성한 오브젝트를 가져와
의존관계를 강제로 변경했기 때문에 한 번 변경하면 나머지 모든 테스트를 수행하는 동안
변경된 애플리케이션 컨텍스트가 계속 사용되므로 바람직하지 못하다. - 그래서 UserDaoTest에 @DirtiesContext 어노테이션을 추가해주어 스프링의 테스트 컨텍스트 프레임워크에게
해당 클래스의 테스트에서 애플리케이션 컨텍스트의 상태를 변경한다는 것을 알려준다.
그러면 테스트 컨텍스트는 이 어노테이션이 붙은 테스트 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않는다.
그리고 테스트 중에 변경한 컨텍스트가 뒤의 테스트에 영향을 주지 않고도록
테스트 메소드를 수행하고 나면 매번 새로운 애플리케이션 컨텍스트를 만들어서 다음 테스트가 사용하게 해준다.
/**
* 스프링 테스트 컨텍스트를 적용한 UserDaoTest
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
// 테스트 메소드에서 애플리케이션 컨텍스트의 구성이나 상태를 변경한다는 것을 테스트 컨텍스트 프레임워크에게 알려준다.
@DirtiesContext
public class UserDaoTest {
// 컨텍스트가 아니라 아예 UserDao 빈을 직접 DI 받아온다.
@Autowired
private UserDao userDao;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// 테스트 코드에 의한 DI를 이용해서 테스트 중에 DAO가 사용할 DataSource 오브젝트를 바꿔줄 수 있다.
DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost:/toby", "toby", "gaga", true);
userDao.setDataSource(dataSource); // 수동 DI
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
@Test
public void addAndGet() throws SQLException {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDao.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDao.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDao.add(user1);
userDao.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDao.add(user1);
assertThat(userDao.getCount(), is(1));
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
userDao.add(user3);
assertThat(userDao.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDao.get("unknown_id"); // 예외가 발생한다.
}
}
- 테스트 코드에서 빈 오브젝트를 수동으로 DI 하는 방법은 장점보다 단점이 많다.
- 코드가 많아져 번거롭기도 하고 애플리케이션 컨텍스트도 매번 새로 만들어야 하는 부담이 있다.
- 이 방법 외에 DI의 장점을 살려서 위처럼 DAO가 테스트에서만 다른 DataSource를 사용하게 할 수 있는 방법이 또 있을까?
있다.
- 테스트에서 사용될 DataSource 클래스가 빈으로 정의된 테스트 전용 설정파일을 따로 만들어두자.
- 두 가지 종류의 설정파일인 applicationContext.xml과 test-applicationContext.xml을 만들어서
하나에는 서버에서 운영용으로 사용할 DataSource를 빈으로 등록해두고,
다른 하나에는 테스트에 적합하게 준비된 DB를 사용하는 가벼운 DataSource가 빈으로 등록되게 만드는 것이다. - 그 후 테스트에서는 항상 테스트 전용 설정파일만 사용하게 해주기 위해
@ContextConfiguration 어노테이션에 있는 locations 엘리먼트의 값을 변경해주면 된다.
- 두 가지 종류의 설정파일인 applicationContext.xml과 test-applicationContext.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">
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver" />
<!-- 테스트를 위해 jdbc:mysql://localhost/testdb" 로 설정 -->
<property name="url" value="jdbc:mysql://localhost/toby" />
<property name="username" value="toby" />
<property name="password" value="gaga" />
</bean>
<bean id="userDao" class="com.gaga.springtoby.user.dao.UserDao">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
/**
* 스프링 테스트 컨텍스트를 적용한 UserDaoTest
*/
@RunWith(SpringJUnit4ClassRunner.class)
// 테스트용 설정파일로 변경한다.
@ContextConfiguration(locations="/test-applicationContext.xml")
public class UserDaoTest {
// 컨텍스트가 아니라 아예 UserDao 빈을 직접 DI 받아온다.
@Autowired
private UserDao userDao;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
@Test
public void addAndGet() throws SQLException {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDao.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDao.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDao.add(user1);
userDao.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDao.add(user1);
assertThat(userDao.getCount(), is(1));
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
userDao.add(user3);
assertThat(userDao.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDao.get("unknown_id"); // 예외가 발생한다.
}
}
- 아예 스프링 컨테이너를 사용하지 않고 테스트를 만들어보자.
- 원한다면 스프링 컨테이너를 이용해서 IoC 방식으로 생성되고 DI 되도록 하는 대신,
테스트 코드에서 직접 오브젝트를 만들고 DI 해서 사용해도 된다. - 즉, @Before 메소드에서 직접 UserDao의 오브젝트를 생성하고,
테스트용 DataSource 오브젝트를 만들어 직접 DI 하는 것이다. - 테스트를 위한 DataSource를 직접 만드는 번거로움은 있지만 애플리케이션 컨텍스트를 아예 사용하지 않으니
코드는 더 단순해지고 이해하기 편해지며 애플리케이션 컨텍스트가 만들어지는 시간을 절약할 수 있다. - 하지만 JUnit은 매번 새로운 테스트 오브젝트를 만들기 때문에 매번 새로운 UserDao 오브젝트가 만들어지게 된다.
- 이를 통해 DI를 위해 컨테이너가 반드시 필요한 것은 아님을 알 수 있다.
DI 컨테이너나 프레임워크는 DI를 편하게 적용하도록 도움을 줄 뿐, 컨테이너가 DI를 가능하게 해주는 것은 아니다.
- 원한다면 스프링 컨테이너를 이용해서 IoC 방식으로 생성되고 DI 되도록 하는 대신,
/**
* 애플리케이션 컨텍스트가 없는 UserDaoTest
*/
public class UserDaoTest {
private UserDao userDao;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// 직접 UserDao의 오브젝트를 생성하고, 테스트용 DataSource 오브젝트를 만들어 직접 DI 한다.
userDao = new UserDao();
DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost:/toby", "toby", "gaga", true);
userDao.setDataSource(dataSource); // 수동 DI
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
@Test
public void addAndGet() throws SQLException {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDao.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDao.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDao.add(user1);
userDao.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDao.add(user1);
assertThat(userDao.getCount(), is(1));
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
userDao.add(user3);
assertThat(userDao.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDao.get("unknown_id"); // 예외가 발생한다.
}
}
- DI를 테스트에 이용하는 세 가지 방법 중 어떤 것을 선택해야 할까?
- 스프링 컨테이너가 없을 때 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하므로
항상 스프링 컨테이너 없이 테스트할 수 있는 방법을 가장 우선적으로 고려하자.
테스트를 위해 필요한 오브젝트의 생성과 초기화가 단순하다면 이 방법을 가장 먼저 고려해야 한다. - 여러 오브젝트의 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우에는
스프링의 설정을 이용한 DI 방식의 테스트를 이용하여 각 다른 설정파일을 만들어 사용하면 편리하다. - 테스트 설정을 따로 만들었다고 하더라도 때로는 예외적인 의존관계를 강제로 구성해서 테스트해야 할 경우가 있다.
이때는 컨텍스트에서 DI 받은 오브젝트를 다시 테스트 코드로 수동 DI 해서 테스트하는 방법을 사용하면 된다.
이 때 테스트 메소드나 클래스에 @DirtiesContext 어노테이션을 붙여야 한다.
- 스프링 컨테이너가 없을 때 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하므로
'Java-Spring > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 템플릿 (0) (0) | 2023.09.28 |
---|---|
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 테스트 (5) (0) | 2023.09.23 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 테스트 (3) (0) | 2023.09.22 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 테스트 (2) (0) | 2023.09.21 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 테스트 (1) (0) | 2023.09.21 |
2.4) 스프링 테스트 적용
테스트를 위한 애플리케이션 컨텍스트 관리
- JUnit은 매번 테스트 클래스의 오브젝트를 새로 만든다.
- @Before 메소드는 테스트 메소드 개수만큼 반복되고 그에 따라 애플리케이션 컨텍스트도 개수만큼 만들어진다.
- 물론 테스트는 가능한 한 독립적으로 매번 새로운 오브젝트를 만들어서 사용하는 것이 원칙이다.
- 하지만 애플리케이션 컨텍스트처럼 생성에 많은 시간과 자원이 소모되는 경우에는
테스트 전체가 공유하는 오브젝트를 만들기도 한다.
이때도 테스트는 일관성 있는 실행 결과를 보장해야 하고, 테스트의 실행 순서가 결과에 영향을 미치지 않아야 한다. - JUnit은 테스트 클래스 전체에 걸쳐 딱 한 번만 실행되는 @BeforeClass 스태틱 메소드를 지원한다.
이 메소드에서 애플리케이션 컨텍스트를 만들어 스태틱 변수에 저장해두고 테스트 메소드에서 사용하게 할 수 있다. - 하지만 이보다는 스프링이 직접 제공하는 애플리케이션 컨텍스트 테스트 지원 기능이 더 편리하다.
- 스프링은 JUnit을 이용한 테스트 컨텍스트 프레임워크를 제공한다.
- 이를 통해 간단한 어노테이션 설정만으로 테스트에서 필요로 하는 애플리케이션 컨텍스트를 만들어 공유하게 할 수 있다.
- 먼저 @Before 메소드에서 애플리케이션 컨텍스트를 생성하는 코드를 제거한 후
ApplicationContext 타입의 인스턴스 변수를 선언하고 @Autowired 어노테이션을 붙여준다.
마지막으로 클래스 레벨에 @RunWith와 @ContextConfiguration 어노테이션을 추가해준다. - 이때 @RunWith는 JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 어노테이션으로
SpringJUnit4ClassRunner라는 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면
JUnit이 테스트를 진행하는 중에 테스트가 사용할 애플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해준다.
@ContextConfiguration은 자동으로 만들어줄 애플리케이션 컨텍스트의 설정파일 위치를 지정한 것이다. - 그러므로 context를 사용하려고 할 때
JUnit의 확장 기능을 통해 애플리케이션 컨텍스트가 들어 있어 NullPointerException이 발생하지 않는 것이다.
/**
* 스프링 테스트 컨텍스트를 적용한 UserDaoTest
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
@Autowired
private ApplicationContext context;
private UserDao userDao;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// XML 애플리케이션 컨텍스트로 UserDao 오브젝트를 받아온다.
this.userDao = this.context.getBean("userDao", UserDao.class);
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
@Test
public void addAndGet() throws SQLException {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDao.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDao.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDao.add(user1);
userDao.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDao.add(user1);
assertThat(userDao.getCount(), is(1));
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
userDao.add(user3);
assertThat(userDao.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDao.get("unknown_id"); // 예외가 발생한다.
}
}
- @Before를 사용해 인스턴스 변수 context와 테스트 오브젝트 자신인 this를 출력해보자.
- context는 세 번 모두 동일하므로
하나의 애플리케이션 컨텍스트가 만들어져 모든 테스트 메소드에서 사용되고 있음을 알 수 있다. - 반면에 UserDaoTest의 오브젝트는 매번 주소 값이 다르므로
JUnit은 테스트 메소드를 실행할 때마다 새로운 테스트 오브젝트를 만들어 내는 것을 알 수 있다.
- context는 세 번 모두 동일하므로
/**
* 스프링 테스트 컨텍스트를 적용한 UserDaoTest
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
@Autowired
private ApplicationContext context;
private UserDao userDao;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// XML 애플리케이션 컨텍스트로 UserDao 오브젝트를 받아온다.
this.userDao = this.context.getBean("userDao", UserDao.class);
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
System.out.println(this.context);
System.out.println(this);
}
@Test
public void addAndGet() throws SQLException {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDao.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDao.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDao.add(user1);
userDao.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDao.add(user1);
assertThat(userDao.getCount(), is(1));
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
userDao.add(user3);
assertThat(userDao.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDao.get("unknown_id"); // 예외가 발생한다.
}
}
- 그렇다면 어떻게 context 변수에 애플리케이션 컨텍스트가 들어 있는 것일까?
- 스프링의 JUnit 확장 기능은 테스트가 실행되기 전에 딱 한 번만 애플리케이션 컨텍스트를 만들어두고
- 테스트 오브젝트가 만들어질 때마다 특별한 방법을 이용해
애플리케이션 컨텍스트 자신을 테스트 오브젝트의 특정 필드에 주입해주는 것이다.
- 스프링의 JUnit 확장 기능은 테스트가 실행되기 전에 딱 한 번만 애플리케이션 컨텍스트를 만들어두고
- 스프링 테스트 컨텍스트 프레임워크는 하나의 테스트 클래스를 넘어
여러 개의 테스트 클래스에 애플리케이션 컨텍스트를 공유하게 해준다.
- 여러 개의 테스트 클래스가 있는데 모두 같은 설정파일을 가진 애플리케이션 컨텍스트를 사용한다면,
스프링은 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해준다. - 만약 두 개의 테스트 클래스가 같은 설정파일을 사용하는 경우에는
테스트 수행 중에 단 한 개의 애플리케이션 컨텍스트가 만들어지며
두 테스트 클래스의 모든 메소드가 하나의 애플리케이션 컨텍스트를 공유하게 된다. - 이 덕분에 테스트 성능이 대폭 향상된다.
- 여러 개의 테스트 클래스가 있는데 모두 같은 설정파일을 가진 애플리케이션 컨텍스트를 사용한다면,
- 스프링의 DI에 사용되는 특별한 어노테이션인 @Autowired?
- @Autowired가 붙은 인스턴스 변수가 있으면,
테스트 컨텍스트 프레임워크는 변수 타입가 일치하는 컨텍스트 내의 빈을 찾고,
타입이 일치하는 빈이 있으면 인스턴스 변수에 주입해준다. - 별도의 DI 설정 없이 필드의 타입정보를 이용해 빈을 자동으로 가져오므로 타입에 의한 자동와이어링이라고 한다.
- 스프링 애플리케이션 컨텍스트는 초기화할 때 자기 자신도 빈으로 등록하기 때문에
앞서 ApplicationContext 타입의 빈이 애플리케이션 컨텍스트에 존재하는 셈이고 DI도 가능했던 것이다.
- @Autowired가 붙은 인스턴스 변수가 있으면,
- 그렇다면 컨텍스트가 아니라 아예 UserDao 빈을 직접 DI 받아오자.
- @Autowired를 이용해 애플리케이션이 갖고 있는 빈을 DI 받을 수 있다면
컨텍스트를 가져와 getBean()을 사용하는 것이 아니라, 아예 UserDao 빈을 직접 DI 받아 코드를 더 깔끔히 할 수 있다. - 이외에도 @Autowired를 지정하기만 하면
변수에 할당 가능한 타입을 가진 빈을 자동으로 찾아주므로 어떤 빈이든 다 가져올 수 있다. - 단, @Autowired는 같은 타입의 빈이 두 개 이상 있는 경우에는 타입만으로는 어떤 빈을 가져올지 결정할 수 없다.
이 경우 변수의 이름과 같은 이름의 빈이 있는지 확인한다.
- @Autowired를 이용해 애플리케이션이 갖고 있는 빈을 DI 받을 수 있다면
/**
* 스프링 테스트 컨텍스트를 적용한 UserDaoTest
*/
@RunWith(SpringJUnit4ClassRunner.class)
public class UserDaoTest {
// 컨텍스트가 아니라 아예 UserDao 빈을 직접 DI 받아온다.
@Autowired
private UserDao userDao;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
@Test
public void addAndGet() throws SQLException {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDao.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDao.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDao.add(user1);
userDao.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDao.add(user1);
assertThat(userDao.getCount(), is(1));
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
userDao.add(user3);
assertThat(userDao.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDao.get("unknown_id"); // 예외가 발생한다.
}
}
DI와 테스트
- 절대로 DataSource의 구현 클래스를 바꾸지 않더라도
UserDao와 DB 커넥션 생성 클래스 사이에 DataSource라는 인터페이스를 둬야만 할까?
- DataSource 인터페이스를 UserDao와 DB 커넥션 생성 클래스 사이에 두게 되면
UserDao는 자신이 사용하는 오브젝트의 클래스가 무엇인지 알 필요가 없고,
DI를 통해 외부에서 사용할 오브젝트를 주입받기 때문에 오브젝트 생성에 대한 부담을 지지 않아도 된다.
또한 코드의 수정 없이도 얼마든지 의존 오브젝트를 바꿔가면 사용할 수 있다. - 하지만 절대로 DataSource의 구현 클래스를 바꾸지 않을 것일 때도
굳이 DataSource 인터페이스를 사용하고 DI를 통해 주입해주는 방식을 이용해야 하는가?
그냥 UserDao에서 직접 생성하고 사용하면 안 될까? - 그래도 인터페이스를 두고 DI를 적용해야 한다.
- 소프트웨어 개발에서는 절대로 바뀌지 않는 것을 없으므로 당장에는 클래스를 바꿔서 사용할 계획이 전혀 없더라도,
언젠가 변경이 필요한 상황이 닥쳤을 때 수정에 들어가는 시간과 비용의 부담을 줄여줄 수 있다. - 클래스의 구현 방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI를 적용할 경우에는
DB 커넥션의 개수를 카운팅하는 부가 기능 등의 다른 차원의 서비스 기능을 도입할 수 있어진다. - 마지막으로 효율적인 테스트를 손쉽게 만들기 위해서는 DI를 적용해야 한다.
테스트를 잘 활용하려면 자동으로 실행 가능하며 빠르게 동작하도록 테스트를 만들어야 하는데
그러기 위해서는 가능한 한 작은 단위의 대상에 국한해서 테스트를 해야하는데
DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하는데 중요한 역할을 한다.
- DataSource 인터페이스를 UserDao와 DB 커넥션 생성 클래스 사이에 두게 되면
- DI는 애플리케이션 컨텍스트 같은 스프링 컨테이너에서만 할 수 있는 작업이 아니다.
- 프레임의 도움 없이 오브젝트 팩토리인 DaoFactory를 만들어 직접 수동으로 DI를 적용해볼 수도 있다.
- UserDao에는 DI 컨테이너가 의존관계 주입에 사용하도록 수정자 메소드를 이전에 만들어뒀다.
이 수정자 메소드는 테스트 코드에서 얼마든지 호출해서 사용할 수 있으므로
이를 이용해 UserDao가 사용할 DataSource 오브젝트를 테스트 코드에서 변경할 수 있다. - 그러므로 테스트를 할 때는 운영용 DB 커넥션가 아니라
테스트 코드에 의한 DI를 이용해서 테스트 중에 DAO가 사용할 DataSource 오브젝트를 바꿔주는 방법을 사용할 수 있다. - 이 방법은 XML 설정파일을 수정하지 않고도 테스트 코드를 통해 오브젝트 관계를 재구성할 수 있다.
- 하지만 이미 애플리케이션 컨텍스트에서 파일의 설정정보에 따라 구성한 오브젝트를 가져와
의존관계를 강제로 변경했기 때문에 한 번 변경하면 나머지 모든 테스트를 수행하는 동안
변경된 애플리케이션 컨텍스트가 계속 사용되므로 바람직하지 못하다. - 그래서 UserDaoTest에 @DirtiesContext 어노테이션을 추가해주어 스프링의 테스트 컨텍스트 프레임워크에게
해당 클래스의 테스트에서 애플리케이션 컨텍스트의 상태를 변경한다는 것을 알려준다.
그러면 테스트 컨텍스트는 이 어노테이션이 붙은 테스트 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않는다.
그리고 테스트 중에 변경한 컨텍스트가 뒤의 테스트에 영향을 주지 않고도록
테스트 메소드를 수행하고 나면 매번 새로운 애플리케이션 컨텍스트를 만들어서 다음 테스트가 사용하게 해준다.
/**
* 스프링 테스트 컨텍스트를 적용한 UserDaoTest
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
// 테스트 메소드에서 애플리케이션 컨텍스트의 구성이나 상태를 변경한다는 것을 테스트 컨텍스트 프레임워크에게 알려준다.
@DirtiesContext
public class UserDaoTest {
// 컨텍스트가 아니라 아예 UserDao 빈을 직접 DI 받아온다.
@Autowired
private UserDao userDao;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// 테스트 코드에 의한 DI를 이용해서 테스트 중에 DAO가 사용할 DataSource 오브젝트를 바꿔줄 수 있다.
DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost:/toby", "toby", "gaga", true);
userDao.setDataSource(dataSource); // 수동 DI
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
@Test
public void addAndGet() throws SQLException {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDao.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDao.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDao.add(user1);
userDao.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDao.add(user1);
assertThat(userDao.getCount(), is(1));
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
userDao.add(user3);
assertThat(userDao.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDao.get("unknown_id"); // 예외가 발생한다.
}
}
- 테스트 코드에서 빈 오브젝트를 수동으로 DI 하는 방법은 장점보다 단점이 많다.
- 코드가 많아져 번거롭기도 하고 애플리케이션 컨텍스트도 매번 새로 만들어야 하는 부담이 있다.
- 이 방법 외에 DI의 장점을 살려서 위처럼 DAO가 테스트에서만 다른 DataSource를 사용하게 할 수 있는 방법이 또 있을까?
있다.
- 테스트에서 사용될 DataSource 클래스가 빈으로 정의된 테스트 전용 설정파일을 따로 만들어두자.
- 두 가지 종류의 설정파일인 applicationContext.xml과 test-applicationContext.xml을 만들어서
하나에는 서버에서 운영용으로 사용할 DataSource를 빈으로 등록해두고,
다른 하나에는 테스트에 적합하게 준비된 DB를 사용하는 가벼운 DataSource가 빈으로 등록되게 만드는 것이다. - 그 후 테스트에서는 항상 테스트 전용 설정파일만 사용하게 해주기 위해
@ContextConfiguration 어노테이션에 있는 locations 엘리먼트의 값을 변경해주면 된다.
- 두 가지 종류의 설정파일인 applicationContext.xml과 test-applicationContext.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">
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver" />
<!-- 테스트를 위해 jdbc:mysql://localhost/testdb" 로 설정 -->
<property name="url" value="jdbc:mysql://localhost/toby" />
<property name="username" value="toby" />
<property name="password" value="gaga" />
</bean>
<bean id="userDao" class="com.gaga.springtoby.user.dao.UserDao">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
/**
* 스프링 테스트 컨텍스트를 적용한 UserDaoTest
*/
@RunWith(SpringJUnit4ClassRunner.class)
// 테스트용 설정파일로 변경한다.
@ContextConfiguration(locations="/test-applicationContext.xml")
public class UserDaoTest {
// 컨텍스트가 아니라 아예 UserDao 빈을 직접 DI 받아온다.
@Autowired
private UserDao userDao;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
@Test
public void addAndGet() throws SQLException {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDao.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDao.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDao.add(user1);
userDao.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDao.add(user1);
assertThat(userDao.getCount(), is(1));
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
userDao.add(user3);
assertThat(userDao.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDao.get("unknown_id"); // 예외가 발생한다.
}
}
- 아예 스프링 컨테이너를 사용하지 않고 테스트를 만들어보자.
- 원한다면 스프링 컨테이너를 이용해서 IoC 방식으로 생성되고 DI 되도록 하는 대신,
테스트 코드에서 직접 오브젝트를 만들고 DI 해서 사용해도 된다. - 즉, @Before 메소드에서 직접 UserDao의 오브젝트를 생성하고,
테스트용 DataSource 오브젝트를 만들어 직접 DI 하는 것이다. - 테스트를 위한 DataSource를 직접 만드는 번거로움은 있지만 애플리케이션 컨텍스트를 아예 사용하지 않으니
코드는 더 단순해지고 이해하기 편해지며 애플리케이션 컨텍스트가 만들어지는 시간을 절약할 수 있다. - 하지만 JUnit은 매번 새로운 테스트 오브젝트를 만들기 때문에 매번 새로운 UserDao 오브젝트가 만들어지게 된다.
- 이를 통해 DI를 위해 컨테이너가 반드시 필요한 것은 아님을 알 수 있다.
DI 컨테이너나 프레임워크는 DI를 편하게 적용하도록 도움을 줄 뿐, 컨테이너가 DI를 가능하게 해주는 것은 아니다.
- 원한다면 스프링 컨테이너를 이용해서 IoC 방식으로 생성되고 DI 되도록 하는 대신,
/**
* 애플리케이션 컨텍스트가 없는 UserDaoTest
*/
public class UserDaoTest {
private UserDao userDao;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// 직접 UserDao의 오브젝트를 생성하고, 테스트용 DataSource 오브젝트를 만들어 직접 DI 한다.
userDao = new UserDao();
DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost:/toby", "toby", "gaga", true);
userDao.setDataSource(dataSource); // 수동 DI
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
@Test
public void addAndGet() throws SQLException {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDao.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDao.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDao.add(user1);
userDao.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDao.add(user1);
assertThat(userDao.getCount(), is(1));
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
userDao.add(user3);
assertThat(userDao.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() throws SQLException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDao.get("unknown_id"); // 예외가 발생한다.
}
}
- DI를 테스트에 이용하는 세 가지 방법 중 어떤 것을 선택해야 할까?
- 스프링 컨테이너가 없을 때 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하므로
항상 스프링 컨테이너 없이 테스트할 수 있는 방법을 가장 우선적으로 고려하자.
테스트를 위해 필요한 오브젝트의 생성과 초기화가 단순하다면 이 방법을 가장 먼저 고려해야 한다. - 여러 오브젝트의 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우에는
스프링의 설정을 이용한 DI 방식의 테스트를 이용하여 각 다른 설정파일을 만들어 사용하면 편리하다. - 테스트 설정을 따로 만들었다고 하더라도 때로는 예외적인 의존관계를 강제로 구성해서 테스트해야 할 경우가 있다.
이때는 컨텍스트에서 DI 받은 오브젝트를 다시 테스트 코드로 수동 DI 해서 테스트하는 방법을 사용하면 된다.
이 때 테스트 메소드나 클래스에 @DirtiesContext 어노테이션을 붙여야 한다.
- 스프링 컨테이너가 없을 때 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하므로
'Java-Spring > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 템플릿 (0) (0) | 2023.09.28 |
---|---|
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 테스트 (5) (0) | 2023.09.23 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 테스트 (3) (0) | 2023.09.22 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 테스트 (2) (0) | 2023.09.21 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 테스트 (1) (0) | 2023.09.21 |