이전에 getCount 메소드를 테스트에 적용하긴 했지만, 테이블이 비어 있는 경우와 add()를 한번 호출한 뒤의 결과 뿐이다.
더 꼼꼼하게 getCount를 테스트 하려면 사용자 정보를 하나씩 추가하며 매번 getCount의 값이 1씩 증가하는지 확인해봐야 한다.
일단 편의를 위해 파라미터가 있는 User클래스의 생성자를 추가한 상태에서 진행한다.
@Test
public void count() throws SQLException {
// ...
User user1 = new User("gyumee" , "박성철", "springno1");
User user2 = new User("leegw700", "이길원", "springno2");
User user3 = new User("bumjin", "박범진", "springno3");
dao.deleteAll();
assertThat(dao.getCount(), is(0));
dao.add(user1);
assertThat (dao .getCount(), is(1));
dao .add(user2);
assertThat(dao.getCount(), is(2));
dao.add(user3);
assertThat(dao.getCount(), is(3));
}
getCount 테스트와 이전에 있던 테스트중에 어떤 테스트가 먼저 실행될지 알 수 없으므로 각 테스트는 실행 순서에 상관없이 독립적으로 같은 결과를 낼 수 있도록 해야 한다.
이번에는 addAndGet() 테스트를 보완해보자. add는 이미 검증이 된 것 같지만, get의 경우는 get이 파라미터로 주어진 id에 해당하는 사용자를 가져온 것인지 그냥 아무거나 가져온 것인지 테스트에서 검증하지는 못했다.
→ User를 하나 더 추가해서 두 개의 User를 add( ) 하고, 각 User의 id를 파라미터로 전달해서 get()을 실행하도록 해보자.
@Test
public void addAndGet() throws SQLException {
// ...
UserDao dao = context.getBean("userDao", UserDao.class);
User user1 = new User("gyumee", "박성철", "springno1")
User user2 = new User("leegw700", "이길원", "springno2");
dao.deleteAll();
assertThat(dao.getCount(), is(0));
dao.add(user1);
dao.add(user2);
assertThat(dao.getCount(), iS(2));
User userget1 = dao.get(user1.getld());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
User userget2 = dao.get(user2.getld());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
이리하여 get 메소드에 대한 검증이 더 탄탄해졌다.
하지만, 만약 get 메소드가 받은 id에 대한 사용자 정보가 없다면 어떻게 될까?
null을 리턴시키는 방법도 있지만, 여기선 id에 해당하는 정보가 없다면 error가 발생하도록 해보자.
@Test(expected=EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException {
// ...
dao.deleteAll();
assertThat(dao.getCount(), is(0));
dao.get("unknown_id");
}
@Test(expected=EmptyResultDataAccessException.class) 이부분은 테스트 코드 실행 결과로 예상되는 값을 넣은 것이다. 즉 이 테스트에서는 EmptyResultDataAccessException이 발생해야 테스트가 성공했다고 인식하는 것이다.
즉 dao.get("unknown_id"); 이 부분에서 에러가 발생하지 않으면 테스트가 실패되는 것이다.
하지만 우리가 get 메소드에 해당하는 id를 찾지 못했을 때 에러를 반환하도록 하지 않았으므로 테스트는 실패한다. 그러므로 get에 id에 해당하는 데이터가 없으면 에러를 던지도록 수정해야한다.
만들고자 하는 기능의 내용을 담고 있으면서, 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고, 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발 방법
즉 만들 기능에 대한 테스트 코드를 먼저 작성한 뒤 테스트 코드가 성공하도록 개발을 하는 방식을 테스트 주도 개발이라고 한다.
TDD는 테스트를 먼저 만들고 그 테스트가 성공하도록 하는 코드만 만드는 식으로 진행하기 때문에
TDD에서는 테스트를 작성하고, 성공시키는 코드를 만드는 작업주기를 가능한 짧게 가져가도록 권장한다.
JUnit이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식은 다음과 같다.
→ 테스트를 수행하는 데 필요한 오브젝트나 정보(픽스처, fixture)들은 @Before에 넣고, 일부 메소드에서만 반복적으로 호출되는 코드는 일반 메서드로 빼두는 것이 좋다.
기존 테스트 코드는 테스트 메소드 수 만큼 애플리케이션 컨텍스트가 만들어지고 있다.
스프링은 이를 위해 애플리케이션 컨텍스트 테스트 지원 기능을 제공한다.
아래의 코드는 스프링 테스트 컨텍스트를 적용한 UserDaoTest이다.
// 스프링의 테스트 컨텍스트 프레임 워크의 JUnit 확장기능 지정
@RunWith(SpringlUnit4ClassRunner.class)
// 테스트 컨텍스트가 자동으로 만들어줄 애플리케이션 컨텍스트의 경로 지정
@ContextConfiguration(locations="/applicationContext.xm1")
public class UserDaoTest {
// 테스트 오브젝트가 만들어지고 나면 스프링 테스트 컨텍스트에 의해 자동으로 값이 주입된다.
@Autowired
private ApplicationContext context;
// ...
@Before
public void setUp() {
this.dao = this.context.getBean("userDao", UserDao.class);
// ...
}
}
여기서 각 테스트 메소드 실행 전에 this.context와 this를 출력해보면 this는 매번 다르게 나오는 반면에 this.context는 매번 같게 나오는 것을 알 수 있다.
또한 다른 테스트 클래스를 만들더라도 @ContextConfiguration 어노테이션에서 같은 경로를 지정해주면 설정파일을 공유할 수 있다.
@Autowired가 붙은 인스턴스 변수가 있으면 태스트 컨텍스트 프레임워크는 변수 타입 과 일치하는 컨텍스트 내의 빈을 찾는다. 타입이 일치하는 빈이 있으면 인스턴스 변수 에 주입해준다.
public class UserDaoTest {
// 테스트 오브젝트가 만들어지고 나면 스프링 테스트 컨텍스트에 의해 자동으로 값이 주입된다.
@Autowired
private UserDao dao;
ApplicationContext DI 받고 DL로 UserDao를 가져오는 방식대신에 바로 UserDao를 DI 받도록 할 수도 있다.
@Autowired는 변수에 할당 가능한 타입을 가진 빈을 자동으로 찾는다. 따라서 SimpleDriverDataSource 클래스 타입은 물론이고, 인터페이스인 DataSource 타입으로 변수를 선언해도 된다.
단, @Autowired는 같은 타입의 빈이 두 개 이상 있는 경우에는 타입만으로는 어떤 빈을 가져올지 결정할 수 없다. 만약 같은 타입의 빈이 여러개 있다면 변수의 이름과 같은 빈이 있다면 해당 빈을 주입하고, 변수 이름으로도 빈을 찾을 수 없다면 예외가 발생한다.
근데 굳이 왜 DataSource 인터페이스 를 사용하고 DI를 통해 주입해주는 방식을 이용해야 할까? 그냥 UserDao에서 직접 SimpleDriverDataSource를 생성하고 사용하면 안 될까? 라는 의문이 든다.
그럼에도 불구하고 인터페이스를 두고 DI를 적용해야 하는 이유는
소프트웨어 개발에서 절대로 바뀌지 않는 것은 없기 때문이다.
클래스의 구현 방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI 를 적용하게 해두면 다른 차원의 서비스 기능을 도입할 수 있기 때문이다.
예시로 1장에서 DB 커넥션의 개수를 카운팅하는 부가기능을 추가했던 것이 있다.
테스트
테스트할 대상의 범위가 넓어지면 테스트를 작성하기가 어려워진다. DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하 는 데 중요한 역할을 한다.
테스트 코드에 의한 DI
만약 애플리케이션이 사용할 설정파일에 정의된 DataSource 빈은 서버의 DB 풀 서비스와 연결해서 운영용 DB 커넥션을 돌려주도록 만들어져 있다고 하자.
그렇다면 테스트할 때 이 DataSource를 이용하다가 deleteAll() 메소드로 모든 데이터가 삭제된다면 정말 큰일이 난다. 그런 경우에는 테스트 코드에 의한 DI를 이용해서 테스트 중에 DAO가 사용할 DataSource를 바꿔주는 방법을 이용하면 된다.
// 테스트 메소드에서 애플리케이션 컨텍스트의 구성이나 상태를 변경한다는 것을 알려준다
@DirtiesContext
public class UserDaoTest {
@Autowired
UserDao dao;
@Before
public void setUp() {
// ...
// 테스트에서 UserDao가 사용할 DataSource오브젝트를 직접 생성한다.
DataSource dataSource = new SingleConnectionDataSource(
"jdbc:mysql://localhost/testdb", "spring", "book", true);
dao.setDataSource(dataSource); // 코드에 의한 수동 DI
테스트를 위한 별도의 DI 설정
위의 방법보다는 사실 별도의 DI 설정 파일을 만들어서 하는편이 낫다.
test-applicationContext.xml
...
<property name="url" value="jdbc:mysql:lllocalhost/testdb" />
...
그리고 경로를 새로 만든 설정파일로 바꿔주면 된다.
@ContextConfiguration(locations="/test-applicationContext.xml")
그 외에 컨테이너 없는 DI 테스트 방식도 있다.