목차
2.3.1 JUnit 테스트 실행 방법
JUnitCore를 이용해 테스트를 실행할 수도 있지만, 테스트의 수가 많아지고 복잡해지면 관리하기가 힘들어짐
IDE
이클립스, STS는 JUnit 테스트를 지원한다.
- @Test 애노테이션이 있는 클래스 선택
- [Run] - [Run As] - [JUnit Test] 선택
- main() 메소드 없어도 무관
- 패키지 선택 후 JUnit Test를 실행할 경우, 해당 패키지 내 모든 @Test 메소드를 실행한다.
- 단축키 : Alt + Shift + X,T
빌드 툴
ANT 또는 Maven 등의 빌드 툴과 스크립트를 사용할 경우, 빌드 툴에서 제공하는 JUnit 플러그인과 태스크 이용가능
- HTML 또는 텍스트 파일로 보기 좋게 생성됨
- 메일 등으로 통보받는 것도 가능
- 통합 테스트 시 유용하며, 개발자 개인별로는 IDE에서 하는 것이 가장 편리
2.3.2 테스트 결과의 일관성
여전히 UserDaoTest 실행 시 마다 DB의 User 테이블을 정리해주어야 한다.
- 외부 상태(DB)에 따라 테스트가 성공하기도 하고, 실패하기도 한다.
- 코드에 변경사항이 없다면 테스트는 항상 동일한 결과를 내야한다.
- 해결방법 : 테스트 후 테스트 이전 상태로 만들어주는 것
deleteAll()의 getCount() 추가
새로운 기능 추가
deleteAll()
User 테이블의 모든 레코드를 삭제
public void deleteAll() throws SQLException {
Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("DELETE FROM users WHERE 1=1");
ps.executeUpdate();
ps.close();
c.close();
}
getCount()
User 테이블의 레코드 개수를 돌려줌
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()과 getCount()의 테스트
추가된 기능에 대한 테스트 필요
- 별도의 메소드를 만들기는 애매
- 기존의 addAndGet() 테스트를 확장
- addAndGet() 메소드 시작 시 deleteAll()을 실행
- 그러나 아직 deleteAll()에 대한 검증이 되지 않음
- deleteAll()이 정상적으로 작동한다면 deleteAll() 이후 getCount()는 0이 되어야 함.
- getCount()도 검증이 되지 않음
전체적인 Flow
- addAndGet() 실행
- deleteAll() 실행
- getCount() == 0 확인
- add() 실행
- getCount() == 1 확인
- add() 오브젝트와 get() 오브젝트의 동일성 확인
UserDaoTest.java
public class UserDaoTest {
@Test
public void addAndGet() throws SQLException {
// ...
dao.deleteAll();
assertThat(dao.getCount(), is(0));
dao.add(user);
assertThat(dao.getCount(), is(1));
// ...
}
}
동일한 결과를 보장하는 테스트
테스트를 실행하면 성공하는 것을 볼 수 있다. User DB를 정리하지 않고, 여러번 실행하여도 잘 실행된다.
- addAndGet() 테스트 종료 직전에 지워주는 방법도 있다.
- 그러나 이 방식은 addAndGet() 테스트 실행 이전에 다른 데이터가 User에 들어가 있다면 실패할 수도 있다. getCount가 맞지 않겠지?
- 당연히 테스트 DB 이어야한다.
운영 DB면..
2.3.3 포괄적인 테스트
데이터를 여러번 넣어도 getCount()가 잘 작동할까?
0,1 두가지를 해봤으니 잘 되겠지?- 제대로 만들지 않은 테스트는 하지 않는 것보다 못하다.
getCount 테스트
좀 더 꼼꼼한 테스트가 필요하다.
- User를 여러번 등록하면서 매번 getCount()의 기능을 테스트
- 기존의 addAndGet()보다는 새로운 테스트를 만드는 것이 더 좋다.
- 테스트 역시 한 번에 한 가지 목적에 충실해야한다.
테스트 시나리오
- deleteAll() 호출 후 getCount() 값이 0인지 확인
- 3개의 User 정보를 입력하면서 getCount() 결과가 1씩 증가하는지 확인
UserDao.java
- User 오브젝트를 여러번 만들어야 하므로 생성자를 만들어두면 편리하다.
- 생성자를 명시적으로 추가하였을 경우, 디폴트 생성자를 함께 정의해주어야 한다. (자바빈 규약)
public class User {
// ...
public User() {
}
public User(String id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
}
UserDaoTest.java
public class UserDaoTest {
@Test
public void addAndGet() throws SQLException, ClassNotFoundException {
// ...
User user = new User("whiteship", "백기선", "married");
// ...
}
}
코드를 수정하였으므로 테스트를 해보자.
테스트가 성공했다면 위의 테스트 시나리오대로 getCount()를 생성하자.
getCount.java
public class getCount {
@Test
public void count() throws SQLException {
// ...
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()에 대한 테스트는 주어진 id에 해당하는 사용자인지, 그냥 아무나 가져온 것인지에 대한 확신이 없다.
- User 2명을 각각 등록하고, 각 User의 id를 파라미터로 전달해서 get()을 실행한 후 테스트
public class UserDaoTest {
@Test
public void addAndGet() throws SQLException {
// ...
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()));
}
// ...
}
get() 예외조건에 대한 테스트
get() 메소드에 전달된 id 값에 해당하는 사용자 정보가 없다면 어떻게 될까?
- null과 같은 특별한 값을 리턴하는 방법
- id에 해당하는 값을 찾을 수 없다고 예외를 던지는 방법
예외를 던지는 방법을 사용
- 예외 클래스를 정의 (스프링이 미리 정의해둔 예외 사용, EmptyResultDataAccessException)
- UserDao의 get() 메소드에서 쿼리를 실행한 결과 아무것도 없으면 해당 예외를 던짐
테스트를 먼저 작성해보자.
- 일반적으로 테스트는 예외가 던져지면 테스트는 중단되고, 테스트는 실패한다. (테스트 에러)
- 그러나 이 경우에는 특정 예외가 던져져야만 성공하는 테스트이다.
- 따라서 asserThat()으로 결과값을 비교할 수 없다.
- 스프링에서는 테스트 중 발생할 것으로 기대하는 예외 클래스를 지정할 수 있다.
- 일단 테스트 메소드를 추가하고, 모든 데이터를 지운 후 존재하지 않는 id로 get()을 호출해보자.
- 이 때 EmptyResultDataAccessException이 발생하면 성공, 아니라면 실패다.
public class UserDaoTest {
// 테스트 중 발생할 것으로 기대되는 예외 클래스를 지정
@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 엘리먼트를 사용하면 테스트 메소드 실행 중 발생할 것으로 기대되는 예외 클래스를 지정할 수 있다.
- 보통의 테스트와는 반대로 정상적으로 테스트를 마치면 테스트가 실패하고, expected에서 지정한 예외가 던져지면 성공한다.
- 현재, 테스트는 실패한다. 아직 UserDao 코드에 손을 대지 않았으므로 rs.next() 실행 시 가져올 row가 없어서 SQLException이 발생한다.
테스트를 성공시키기 위한 코드의 수정
테스트가 성공하도록 get() 메소드 코드를 수정하여야 한다.
UserDao.java
public class UserDao {
public User get(String id) throws SQLException {
// ...
// User는 null로 초기
User user = null;
// 쿼리 결과가 있을 경우만 User 오브젝트 생성 후 값을 넣어준다.
if (rs.next()) {
user = new User();
user.setId(rs.getString("ID"));
user.setName(rs.getString("NAME"));
user.setPassword(rs.getString("PASSWORD"));
}
// ...
// 결과가 없다면 User는 계속 null일 것이다.
if (user == null) {
// 예외를 던져준다.
throw new EmptyResultDataAccessException(1);
}
return user;
}
}
세 개의 테스트는 모두 성공할 것이다. 새로 추가한 기능도 정상적으로 동작하고, 기존의 기능에도 영향을 주지 않았다는 확신을 얻을 수 있다.
포괄적인 테스트
이 정도의 간단한 DAO의 경우, 다양한 테스트를 하지 않더라도 문제가 생기지 않을 것이라고 자신할 수 있는 개발자가 있을지도 모른다. 하지만 포괄적인 테스트를 만들어두면 나중에 문제가 생겼을 경우에도 원인을 찾기에도 편리하다.
테스트 중 자주하는 실수 : 성공하는 테스트만 골라서 만드는 것
- '내 PC에서는 잘 되는데....' 와 동일
- 제대로 만들지 않은 테스트는 없는 것보다 못하다.
- 테스트 코드 작성에 대한 공부도 필요
- "항상 네거티브 테스트를 먼저 만들라" - 로드 존슨
2.3.4 테스트가 이끄는 개발
2.3.3 후반부에 코드를 수정 후 테스트를 만든 것이 아니라 테스트를 먼저 작성하고 나서, 테스트가 실패하는 것을 보고 난 후 코드를 수정하였다.
기능설계를 위한 테스트
작업 순서
- 만들어야 할 기능을 결정 (존재하지 않는 id로 get()메소드를 실행하면 특정 예외가 던져져야 한다.)
- getUserFailure() 테스트를 작성
- 테스트할 코드가 없는데 어떻게 테스트를 먼저 작성하지?
- 추가하고 싶은 기능을 코드로 표현했기에 가능
단계 | 내용 | 코드 | |
---|---|---|---|
조건 | 어떤 조건을 가지고 | 가져올 사용자 정보가 존재하지 않는 경우 | dao.deleteAll(); asserThat(dao.getCount),is(0)); |
행위 | 무엇을 할 때 | 존재하지 않는 id로 get()을 실행하면 | get("unknown_id"); |
결과 | 어떤 결과가 나온다 | 특별한 예외가 던져진다 | @Test(expected=EmptyResultDataAccessException.class) |
테스트 코드는 마치 하나의 기능 정의서와 같다.
- 테스트가 실패하면 설계한 대로 코드가 작성되지 않았음을 알 수 있음
- 코드를 수정하고 테스트를 수행하는 과정을 반복한다.
- 테스트가 성공한다면 코드 구현과 테스트라는 두 가지 작업이 동시에 끝난다.
테스트 주도 개발
테스트 주도 개발(TDD; Test Driven Development)
만들고자 하는 기능의 내용을 담고 있으면서 만들어진 코드를 검증해 줄 수 있도록 테스트 코드를 먼저 작성하고, 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발 방법
테스트 우선 개발(TFD; Test First Development)라고도 함.
개발자가 테스트를 만들어가면 개발하는 방법이 주는 장점을 극대화
실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다. - TDD 기본원칙
개발을 하다보면 테스트를 만들 타이밍을 놓치는 경우가 있다.
- 빨리 기능을 완성하고 싶다는 욕구
- 한 번 집중하면 정신없이 빠져드는 습성
- 코드를 다 만들고 시간이 지나면 테스트 만들기가 귀찮음
TDD의 장점
- 테스트를 빼먹지 않고 꼼꼼하게 만들어 낼 수 있다.
- 테스트를 작성하는 시간과 애플리케이션 코드를 작성하는 시간의 간격이 짧아진다.
- 자연스럽게 단위 테스트를 만들 수 있다.
- 머릿속에서 진행되는 시뮬레이션을 코드로 작성한다.
2.3.5 테스트 코드 개선
테스트 코드도 리팩토링이 필요하다.
- context와 dao를 가져오는 부분이 반복되고 있다.
- 중복된 코드는 메소드로 분리하는 것이 가장 손쉬운 방법이다.
- 테스트의 경우, JUnit이 제공하는 기능을 사용할 수도 있다.
@Before
@Before 애노테이션을 사용하면 테스트 실행 전 준비작업을 분리할 수 있다.
- 중복됐던 코드를 setUp()이라는 메소드에 넣고, 중복된 코드는 제거한다.
- dao는 로컬 변수였으므로 인스턴스 변수로 바꾸어준다.
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 addAndGet() throws SQLException { // ...
}
@Test
public void count() throws SQLException {
// ...
}
@Test(expected=EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException {
// ...
}
}
JUnit의 테스트 실행 과정
- 테스트 클래스에서 @Test가 붙어있는 public void 형의 파라미터가 없는 테스트 메소드를 모두 찾는다.
- 테스트 클래스의 오브젝트를 하나 만든다.
- @Before가 붙은 메소드를 먼저 실행한다.
- @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장한다.
- @After가 붙은 메소드를 실행한다.
- 2~5를 반복한다.
- 모든 테스트 결과를 종합해서 돌려준다.
@Before/@After
- 자동으로 실행되므로 메소드를 직접 호출할 필요가 없다.
- 직접 호출하지 않으므로 인스턴스 변수를 이용해야 한다.
JUnit의 테스트 클래스 오브젝트는 매번 새로 만들어진다.
- 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 보장
- 따라서 인스턴스 변수도 부담없이 사용할 수 있다.
테스트 메소드 일부에서만 사용(중복)되는 코드가 있다면?
- Before이 아닌 새로운 메소드로 분리하여 직접 호출하는 것이 낫다.
- 혹은 공통적인 특징을 지닌 테스트 메소드를 모아 별도의 테스트 클래스로 만들수도 있다.
픽스쳐
픽스쳐
테스트를 수행하는 데 필요한 정보나 오브젝트
일반적으로 픽스쳐는 여러 테스트에서 반복적으로 사용되기 때문에 @Befor 메소드를 이용해서 만들어두면 편리하다. (ex. dao)
- add() 메소드에 전달되는 User오브젝트들도 픽스처라고 볼 수 있다.
- 해당 부분에도 중복이 발생
- getUserFailure()에서는 사용되지 않지만 확장성을 고려하여 @Before에서 생성하는 것이 더 좋을 것으로 보임
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");
}
}
' Spring > 토비의 스프링 3.1' 카테고리의 다른 글
2.5 학습 테스트로 배우는 스프링 (0) | 2019.01.17 |
---|---|
2.4 스프링 테스트 적용 (0) | 2019.01.17 |
2.2 UserDaoTest 개선 (0) | 2019.01.11 |
2.1 UserDaoTest 다시보기 (0) | 2019.01.11 |
2장. 테스트 (0) | 2019.01.11 |