이번 기회에 스프링으로 프로젝트를 하면서 스프링 테스트에 대해 정리하고자 토비의 스프링 2장 테스트를 읽어봤다. 다음은 정리한 내용들이다. 틀린 부분이 있다면 피드백 부탁드립니다!
스프링이 개발자에게 제공하는 중요한 가치중 하나가 테스트이다.
테스트란 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인해서, 만든 코드를 확신할 수 있게 해주는 작업이다. 또한 테스트의 결과가 원하는 대로 나오지 않는 경우에는 코드나 설계에 결함이 있음을 알 수 있다. 이를 통해 코드의 결함을 제거해가는 작업, 일명 디버깅을 거치게 되고, 결국 최종적으로 테스트가 성공하면 모든 결함이 제거됐다는 확신을 얻을 수 있다.
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("password");
dao.add(user);
System.out.println(user.getId() + "등록 성공");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user.getPassword());
System.out.println(user2.getId() + "조회 성공");
}
}
MVC 프레젠테이션 계층을 구현한다. 테스트용 웹 애플리케이션을 서버에 띄우고 요청을 보내본다.
이 방법은 배보다 배꼽이 더 크다;
여기서 단위는 충분히 하나의 관심에 집중해서 효율적으로 테스트할 만한 범위의 단위이다. 크게는 사용자 관리 기능을 모두 통틀어서 하나의 단위로 볼 수 있고, 작게 보자면 메서드 하나만 가지고 하나의 단위로 볼 수 있다. 일반적으로 단위는 작을수록 좋다.
외부의 리소스에 의존하는 테스트는 단위 테스트가 아니다.
Dao 단위 테스트의 경우에 DB에 의존적인, 즉 테스트마다 DB가 격리되지 않는다면 단위 테스트라고 보기 어렵다.
테스트가 코드를 통해 자동으로 실행된다. 즉 UserDaoTest 를 실행하면 자동으로 테스트가 실행된다. 만약 앞서 나온 웹을 통해 테스트를 수행한다면 코드가 아닌 개발자 스스로가 테스트를 수행해야한다.
자동수행 테스트 코드의 장점은 자주 반복할 수 있다는 것이다.
작은 단계를 거치는 동안 테스트를 수행해서 확신을 가지고 코드를 변경해갔기 때문에 전체적으로 코드를 개선하는 작업에 속도가 붙고 더 쉬워졌다.
또한 UserDao의 기능을 추가하려고 할 때도 미리 만들어둔 테스트 코드는 유용하게 쓰일 수 있다. 기존에 만들어뒀던 기능들이 새로운 기능을 추가하느라 수정한 코드에 영향을 받지 않고 여전히 잘 동작하는지를 확인할 수도 있다.
// 수정 전 테스트 코드
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("조회 테스트 성공");
}
Main 메서드로 만든 테스트의 한계를 극복하기 위해서 JUnit 사용
JUnit 은 프레임워크이다. 프레임워크는 개발자가 만든 클래스에 대한 제어 권한을 넘겨받아서 주도적으로 애플리케이션의 흐름을 제어한다.
main 메서드는 제어권을 직접 갖는다는 의미이다. JUnit 프레임워크를 사용하기 위해서 일반 메서드로 옮겨야한다. 이때 두 가지 조건이 붙는다.
public class UserDaoTest {
@Test
public void andAndGet() throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
// 생략
}
}
if (!user.getName()).equals(user2.getName()){...}
// 이 if 문장의 기능을 JUnit 이 제공해주는 assertThat 이라는 스태틱 메서드를 이용해 다음과 같이 변경할 수 있다.
assertThat(user2.getName()).is(user.getName()));
JUnit은 예외가 발생하거나 assertThat 메서드에서 실패하지 않고 테스트 메서드의 실행이 완료되면 테스트가 성공했다고 인식한다.
import org.junit.runner.JUnitCore;
...
public static void main(String[] args) {
JUnitCore.main("springbook.user.dao.UserDaoTest");
}
IDE에 내장된 JUnit 테스트 지원 도구를 사용한다.
개발자 개인별로는 IDE 에서 JUnit 도구를 활용해 테스트를 실행하는 게 가장 편리하다. 그런데 여러 개발자가 만든 코드를 모두 통합해서 테스트를 수행해야 할 때도 있다. 이런 경우에는 서버에서 모든 코드를 가져와 통합하고 빌드한 뒤에 테스트를 수행하는 것이 좋다. 이때는 빌드 스크립트를 이용해 JUnit 테스트를 실행하고 그 결과를 메일 등으로 통보받는 방법을 사용하면 된다.
코드에 변경사항이 없다면 테스트는 항상 동일한 결과를 내야 한다.
deleteAll 메서드를 만들어서 테스트가 등록한 사용자 정보를 삭제하도록 구현함 → 매번 수동으로 DB 데이터를 지워주는 부분을 자동화함
getCount 메서드를 만들어서 USER 테이블의 레코드 개수를 조회한다 → 위에서 만든 deleteAll 이 잘 동작했는지 검증한다.
@Test
public void addAndGet() throws SQLException {
...
dao.deleteAll();
assertThat(dao.getCount(), is(0));
}
User user = new User();
user.setId("id");
user.setName("칙촉");
user.setPassword("password");
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()));
위의 코드와 다르게 테스트를 마치기 직전에 데이터를 지워도 동일한 테스트 결과를 얻을 수 있다. 하지만 해당 테스트 실행 이전에 다른 이유로 USER 테이블에 데이터가 들어가 있다면 테스트가 실패할 수도 있다.
스프링은 DB를 사용하는 코드를 테스트하는 경우 매우 편리한 테스트 방법을 제공해준다.
@DataJpaTest, @JdbcTest 는
@Transactional 어노테이션이 있어서 자동으로 롤백시켜준다.(테스트 코드에서 @Transactional 어노테이션이 있으면 select 이외의 모든 쿼리는 롤백 대상이된다)
실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다. → 오버엔지니어링을 방지할 수 있다.
테스트가 실패하면 설계한 대로 코드가 만들어지지 않았음을 바로 알 수 있다 → 즉각적인 피드백을 받을 수 있다.
중요성 코드를 만들고 나서 시간이 많이 지나면 테스트를 만들기가 귀찮아진다.또, 작성한 코드가 많기 때문에 무엇을 테스트해야 할지 막막할 수도 있다.
JUnit이 테스트를 수행하는 방식
각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해 매번 새로운 오브젝트를 만들게 했다. 덕분에 멤버 필드도 부담없이 사용할 수 있다.
테스트 클래스에서 자주 사용되는 값, 오브젝트들은 멤버필드로 선언해서 사용한다. 어차피 매번 새로운 테스트 오브젝트가 만들어지니까 멤버필드에서 바로 초기화해도 상관없다. 하지만 픽스처 생성 로직이 흩어져 있는 것 보다는 모여 있는 편이 나을 테니 @Before 메서드를 이용
public class UserDaoTest {
private UserDao dao;
private User user1;
private User user2;
private User user3;
@Before
public void serUp() {
...
this.user1 = new User("id1", "영", "password");
this.user2 = new User("id2", "윤", "password");
this.user3 = new User("id3", "안", "password");
}
...
}
현재 테스트 코드에는 몇가지 문제가 있다. ApplicationContext 를 여러번 만들고 있다. 추후에 빈이 많아지고 복잡해지면 ApplicationContext 생성에 적지 않은 시간이 걸릴 수 있다. ApplicationContext 만들어질때 모든 싱글톤 빈 오브젝트를 초기화하기 때문이다.
또 한 가지 문제는 ApplicationContext가 초기화될 떄 어떤 빈은 독자적으로 많은 리소스를 할당하거나 독립적인 스레드를 띄우기도 한다는 점이다. 이런 경우에는 테스트를 마칠 때마다 ApplicationContext 내의 빈이 할당한 리소스 등을 깔끔하게 정리해주지 않으면 다음 테스트에서 새로운 ApplicationContext가 만들어지면서 문제를 일으킬 수도 있다.
→ @BeforeClass 를 사용해서 스태틱 필드에 ApplicationContext를 저장한다. @BeforeClass 는 테스트 클래스 전체에 걸쳐 딱 한 번만 실행된다. But 더 편리하게 할 수 있다.
스프링은 JUnit을 이용하는 테스트 컨텍스트 프레임워크를 제공한다. 테스트 컨텍스트의 지원을 받으면 간단한 어노테이션 설정만으로 테스트에서 필요로 하는 ApplicationContext를 만들어서 모든 테스트가 공유하게 할 수 있다.(추가 ApplicationContext는 초기화할 때 자기 자신도 빈으로 등록한다. 그래서 Autowired 를 통해 주입받을 수 있는것)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(location="/applicationContext.xml")
public class UserDaoTest {
@Autowired
private ApplicationContext context;
...
@Before
public void setUp() {
this.dao = this.context.getBean("userDao", UserDao.class);
...
}
}
@RunWith: JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용한다. SpringJUnit4ClassRunner라는 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면 JUnit이 테스트를 진행하는 중에 테스트가 사용할 ApplicationContext를 만들고 관리하는 작업을 진행해준다.
@ContextConfiguration: 자동으로 만들어줄 ApplicationContext 설정파일 위치를 지정한 것이다.
결과: 하나의 테스트 클래스 내의 테스트 메소드는 같은 ApplicationContext를 공유해서 사용할 수 있게 됨
(추가: 최초 ApplicationContext가 초기화될때 bean 이 초기화되는게 아니라 getBean을 호출할때 초기화된다. )
여러 개의 테스트 클래스가 있는데 모두 같은 설정파일을 가진 ApplicationContext를 사용한다면, 스프링은 테스트 클래스 사이에서도 ApplicationContext를 공유하게 해준다. → 테스트 전체에 걸쳐서 단 한개의 ApplicationContext만 만들어져 사용됨, 테스트 성능이 대폭 향상됨
별도의 DI 설정 없이 필드의 타입정보를 이용해 빈을 자동으로 가져올 수 있다.(추가: 타입이 여러개라면 변수의 이름과 같은 이름의 빈이 있는지 확인한다, 변수 이름으로도 찾을수 없을 경우 예외 발생) ApplicationContext가 가지고 있는 빈을 DI 받을수 있다면 굳이 ApplicationContext를 주입 받는게 아니라 우리가 사용할 UserDao를 직접 DI 받도록 함
테스트용 DB에 연결해주는 DataSource를 테스트 내에서 직접 만들어서 사용함, applicationContext.xml 파일의 설정정보를 따라 구성한 오브젝트를 가져와 의존관계를 강제로 변경
@DirtiesContext
public class UserDaoTest {
@Autowired
UserDao userDao;
@Before
public void setUp() {
...
DataSource dataSource = new SingleConnectionDataSource(
"jdbc:mysql://localhost/testdb", "spring", "book", true
);
}
...
}
// 코드에 의한 수동 DI
dao.setDataSource(dataSource);
But DirtiesContext 는 매번 ApplicationContext를 새로 만듦!
@DirtiesContext 는 클래스에만 적용할 수 있는 건 아니다. 하나의 메서드에서만 컨텍스트 상태를 변경한다면 메서드 레벨에 @DirtiesContext 를 붙여주는 편이 낫다. 해당 메서드의 실행이 끝나고 나면 이후에 진행되는 테스트를 위해 변경된 애플리케이션 컨텍스트는 폐기되고 새로운 애플리케이션 컨텍스트가 만들어진다.
아예 테스트에서 사용될 DataSource 클래스가 빈으로 정의된 테스트 전용 설정파일을 따로 만들어두는 방법을 이용.
스프링 컨테이너 없이 테스트 코드의 수동 DI 만을 이용
public class UserDaoTest {
// @Autowired 가 없다
UserDao dao;
...
@Before
public void setUp() {
...
// 객체 생성 및 관계설정을 직접 해준다
dao = new UserDao();
DataSource dataSource = new SingleConnectionDataSource(
"jdbc:mysql://localhost/testdb", "spring", "book", true
);
dao.setDataSource(dataSource);
}
}
침투적 기술은 기술을 적용했을 때 애플리케이션 코드에 기술 관련 API 가 등장하거나, 특정 인터페이스나 클래스를 사용하도록 강제하는 기술을 말한다. 침투적 기술을 사용하면 애플리케이션 코드가 해당 기술에 종속되는 결과를 가져온다. 반면에 비침투적인 기술은 애플리케이션 로직을 담은 코드에 아무런 영향을 주지 않고 적용이 가능하다. 따라서 기술에 종속적이지 않은 순수한 코드를 유지할 수 있게 해준다. 스프링은 이런 비침투적인 기술의 대표적이 예다. 그래서 스프링 컨테이너 없는 DI 테스트도 가능한 것이다.
컨테이너 없는 DI 테스트
항상 스프링 컨테이너 없이 테스트할 수 있는 방법을 가장 우선적으로 고려하자. 이 방법이 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하다.
@RunWith, @ContextConfiguration
여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우
@DirtiestContext
예외적인 의존관계를 강제로 구성해서 테스트해야 할 경우
예시:
public class JUnitTest {
static JUnitTest testObject;
@Test
public void test1() {
assertThat(this, is(not(sameInstance(testObject))));
testObject = this;
}
@Test
public void test2() {
assertThat(this, is(not(sameInstance(testObject))));
testObject = this;
}
@Test
public void test3() {
assertThat(this, is(not(sameInstance(testObject))));
testObject = this;
}
}
sameInstatnce: 동일성을 비교한다.
예시의 결과로 알 수 있는점은 JUnit 이 @Test 메서드 마다 객체를 생성한다는 것이다.
코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트를 말한다.
QA 과정, 또는 사용자 버그가 발생한 경우에 무턱대로 코드를 뒤져가면서 수정하려고 하기보다는 먼저 버그 테스트를 만들어보는 편이 유용하다.
같은 결과를 내는 값의 범위를 구분해서 각 대표 값으로 테스트를 하는 방법을 말한다. 어떤 작업의 결과의 종류가 true, false 또는 예외발생 세 가지라면 각 결과를 내는 입력 값이나 상황의 조합을 만들어 모든 경우에 대한 테스트를 해보는 것이 좋다.
에러는 동등분할 범위의 경계에서 주로 많이 발생한다는 특징을 이용해서 경계의 근처에 있는 값을 이용해 테스트하는 방법이다. 보통 숫자의 입력 값인 경우 0 이나 그 주변 값 또는 정수의 최대값, 최소값 등으로 테스트해보면 도움이 될 때가 많다.
spring 사용자들아 웹 서비스 만들때 웹 테스트 같이 덩어리로 테스트 하지말고 좀 더 작은 단위로 테스트해라.
단위 테스트할때 JUnit 프레임워크 써.
TDD 한번 해봐.