JUnit
가장 좋은 JUnit 테스트 실행 방법은 자바 IDE에 내장된 JUnit 테스트 지원 도구를 사용하는 것 이다.
IDE
IDE에서 지원하는 JUnit 테스트 도구를 통해 얻을 수 있는 정보
테스트 실패 시 얻을 수 있는 정보
빌드 툴
프로젝트의 빌드를 위해 ANT 또는 Maven 과 같은 빌드 툴을 사용시, 빌드 툴에서 제공하는 JUnit 플러그인 또는 태스크를 이용해 JUnit 테스트를 실행할 수 있다.
테스트 실행 결과는 옵션에 따라 보기 좋게만들어진다.
ex) HTML, 텍스트 파일
빌드 툴을 이용하여 테스트하는 경우
지금까지의 테스트 실행 과정에서의 불편한 점이 있다.
해당 사항에서 생각해볼 문제가 존재한다.
가장 좋은 해결 방법은 테스트 종료 후, 자동으로 테스트가 등록한 데이터를 삭제하여 테스트 이전 상태로 만들어주는 것 이다.
deleteAll()의 getCount()추가
UserDao 클래스에 아래의 함수를 추가해준다.
public void deleteAll() throws SQLException {
Connection c = this.dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("delete from users");
ps.executeUpdate();
ps.close();
c.close();
}
public int getCount() throws SQLException {
Connection c = this.dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("select count(*) from users");
ResultSet rs = ps.executeQuery();
rs.next();
int count = rs.getInt(1);
rs.close();
ps.close();
c.close();
return count;
}
deleteAll() 과 getCount()의 테스트
추가된 기능의 테스트도 만들어주어야 한다.
그런데, 해당 기능들은 독립적인 테스트가 불가능하다.
(User 테이블에 데이터가 존재하여야 가능하기 때문이다.)
그러므로 기존 addAndGet() 테스트를 확장하는 방법을 사용하자.
addAndGet() {
deleteAll() // DB의 모든 데이터 지움
getCount() == 0 ? // 모든 데이터를 지웠으니, 현재 User 테이블의 데이터 개수 : 0
add(user) // 새로운 데이터를 DB에 추가
getCount() == 1? // 새로운 데이터를 추가했으니, 현재 User 테이블의 데이터 개수 : 1
get(user.getId()) // get 테스트 수행
}
@Test
public void addAndGet() throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
// delete All test
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// insert test
User user = new User();
user.setId("Id");
user.setName("test_name");
user.setPassword("Password");
userDao.add(user);
assertThat(userDao.getCount(), is(1));
// select test
User searchedUser = userDao.get(user.getId());
assertThat(searchedUser.getName(), is(user.getName()));
assertThat(searchedUser.getPassword(), is(user.getPassword()));
}
동일한 결과를 보장하는 테스트
단위 테스트는 항상 일관성 있는 결과가 보장돼야 한다.
테스트 메소드는 한 번에 한 가지 검증 목적에만 충실하는 것이 좋다.
getCount() 테스트
테스트 시나리오
@Test
public void count() throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
User user1 = new User("gyumee", "박성철", "springno1");
User user2 = new User("leegw700", "이길원", "springno2");
User user3 = new User("bumjin", "박범진", "springno3");
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
userDao.add(user1);
assertThat(userDao.getCount(), is(1));
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
userDao.add(user3);
assertThat(userDao.getCount(), is(3));
}
addAndGet() 테스트 보완
get()메소드에 대한 테스트 기능을 좀 더 보완해보자.
테스트 시나리오
@Test
public void addAndGet() throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
// delete All test
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// insert test
User user1 = new User("gyumee", "박성철", "springno1");
User user2 = new User("leegw700", "이길원", "springno2");
userDao.add(user1);
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
// select test
User searchedUser1 = userDao.get(user1.getId());
assertThat(searchedUser1.getName(), is(user1.getName()));
assertThat(searchedUser1.getPassword(), is(user1.getPassword()));
User searchedUser2 = userDao.get(user2.getId());
assertThat(searchedUser2.getName(), is(user2.getName()));
assertThat(searchedUser2.getPassword(), is(user2.getPassword()));
}
get() 예외조건에 대한 테스트
get() 메소드에 전달된 id값에 해당사는 user 정보가 없을때를 고려해보자.
해결 방법
2번의 방법을 사용해서 예외처리를 해보자.
예외처리에 대한 테스트는 이전 테스트 방법과는 다르다.
이를 위해 JUnit은 예외조건 테스트를 위한 특별한 방법을 제공해준다.
@Test()의 속성 중 expected 에 해당 예외 클래스를 설정해주면 된다.
@Test(expected = EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
userDao.get("unknown_id");
}
EmptyResultDataAccessException 을 위해, spring-dao.jar 파일을 import 해주자
해당 테스트 실행시, 당연히 테스트는 실패한다.
get() 메소드에서 쿼리 결과의 첫 번째 로우를 가져오게 하는 rs.next()를 실행할 때,
가져올 로우가 없다는 SQLException이 발생한다.
테스트를 성공시키기 위한 코드의 수정
get() 메소드에 id에 해당하는 데이터가 없으면 EmptyResultDataAccessException 예외를 던지도록 수정해주자.
public User get(String id) throws SQLException {
...
User user = null;
if (rs.next()) {
user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
}
rs.close();
ps.close();
c.close();
if (user == null) {
throw new EmptyResultDataAccessException(1);
}
return user;
}
포괄적인 테스트
테스트를 만들때 생각해야할 점
DAO의 메소드에 대한 포괄적인 테스트를 만들어두는 편이 훨씬 안전하고 유용하다.
즉, 사소한 부분에 대해서도 테스트를 만들어두자!
테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는 게 좋다.
테스트 개발 을 위한 설계 문서
TDD 의 특징
테스트 코드 클래스를 살펴보면, 모든 테스트에 반복적인 부분이 존재한다.
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
중복된 코드는 별도의 메소드로 뽑아내는 것이 가장 손쉬운 방법이다.
하지만, 이번에는 JUnit이 제공하는 기능을 활용해보겠다.
JUnit 은 테스트를 실행할 때마다 반복되는 준비 작업을 병도의 메소드에 넣게 해주고, 이를 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜 주는 기능이 있다.
먼저 세 개의 테스트 메소드에서 반복적으로 등장하는 앞의 코드를 제거한다.
@Before
중복됐던 코드를 setUp()이라는 메소드를 생성하여 넣어준다.
userDao 변수가 로컬 변수였기 때문에, 테스트 메소드에서 접근 가능하도록 인스턴스 변수로 변경한다.
public class main {
private UserDao userDao;
@Before
public void setUp() {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
this.userDao = context.getBean("userDao", UserDao.class);
}
@Test
public void addAndGet() throws SQLException {...}
@Test
public void count() throws SQLException {...}
@Test(expected = EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException {...}
}
JUnit이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식은 다음과 같다.
실제로는 이보다 더 복잡하다.
보통 하나의 테스트 클래스 안에 있는 테스트 메소드들은 공통적인 준비 작업 - @Before 과 공통적인 정리 작업 - @After가 필요하다.
대신 테스트 메소드와 서로 주고받을 정보나 오브젝트가 있다면, 인스턴스 변수를 이용해야 한다.
각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다.
fixture (픽스처)
테스트를 수행하는 데 필요한 정보나 오브젝트
일반적으로 픽스처는 여러 테스트에서 반복적으로 사용된다. 그러므로 @Before를 통해 메소드를 생성하여 사용하면 편리하다.
User 오브젝트의 생성 과정을 @Before 메소드와 인스턴스 변수를 이용해 분리해보자.
public vlass main {
private UserDao userdao;
private User user1;
private User user2;
private USer user3;
@Before
public void setUp() {
...
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
}
소스코드 : github