2장-테스트
2장 테스트
스프링이 개발자에게 제공하는 가장 중요한 가치는 객체지향
과 테스트
이다.
애플리케이션은 계속 변하고 복잡해져간다.
첫번째 전략은 확장과 변화를 고려한 객체지향적 설계
와 그것을 효과적으로 담아낼 수 있는 IoC/DI
같은 기술이라면,
두번째 전략은 만들어진 코드를 확신할 수 있게 해주고, 변화에 유연하게 대처할 수 있는 자신감을 주는 테스트 기술
이다.
또한, 테스트
는 스프링을 학습하는데 있어 가장 효과적인 방법의 하나
다.
2장에서는 테스트란 무엇
이며, 그 가치와 장점
, 활용전략
, 스프링과의 관계
를 살펴본다.
그리고 대표적인 테스트 프레임워크를 소개
하고, 이를 이용한 학습전략
도 알아볼 것이다.
2.1 테스트의 유용성
테스트는 결국 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인해서, 만든 코드를 확신할 수 있게 해주는 작업이다.
2.1.2 UserDaoTest의 특징
이 테스트 코드의 내용을 정리해보면 다음과 같다.
- 자바에서 가장 손쉽게 실행 가능한 main() 메소드를 이용한다.
- 테스트할 대상인 UserDao의 오브젝트를 가져와 메소드를 호출한다.
- 테스트에 사용할 입력 값(User 오브젝트)을 직접 코드에서 만들어 넣어준다.
- 테스트의 결과를 콘솔에 출력해준다.
- 각 단계의 작업이 에러 없이 끝나면 콘솔에 성공 메시지로 출력해준다.
public class UserDaoTest {
public static void main(String[] args) throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext (
"applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
User user = new User();
user.setId("user");
user.setName("백기선");
user.setPassword("married");
dao.add(user);
System.out.println(user.getId() + "등록 성공");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + "조회 성공");
}
}
이 테스트 방법에서 가장 돋보이는건, main() 메소드를 이용해 쉽게 테스트 수행을 가능하게 했다는 점과 테스트할 대상인 UserDao를 직접 호출해서 사용한다는 점이다.
웹을 통한 DAO 테스트 방법의 문제점
- 웹 화면을 통해 값을 입력하고, 기능을 수행하고, 결과를 확인하는 방법은 가장 흔히 쓰이는 방법이지만, DAO에 대한 테스트로서는 단점이 너무 많다.
- DAO뿐만 아니라 서비스 클래스, 컨트롤러, JSP 뷰 등 모든 레이어의 기능을 다 만들고 나서야 테스트가 가능하다는 점이 가장 큰 문제다.
-
테스트를 하는 중에 에러가 나거나 테스트가 실패했다면, 과연 어디에서 문제가 발생했는지를 찾아내야 하는 수고도 필요하다.
- 그렇다면
테스트를 어떻게 만들면 이런 문제를 피할 수 있고, 효율적으로 테스트를 활용할 수 있을지 생각해보자.
작은 단위의 테스트
- 테스트하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트하는 것이 바람직하다.
-
따라서 테스트는 가능하면 작은 단위로 쪼개서 집중해서 할 수 있어야 한다.
-
이렇게 작은 단위의 코드에 대해 테스트를 수행한 것을
단위 테스트(unit test)
라고 한다. - 때로는 웹 사용자 인터페이스부터 시작해서 DB에 이르기까지의 애플리케이션 전 계층이 참여하고, 또 단순 사용자 등록 작업 하나가 아니라, 초기 등록에서부터 시작해서 등록이 성공하면 로그인하고, 각종 기능을 모두 사용한 다음에 로그아웃까지 하는 전과정을 하나로 묶어서 테스트할 필요가 있다.
-
각 단위 기능은 잘 동작하는데 묶어놓으면 안되는 경우가 종종 발생하기 때문이다.
-
따라서 이런 길고 많은 단위가 참여하는 테스트도 언젠가는 필요하다.
- 작은 단위로 나눠서 테스트 하는 단위 테스트가 필요한 이유는
개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는지를 개발자 스스로 빨리 확인받기 위해서다.
자동수행 테스트 코드
- UserDaoTest의 한 가지 특징은
테스트할 데이터가 코드를 통해 제공되고, 테스트 작업 역시 코드를 통해 자동으로 실행한다는 점
이다. - UserDaoTest는
자바 클래스의 main() 메소드를 실행하는 가장 간단한 방법
만으로테스트의 전 과정이 자동을 진행
된다. - 이렇게
테스트
는자동으로 수행되도록 코드로 만들어지는 것이 중요하다.
테스트 자체가 사람의 수작업을 거치는 방법을 사용하기보다는 코드로 만들어져서 자동으로 수행될 수 있어야 한다는건 매우 중요하다
자동으로 수행되는 테스트의 장점
은자주 반복
할 수 있다는 것이다.
지속적인 개선과 점진적인 개발을 위한 테스트
-
만약 처음부터 스프링을 적용하고 XML 설정을 만들고 모든 코드를 다 만들고 난 뒤에 이를 검증하려고 했다면, 아마 쏟아지는 에러 메시지에 기가 질려서 무엇을 해야할지 몰라 막막해졌을지도 모르겠다.
-
그래서 일단은 단순 무식한 방법으로 정상동작하는 코드를 만들고, 테스트를 만들어뒀기 때문에 매우 작은 단계를 거쳐가면서 계속 코드를 개선해나갈 수 있었다.
-
테스트를 이용하면 새로운 기능도 기대한 대로 동작하는지 확인할 수 있을 뿐 아니라, 기존에 만들어뒀던 기능들이 새로운 기능을 추가하느라 수정한 코드에 영향을 받지 않고 여전ㅇ히 잘 동작하는지를 확인할 수도 있다.
2.1.3 UserDaoTest의 문제점
UserDaoTest가 UI까지 동원되는 번거로운 수동 테슽에 비해 장점이 많은 건 사실이다. 하지만 만족스럽지 못한 부분도 있다.
- 수동 확인 작업의 번거로움
- UserDaoTest는 테스트를 수행하는 과정과 입력 데이터의 준비를 모두 자동으로 진행하도록 만들어졌다. 하지만 여전히 사람의 눈으로 확인하는 과정이 필요하다.
- 실행 작업의 번거로움
- 좀 더 편리하고 체계적으로 테스트를 실행하고 그 결과를 확인하는 방법이 절실히 필요하다.
2.2 UserDaoTest 개선
UserDaoTest의 두 가지 문제점을 개선해보자.
2.2.1 테스트 검증의 자동화
첫번째 문제점인 테스트 결과의 검증 부분을 코드로 만들어보자.
수정 전 테스트 코드
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + "조회 성공");
수정 후 테스트 코드
if (!user.getName().equals(user2.getName())) {
System.out.println("테스트 실패 (name)"));
}
else if (!user.getPassword().equals(user2.getPassword())) {
System.out.println("테스트 실패 (password)"));
}
else {
System.out.println("조회 테스트 성공"));
}
- add()를 통한 등록 자체는 별다르게 검증할 것이 없다.
-
get()을 통한 검증 과정에서 사실 add()의 작업도 함께 확인하는 것이다.
- 결국
테스트
란개발자가 마음 편하게 잠자리에 들 수 있게 해주는 것
이다.
2.2.2 테스트의 효율적인 수행과 결과 관리
- main() 메서드로 만든 테스트는 테스트로서 필요한 기능은 모두 갖춘 셈이다.
- 하지만 좀 더 편리하게 테스트를 수행하고 편리하게 결과를 확인하려면 단순한 main() 메서드로는 한계가 있다.
- 일정한 패턴을 가진 테스트를 만들 수 있고
- 많은 테스트를 실행시킬 수 있으며
- 테스트 결과를 종합해서 볼 수 있고
- 테스트가 실패한 곳을 빠르게 찾을 수 있는 기능을 갖춘 테스트 지원 도구
- 테스트 지원 도구에 맞는 테스트 작성방법이 필요하다.
- 이를 위해 프로그래머를 위한
자바 테스팅 프레임워크
라고 불리는JUnit
은자바 테스트 지원 도구
로 활용된다. JUnit
은 이름 그대로자바로 단위 테스트를 만들 때 유용
하게 쓸 수 있다.
JUnit 테스트로 전환
- 프레임워크에서 동작하는 코드는 main() 메소드도 필요 없고 오브젝트를 만들어서 실행시키는 코드를 만들 필요도 없다.
테스트 메소드 전환
- 기존에 만들었던 main() 메서드 테스트는 그런 면에서 프레임워크에 적용하기엔 적합하지 않다.
- 가장 먼저 할 일은 main() 메서드에 있던 테스트 코드를 일반 메소드로 옮기는 것이다.
- 새로 만들 테스트 메소드는 JUnit 프레임워크가 요구하는 조건 두가지를 따라야 한다.
- 메서드가 public으로 선언돼야 하는 것이고
- 메소드 @Test라는 애노테이션을 붙여주는 것이다.
import org.junit.Test;
...
public class UserDaoTest {
@Test // JUnit에게 테스트용 메소드임을 알려준다.
public void addAndGet() throws SQLException { // JUnit 테스트 메소드는 반드시 public으로 선언돼야 한다.
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
...
}
}
- UserDaoTest의 메소드명은 이왕이면 테스트의 의도가 무엇인지 알 수 있는 이름이 좋다.
- 또한 public 액세스 권한을 주는 것을 잊으면 안된다.
- 마지막으로 @Test 애노테이션을 붙여주면 된다.
검증 코드 전환
- 테스트의 결과를 검증하는 if/else 문장을 JUnit이 제공하는 방법을 이용해 전환해본다.
- if 문장의 기능을
assertThat
이라는 스태틱 메소드를 이용해 다음과 같이 변경할 수 있다. - assertThat(user2.getName(), is(user.getName()));
assertThat() 메소드
는 첫번째 파라미터의 값을 뒤에 나오는 매처(matcher)라고 불리는 조건으로 비교해서 일치하면 다음으로 넘어가고 아니면 테스트가 실패하도록 만들어준다.- is()는 매처의 일종으로 equals()로 비교해주는 기능을 가졌다.
JUnit을 적용한 UserDaoTest
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
...
public class UserDaoTest {
@Test
public void addAndGet() throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext (
"applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
User user = new User();
// JUnit 테스트로 전환하는 김에 새로운 기분으로 테스트 데이터도 바꿔보자.
user.setId("gyumee");
user.setName("박성철");
user.setPassword("springno1");
dao.add(user);
User user2 = dao.get(user.getId());
assertThat(user2.getName(), is(user.getName));
assertThat(user2.getPassword(), is(user.getPassword()));
}
}
JUnit 테스트 실행
JUnit 프레임워크를 이용해 앞에서 만든 테스트 메소드를 실행하도록 코드를 만들어보자.
JUnit을 이용해 테스트를 실행해주는 main() 메소드
import org.junit.runner.JUnitCore;
...
public static void main(String[] args) {
JUnitCore.main("springbook.user.dao.UserDaoTest");
}
2.2 개발자를 위한 테스팅 프레임워크 JUnit
- JUnit 테스트 작성방법과 실행 방법은 알고 있어야 한다.
- 스프링의 기능을 익히기 위해서라도 JUnit은 꼭 사용할줄 알아야한다.
2.3.1 JUnit 테스트 실행 방법
- 가장 좋은 JUnit 테스트 실행 방법은 자바 IDE에 내장된 JUnit 테스트 지원 도구를 사용하는 것이다.
IDE
- 대부분의 자바 개발자가 사용하고 있는 사실상의 표준 자바 IDE인 이클립스는 오래전부터 JUnit 테스트를 지원하는 기능을 제공하고 있다.
- JUnit은 한 번에 여러 테스트 클래스를 동시에 실행할 수도 있다.
- 이클립스의 단축키를 기억해두면 더욱 빠르고 편하게 테스트할 수도 있다.
- Alt + Shift + X + T를 순서대로 누르면 선택해둔 클래스나 패키지, 프로젝트의 테스트가 바로 실행된다.
빌드툴
- 프로젝트의 빌드를 위해 ANT나 메이븐(Maven)과 같은 빌드툴과 스크립트를 사용하고 있다면, 빌드 툴에서 제공하는 JUnit 플러그인이나 태스크를 이용해 JUnit 테스트를 실행할 수도 있다.
- 그런데 여러 개발자가 만든 코드를 모두 통합해서 테스트를 수행해야될 때도 있다.
- 이런 경우에는 서버에서 모든 코드를 가져와 통합하고 빌드한 뒤에 테스트를 수행하는 것이 좋다.
2.3.2 테스트 결과의 일관성
- 테스트가 외부 상태에 따라 성공하기도 하고 실패하기도 한다.
- 예를 들어, UserDaoTest를 실행하기 전에 DB의 USER 테이블 데이터를 모두 삭제해줘야 할 때였다.
- 깜빡 잊고 그냥 테스트를 실행했다가는 이전 테스트를 실행했을 때 등록됐던 사용자 정보와 기본키가 중복된다면서 add() 메소드 실행 중에 에러가 발생할 것이다.
- 문제는
테스트가 외부 상태에 따라 성공하기도 하고 실패하기도 한다는 점이다
- 가장 좋은 해결책은
addAndGet() 테스트를 마치고 나면 테스트가 등록한 사용자 정보를 삭제해서, 테스트를 수행하기 이전 상태로 만들어주는 것이다.
- 이로써 테스트를 아무리 여러 번 반복해서 실행하더라도 항상 동일한 결과를 얻을 수 있다.
deleteAll()의 getCount() 추가
일관성 있는 결과를 보장하는 테스트를 만들기 위해 준비할 것은 UserDao에 새로운 기능을 추가해주는 일이다.
deleteAll
- deleteAll() 메소드로, USER 테이블의 모든 레코드를 삭제해주는 간단한 기능이 있다.
public void deleteAll() throws SQLException {
Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("delete from users");
ps.executeUpdate();
ps.close()
c.close()
}
getCount()
- getCount()메서드는 USER 테이블의 레코드 개수를 돌려준다.
public int getCount() throws SQLException {
Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("select count(*) form users");
ResultSet rs = ps.executeQuery();
rs.next();
int count =rs.getInt();
rs.close();
ps.close();
c.close();
return count;
}
deleteAll()과 getCount()의 테스트
- deleteAll()과 getCount()를 적절히 모두 활용한다
- getCount()로 검증작업을 추가한다.
deleteAll()과 getCount()가 추가된 addAndGet() 테스트
@Test
public void addAndGet() throws SQLException {
...
dao.deleteAll();
assertThat(dao.getCount(), is(0));
User user = new User();
user.setId("gyumee");
user.setName("박성철");
user.setPassword("springno1");
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()));
}
동일한 결과를 보장하는 테스트
- 단위 테스트는 코드가 바뀌지 않는다면 매번 실행할 때마다 동일한 테스트 결과를 얻을 수 있어야 한다.
- 동일한 테스트 결과를 얻을 수 있는 다른 방법도 있다.
-
테스트하기 전에 테스트 실행에 문제가 되지 않는 상태를 만들어주는 편이 더 나을 것이다.
- 단위 테스트는 항상 일관성 있는 결과가 보장돼야 한다는 점을 잊어선 안된다.
2.3.3 포괄적인 테스트
- 성의없이 테스트를 만드는 바람에 문제가 있는 코드인데도 테스트가 성공하게 만드는건 더 위험하다.
getCount() 테스트
- @Test가 붙어 있고 public 접근자가 있으며 리턴 값이 void형이고 파라미터가 없다는 조건을 지키기만 하면 된다.
파라미터가 있는 User클래스 생성자
public User(String id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
public User() {
// 자바빈의 규약을 따르는 클래스에 생성자를 명시적으로 추가했을 때는 파라미터가 없는 디폴트 생성자도 함께 정의해주는 것을 잊지말자.
}
- 모든 코드의 수정 후에는 그 수정에 영향을 받을 만한 테스트를 실행하는 것을 잊지말자.
getCount() 테스트
@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));
}
- JUnit은 특정한 테스트 메소드의 실행 순서를 보장해주지 않는다.
- 테스트의 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘못 만든 것이다.
addAndGet() 테스트 보완
- get이 파라미터로 주어진 id에 해당하는 사용자를 가져온 것인지, 그냥 아무거나 가져온 것인지 테스트에서 검증하지는 못했다.
- 그래서 get() 메소드에 대한 테스트 기능을 좀 더 보완할 필요가 있다.
get() 테스트 기능을 보완한 addAndGet() 테스트
@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");
// 중복되지 않는 값을 가진 두 개의 User 오브젝트를 준비해둔다.
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의 id로 get()을 실행하면 첫번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = dao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
// 두번째 User에 대해서도 같은 방법으로 검증한다.
}
get() 예외조건에 대한 테스트
get() 메서드의 예외상황에 대한 테스트
@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");
// 이 메소드 실행 중에 예외가 발생해야 한다.
// 예외가 발생하지 않으면 테스트가 실패한다.
}
댓글남기기