2022. 6. 3. 11:45ㆍ토비의 스터디
JUnit 프레임워크를 자세히 살펴본다. 스프링의 핵심 기능 중 하나인 스프링 테스트 모듈도 JUnit을 이용한다. 따라서 JUnit은 스프링을 활용하려면 꼭 사용할 줄 알아야 한다.
2.3.1 JUnit 테스트 실행 방법
IDE와 빌드툴로 테스트 실행이 가능한데, IDE가 손쉽고 빠른 테스트를 제공한다. 실행 방법은 따로 포스팅 할 것이 없어 해당 절은 생략한다.
2.3.2 테스트 결과의 일관성
JUnit을 적용해서 테스트 코드를 만들었으나, 아직 개선할 점이 남아있다.
가장 불편했던 점은, 매번 테스트를 실행하기 전에 DB의 USER 테이블 데이터를 지워야 했다는 것이다.
깜빡 잊고 그냥 실행한 경우, 등록된 사용자 정보와 기본키가 중복된다면서 add() 메소드 실행중에 에러가 발생할 것이다.
이 경우 테스트가 외부 상태에 따라 성공하기도, 실패하기도 한다는 점이다. 지금처럼 별도의 준비작업 없이는 테스트 여부가 달라진다면 좋은 테스트가 아니다.
테스트 한 개를 마치고 나면 테스트가 등록한 데이터를 삭제해서 테스트를 수행하기 이전 상태로 만들어주어야 한다. 그러면 매번 테스트에서 동일한 결과를 얻을 수 있다.
deleteAll()의 getCount() 추가
일관성 있는 테스트를 위해 deleteAll() 메소드를 추가한다. USER 테이블의 모든 레코드를 삭제해주는 간단한 기능이 있다.
public void deleteAll() throws SQLException{
Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("delete from users");
ps.executeUpdate();
ps.close();
c.close();
}
User 테이블의 레코드 개수를 돌려주는 getCount() 메소드도 추가한다.
public int getCount() throws SQLException {
Connection c = 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() 메소드는 USER 테이블의 내용을 모두 삭제하므로 이 메소드를 테스트가 시작될 때 실행되도록 하자.
deleteAll() 메소드가 정상 작동한다면 getCount()의 결과가 0이 나와야 한다. 아래와 같이 테스트를 진행한다.
public class UserDaoTest {
@Test
public void addAndGet() throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
dao.deleteAll();
assertThat(dao.getCount(), is(0));
User user = new User();
user.setId("kiseo");
user.setName("기서");
user.setPassword("1234");
dao.add(user);
assertThat(dao.getCount(), is(1));
User user2 = dao.get(user.getId());
assertThat(user2.getName(), is(user.getName()));
assertThat(user2.getPassword(), is(user.getPassword()));
}
}
deleteAll() 직후에 카운트가 0, add() 직후에 카운트가 1이 나오면 된다.
동일한 결과를 보장하는 테스트
deleteAll() 메소드 덕분에 일관성 있는 결과가 보장되었다. 단위 테스트는 코드가 바뀌지 않는다면 매번 실행할 때마다 동일한 테스트 결과를 얻을 수 있어야 한다. 현재는 테스트 시작 전에 DB의 모든 데이터를 삭제하고 있다. 이 방법보다는 테스트 전에 테스트 실행에 문제가 되지 않는 상태를 만들어주는 편이 더 바람직하다.
스프링은 DB를 사용하는 코드를 테스트할 경우 매우 편리한 방법을 제공한다. 차차 알아보자..
2.3.3 포괄적인 테스트
위의 테스트를 좀 더 꼼꼼하게 진행한다.
@Test
public void count() throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
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));
}
addAndGet() 와 count()의 테스트를 진행한다. 두 개의 테스트는 모두 성공한다.
다만 주의할 점은 두 개의 테스트가 어떤 순서로 실행될지는 알 수 없다.
따라서 테스트의 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘못 만든 것이다. 모든 테스트는 실행 순서에 무관하게 독립적으로 동일한 결과를 내야 한다.
addAndGet() 테스트 보완
현재 테스트에는 id로 사용자를 검색하는 get() 메소드에 대한 테스트가 부족하다. get() 메소드에 대한 테스트 기능을 보완해보자.
@Test
public void andAndGet() throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
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.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
User userget2 = dao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
이렇게 하면 주어진 id에 해당하는 정확한 User 정보를 가져오는지 확인할 수 있다.
get() 예외조건에 대한 테스트
만약 get() 메소드에 전달된 id 값에 해당하는 사용자가 없다면 어떻게 될까??
null과 같은 특별한 값을 리턴하거나, id에 해당하는 정보를 찾을 수 없다고 예외를 던질 수 있다. 각자 장단점이 있는데 책에는 예외를 던지는 경우가 기술되어 있다.
get() 메소드에서 쿼리 실행 시 결과가 없으면 스프링이 기본 제공하는 EmptyResultDataAccessException 예외를 던지도록 만들어본다. 일반적으로는 테스트 중에 예외가 발생하면 메소드 실행은 중단되고 테스트는 실패한다. 즉 예외 발생 여부는 메소드 실행의 리턴 값으로는 확인할 수 없다는 뜻이다.
이 경우 JUnit은 예외 조건 테스트를 위한 특별한 방법을 제공한다. 테스트 결과 EmptyResultDataAccessException 이 던져지면 성공하는 테스트를 작성해본다.
@Test(expected=EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
dao.deleteAll();
assertThat(dao.getCount(), is(0));
dao.get("unknown_id");
}
@Test에 expected를 추가해서 지정한 예외가 던져지면 성공하도록 할 수 있다.
나는 JUnit5를 사용중인데
import org.assertj.core.api.Assertions;
Assertions의 assertThatThrownBy() 메소드를 사용한다.
Assertions.assertThatThrownBy(()-> System.out.println("예외가 터지는 코드"),
"예외 발생", EmptyResultDataAccessException.class);
파라미터로 예외가 발생하는 람다식, 메시지, 발생하리라 기대하는 예외 클래스를 적어주면 된다.
이제 EmptyResultDataAccessException를 던지는 get() 메소드를 만들고 테스트를 실행하면 성공한다.
/*
rs.next();
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
*/
//아래의 코드로 변경해 예외를 발생
User user = null;
if (rs.next()) {
user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
}
...
if (user == null) throw new EmptyResultDataAccessException(1);
2.3.4 테스트가 이끄는 개발
get() 메소드의 예외 테스트를 만드는 과정에서 테스트를 먼저 만들고, 테스트가 실패하자 UserDao의 get()메소드를 수정했다. 테스트 할 코드도 없는데 테스트 코드부터 만드는 것이 처음에는 이상해보여도 이는 개발자들이 적극적으로 사용하고 있는 방법이다.
기능설계를 위한 테스트
작업을 돌이켜보면 만들어진 코드를 보고 어떻게 테스트할지 결정한 것이 아니라 테스트를 먼저 작성했다.
그리고 테스트에는 만들고 싶은 기능에 대한 조건, 행위, 결과에 대한 내용이 잘 담겨있다.
- 조건 - 가져올 사용자 정보가 존재하지 않은 경우에 : dao.deleteAll()
- 행위 - 존재하지 않는 id로 get()을 실행하면 : get("unknown_id");
- 결과 - 특별한 예외가 던져진다. : @Test(expected=EmptyResultDataAccessException.class)
테스트가 하나의 기능정의서처럼 보인다.
결국 테스트가 성공한다면 그 순간 코드 구현과 테스트라는 두가지 작업이 동시에 끝나는 것이다.
테스트 주도 개발
지금까지 설명한 개발 방식을 테스트 주도 개발(TDD, Test Driven Development)이라고 한다.
코드를 만들고 나서 시간이 많이 지나면 테스트를 만들기가 귀찮아지는 경우가 많다.
또, 작성한 코드가 많아질수록 무엇을 테스트해야 할지 막막하다.
TDD는 테스트를 먼저 만들기 때문에 테스트를 빼먹지 않고 꼼꼼하게 만들어낼 수 있다!
엔터프라이즈 애플리케이션의 테스트가 만들이 매우 어렵다고 생각될 수 있다. 하지만 스프링은 테스트를 쉽고 빠르게 작성할 수 있는 편리한 기능들을 많이 제공하므로 TDD가 가능하다.
2.3.5 테스트 코드 개선
JUnit 프레임워크는 테스트 메소드를 실행할 때 부가적으로 해주는 작업이 몇 가지 있다. 그 중 테스트 실행마다 반복되는 준비 작업을 별도의 메소드에 넣게 해주고, 이를 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜주는 기능을 소개한다.
먼저 세 개의 테스트 메소드에 반복 등장하는 코드를 제거한다.
@Before
중복 코드를 setUp() 이라는 메소드에 옮겨주자.
public class UserDaoTest {
private UserDao dao;
@Before
public void setUp() {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
this.dao = context.getBean("userDao", UserDao.class);
}
@Test
public void andAndGet() throws SQLException {
...
}
...
}
setUp()메소드에 추가한 @Before 애노테이션은 각각의 테스트 메소드가 실행하기 전에 실행된다.
알다시피 JUnit은 스스로 제어권, 실행 흐름을 갖고 있는 프레임워크다. @Before 애노테이션이 동작하는 순서는 다음과 같다. JUnit의 실행 흐름을 간단히 살펴보자.
- 테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다.
- 테스트 클래스의 오브젝트를 하나 만든다.
- @Before가 붙은 메소드가 있으면 실행한다.
- @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장한다.
- @After가 붙은 메소드가 있으면 실행한다.
- 나머지 테스트 메소드에 대해 2~5번을 반복한다.
- 테스트 결과를 종합해 돌려준다.
테스트 클래스 안에 있는 공통적인 준비 작업, 정리작업은 @Before, @After를 사용하면 JUnit이 자동으로 실행해주니 매우 편리하다. 각 테스트 메소드에서 setUp()을 직접 호출할 필요도 없다.
대신 서로 주고받을 정보나 오브젝트가 있다면 인스턴스 변수를 이용해야 한다. 여기서는 UserDao 오브젝트를 인스턴스 변수 dao에 저장해뒀다가 각 테스트 메소드에서 사용하게 만들었다.
또 주의할 점은 각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다는 점이다. 한번 만들어진 테스트 클래스의 오브젝트는 한 개의 테스트 메소드 실행 후 버려진다. 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해 이렇게 설계되었다.
픽스처
테스트를 수행하는 데 필요한 정보나 오브젝트를 픽스처라고 한다. 일반적으로 픽스처는 여러 테스트에서 반복적으로 사용되기 때문에 @Before 메소드를 이용해 생성해 두면 편리하다.
UserDaoTest.java 에서는 dao가 대표적인 픽스처이다.
public class UserDaoTest {
private UserDao dao;
private User user1;
private User user2;
private User user3;
@Before
public void setUp() {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
this.dao = context.getBean("userDao", UserDao.class);
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
이렇게 user1,2,3 도 픽스처로 재구성할 수 있다. 각 테스트 메소드에서는 인스턴스 변수 user1,2,3을 생성하는 코드 없이 사용하기만 하면 된다.
'토비의 스터디' 카테고리의 다른 글
[토비의 스프링 3.1] 2.4 스프링 테스트 적용 (0) | 2022.06.30 |
---|---|
[토비의 스프링 3.1] 2.2 UserDaoTest 개선 (0) | 2022.06.09 |
[토비의 스프링 3.1] 2.1 UserDaoTest 다시 보기 (0) | 2022.06.01 |
[토비의 스프링 3.1] 1.7 의존관계 주입(DI) (0) | 2022.05.28 |
[토비의 스터디 3.1] 1.6 싱글톤 레지스트리와 오브젝트 스코프 (0) | 2022.05.27 |