[토비의 스프링] 2.3 개발자를 위한 테스팅 프레임워크 JUnit
지금까지 테스트를 실행하면서 가장 불편한 점은, 매번 UserDaoTest 태스트를 실행하기 위해 직접 데이터베이스를 모두 비워줬던 것이다.
그냥 실행하면 기본키가 충돌하기 때문이다.
테스트가 외부 상태에 따라 성공하기도 하고 실패하기도 하는 것이 좋은 방법일까?
당연히 테스트는 별도의 준비 작업 없이 반복적으로 수행해도 일관되게 성공해야 한다.
그렇기에 테스트를 마치고 나면 테스트에서 삽입한 데이터를 삭제하여, 테스트를 수행하기 이전 상태로 만들어줘야 한다.
일단 UserDao에 다음과 같은 메서드를 추가해보자.
public void deleteAll() throws SQLException, ClassNotFoundException {
Connection connection = connectionHelper.makeConnection();
PreparedStatement preparedStatement = connection.prepareStatement("delete from users");
preparedStatement.executeUpdate();
preparedStatement.close();
connection.close();
}
public int getCount() throws ClassNotFoundException, SQLException {
Connection connection = connectionHelper.makeConnection();
PreparedStatement preparedStatement = connection.prepareStatement("select count(*) from users");
var resultSet = preparedStatement.executeQuery();
resultSet.next();
int count = resultSet.getInt(1);
resultSet.close();
preparedStatement.close();
connection.close();
return count;
}
그리고는 테스트에 다음과 같이 추가해본다.
class UserDaoTest {
@Test
public void addAndGet() throws SQLException, ClassNotFoundException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
userDao.deleteAll();
Assertions.assertEquals(0, userDao.getCount());
User createdUser = new User();
createdUser.setId("seungkyu");;
createdUser.setName("승규");
createdUser.setPassword("password");
userDao.add(createdUser);
Assertions.assertEquals(1, userDao.getCount());
User readUser = userDao.get("seungkyu");
Assertions.assertEquals(createdUser.getId(), readUser.getId());
Assertions.assertEquals(createdUser.getName(), readUser.getName());
Assertions.assertEquals(createdUser.getPassword(), readUser.getPassword());
}
}
이 테스트가 좋은 테스트는 아니라고 생각한다.
하나의 테스트에서 너무 많은 메서드를 테스트한다.
만약 테스트를 작성하면 이렇게 너무 많은 메서드를 하나의 테스트에서 진행하지는 말도록 하자.
이런 경우에는 어디서 문제가 생겼는지 파악하기 힘들어진다.
이제 테스트를 반복 실행해보아도, 동일하게 성공하는 것을 볼 수 있다.
getCount() 메서드를 테스트에 적용했지만, 기존의 테스트에서 확인할 수 있었던 것은 deleteAll()을 실행했을 때 테이블이 비어있는 경우와 add()를 한번 호출한 뒤의 결과만 테스트했다.
2개 이상의 add()도 물론 잘 작동하겠지만, 그래도 테스트 해보는 것이 더 좋은 자세이다.
그렇기에 다른 메서드부터 차례로 꼼꼼하게 테스트 해보도록 하자.
여러개의 User를 등록하며 getCount()를 호출하는 것이다.
@Test
public void count() throws SQLException, ClassNotFoundException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
userDao.deleteAll();
for(int i = 0; i < 3; i++){
User user = new User();
user.setId(UUID.randomUUID().toString().substring(0, 6));
user.setName(UUID.randomUUID().toString().substring(0, 6));
user.setPassword(UUID.randomUUID().toString().substring(0, 6));
userDao.add(user);
Assertions.assertEquals(i + 1, userDao.getCount());
}
}
데이터를 모두 지우고 하나씩 추가하며 개수를 세어보니, 잘 동작하는 것을 볼 수 있다.
이렇게 get() 메서드가 제대로 동작한다고 믿을 수 있게 되었다.
만약 아이디가 충돌해서 에러가 생긴다면 어떨까?
이렇게 말이다.
당연히
에러가 발생해도 테스트를 통과시킬 수 있도록 하는 방법이 있다.
@Test
public void countWithDuplicate() throws SQLException, ClassNotFoundException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
userDao.deleteAll();
String name = UUID.randomUUID().toString().substring(0, 6);
User user = new User();
user.setId(name);
user.setName(UUID.randomUUID().toString().substring(0, 6));
user.setPassword(UUID.randomUUID().toString().substring(0, 6));
userDao.add(user);
for(int i = 0; i < 2; i++){
user.setId(name);
user.setName(UUID.randomUUID().toString().substring(0, 6));
user.setPassword(UUID.randomUUID().toString().substring(0, 6));
Assertions.assertThrows(SQLIntegrityConstraintViolationException.class,
() -> userDao.add(user));
Assertions.assertEquals(1, userDao.getCount());
}
}
대신 기대했던 에러가 발생하지 않는다면, 해당 테스트는 실패한다.
예외가 반드시 발생하는 경우에 사용할 수 있다.
이제 마지막으로 할 일은 get() 메서드의 테스트를 완료하는 것이다.
우선 get()에서 유저가 존재하지 않는다면 NPE를 발생하도록 수정했다.
public User get(String id) throws ClassNotFoundException, SQLException {
Connection connection = connectionHelper.makeConnection();
PreparedStatement preparedStatement = connection.prepareStatement(
"select * from users where id = ?"
);
preparedStatement.setString(1, id);
var resultSet = preparedStatement.executeQuery();
User user = null;
if(resultSet.next()){
user = new User();
user.setId(resultSet.getString("id"));
user.setName(resultSet.getString("name"));
user.setPassword(resultSet.getString("password"));
}
resultSet.close();
preparedStatement.close();
connection.close();
if(user == null){
throw new NullPointerException("user is null");
}
return user;
}
그리고 없는 계정으로 조회할 때 에러가 잘 발생하는지 테스트를 해보았다.
@Test
public void get() throws SQLException, ClassNotFoundException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
Assertions.assertThrows(NullPointerException.class, () -> userDao.get(UUID.randomUUID().toString().substring(0, 6)));
}
자 이제 테스트코드를 리펙토링 해보자.
지금 테스트 코드에서는 데이터베이스에 연결하는 공통적인 코드를 가지고 있다.
항상 테스트의 시작 부분에서 작성된 코드이다.
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
userDao.deleteAll();
이 부분을 추출할 수는 없을까?
당연히 가능하다.
JUnit에서는 공통 시작 부분을 추출할 수 있도록 @BeforeEach annotation을 지원한다.
해당 클래스의 모든 테스트에서 시작 전에 수행되어야 할 명령어를 명시해두면 된다.
@BeforeEach
public void setUp() throws SQLException, ClassNotFoundException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
this.userDao = context.getBean("userDao", UserDao.class);
userDao.deleteAll();
}
당연히 메서드의 종료 후 동작할 명령어도 명시할 수 있다.
@AfterEach
public void tearDown(){
System.out.println("테스트 종료");
}
이렇게 테스트를 수행할 때에는 JUnit에서 각각의 테스트 메서드에 대하여, 각각의 오브젝트가 생성된다고 한다. 그렇기 때문에 각 테스트끼리는 정보를 인스턴스를 제외하고는 주고받을 수 없다.