이번장에선 TDD와 스프링에 대해서 요약해보고자 한다. 앞선 포스팅에서 처럼 토비의 스프링을 읽고 요약한 자료이다.
문제가 있을 시 lshn1007@hanyang.ac.kr 로 메일 주시면 삭제하겠습니다.
보통 테스트를 한다고 하면 애플리케이션에 필요한 기능을 대충이라도 모두 구현한 후 테스트를 하는데 이 방법은 문제점이 많다. 알고리즘 문제에서 만일 testcase가 주어지지 않는다면.. 당신은 정답이라 확신할 수 있는가?
이러한 단점이 발생한 원인은 한번에 많은 것을 몰아서 테스트를 했기 때문이다. 이렇게 할 경우 테스트 수행 과정도 복잡해지고, 오류가 발생했을 때 정확한 원인을 파악하기가 쉽지 않다. 따라서 테스트는 가능하면 작은 단위로 쪼개서 수행해야 한다. 이 때 말하는 단위란, 명확히 정의된 단위가 아니고, 테스트가 오로지 하나의 관심에 집중될 수 있는 경우 이를 하나의 단위라고 말할 수 있다.
작은 단위의 코드에 대해 테스트를 수행한 것을 단위 테스트(Unit test) 라고 하는데, 일반적으로 단위는 작을수록 좋으며 단위의 범위를 넘어서는 다른 코드들은 신경쓰지 않고, 테스트가 동작할 수 있으면 좋다. 단위 테스트는 개발 후에 기능 오류를 빠르게 확인해볼 수 있고, 개발자가 작성한 코드가 원래 의도한대로 동작하는지를 스스로 확인해볼 수 있기 때문에 반드시 필요하다.
개별로 단위 테스트를 수행해도 좋고, 통합 테스트를 최종적으로 진행하는 것도 좋다. 다만 Spring의 경우 DB server와 연결되는 경우도 있기 때문에 단위를 잘 고려해야 하는 것이 좋다.
애플리케이션을 직접 구동하고 값을 입력하여 기능이 정상적으로 동작했는지 눈으로 확인하는 것과 같이 테스트를 수동으로 진행하는 것은 문제가 많다.
반면에 테스트 코드를 작성하여 테스트를 수행하면 장점이 많다.
테스트는 외부 환경, 테스트 실행 순서에 영향 없이 항상 동일한 결과가 보장되어야 한다. 항상 동일한 결과를 보장하도록 테스트를 만드는 방법은 아래와 같다.
개발한 코드에 문제가 있음에도 운이 좋게 테스트 케이스에서 성공하는 경우(=못해도 하루에 두 번은 맞는다는 죽은 시계)가 있을 수 있다. 따라서 한 가지 결과만 가지고 검증하지 말고, 가급적이면 여러 경우를 대입해 검증해보는 것이 좋다. 즉 동일 단위 테스트에 대해 다양한 검증을 시도하면 할 수록 좋다.
개발자가 테스트를 직접 만들 때 자주 하는 실수가 하나 있다. 바로 성공하는 테스트만 골라서 만드는 것이다. 이렇게 할 경우 코드에서 향후 발생할 수 있는 다양한 예외 상황을 살펴보지 못하고 넘어갈 수 있기 때문에 테스트 케이스를 만들 때는 항상 네거티브 케이스를 먼저 만드는 습관을 들이는 게 좋다.
테스트 클래스를 만들고, 클래스 안에 테스트 메소드를 만들면 된다.
이 때, 테스트 메소드는 JUnit 프레임워크가 요구하는 조건 2가지를 준수하여 작성해야 한다.
(1) 메소드의 접근 제어자를 public, 반환형은 void로 선언해야 한다. (JUnit에서는 public메소드만을 테스트 메소드로 허용)
(2) 메소드에 @Test 어노테이션을 붙여줘야 한다.
규칙은 아래와 같다.
테스트 클래스 명명 규칙 : JUnit에서 사용할 테스트 클래스는 관례에 따라 '테스트할 클래스명 + Test' 와 같이 명명한다.
테스트 메소드 명명 규칙 : 테스트 메소드의 이름은 테스트 의도가 무엇인지 알 수 있는 이름으로 명명하는 것이 좋다. @DisplayName 을 활용하는 것도 방법이 될 수 있다.
public class UserDaoTest {
// @Test : JUnit에게 테스트용 메소드임을 알려준다.
@Test
public void addAndGet() throws SQLException {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
...
}
}
왜 테스트 메소드를 실행할 때마다 새로운 오브젝트를 생성할까?
각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 보장하기 위함이다.
픽스처(fixture)란?
테스트를 수행하는 데 필요한 정보나 오브젝트를 픽스처라고 한다. 일반적으로 픽스처는 여러 테스트에서 반복적으로 사용하므로 @BeforeEach 어노테이션을 이용해 생성하거나 DI를 이용하면 편리하다.
import org.junit.Test;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.*;
public class UserControllerTest {
@Test
public void getUser() {
assertThat(true, is(false)); // Test failed
}
}
is()
: param1과 param2를 equals()로 비교한다.
not()
: assertThat()에서 뒤에 나오는 matcher를 통해 비교한 결과를 반대로 뒤집는다.
sameInstance()
: param1과 param2의 동일성을 확인한다.
hasItem()
: param1 collection에 param2가 포함되어 있는지 확인한다.
nullValue()
: param1이 null인지 확인한다.
either()
: either A or B 형식으로 matcher를 사용할 수 있게 해준다. (A, B 매쳐 둘 중 하나가 성공할 경우 테스트 성공)
@RunWith(value = SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserControllerTest {
@Autowired
private ApplicationContext applicationContext;
@Test
public void getUser() {
User user = applicationContext.getBean(User.class);
assertThat("beanUserId", is(user.getId()));
assertThat("beanUserPassword", is(user.getPassword()));
}
}
@Qualifier
Primary
가 있는데 이전 포스팅을 참조하자.JUnit에서는 예외 상황에 대한 테스트를 수행해볼 수 있다. @Test 어노테이션의 expected 속성의 값을 테스트 중에 발생할 것으로 기대되는 예외 클래스로 설정해주면 된다. 이렇게 하면 테스트 결과로 예외가 던져지지 않을 경우 테스트 결과는 실패가 된다. 최근에는 assertThrow를 이용해 Build패턴으로 예제가 많은 듯 하니 참고하자! 토비의 스프링은 12년도에 출판된 도서이다.
@SpringBootTest
@RunWith(value = SpringJUnit4ClassRunner.class)
//@ContextConfiguration(locations = "/applicationContext.xml")
public class UserControllerTest {
@Autowired
private UserController userController;
// 테스트 중에 발생할 것으로 예상되는 예외 기입
@Test(expected = NullPointerException.class)
public void getNullPointerException() {
userController.getNullPointerException();
}
}
Spring application의 기능을 테스트하기 위해 애플리케이션 컨텍스트를 @Before 메소드로 생성하려고 하면 매 테스트 메소드가 실행되기 전 애플리케이션 컨텍스트가 생성하므로 application의 규모가 커지면 커질수록 애플리케이션 컨텍스트 생성에 많은 시간과 자원이 소모될 수 있다.
본래 테스트는 가능한 한 독립적으로 매번 새로운 오브젝트를 만들어서 사용하는 것이 원칙이지만, 애플리케이션 컨텍스트처럼 생성에 많은 시간과 자원이 소모되는 경우에는 테스트 전체가 공유하는 오브젝트를 만들기도 한다.
따라서, 이를 위해 JUnit에서 제공하는 @BeforeClass 를 이용해 static 필드로 선언한 애플리케이션 컨텍스트를 초기화하여 사용할 수도 있겠으나, 이보다는 스프링이 직접 제공하는 애플리케이션 컨텍스트 지원 기능을 사용하는 것이 더 편리하다. (@RunWith & @ContextConfiguration)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
/**
* 테스트 오브젝트가 만들어지고 나면 스프링 테스트 컨텍스트에 의해 자동으로 값이 주입된다.
* 스프링 애플리케이션 컨텍스트는 초기화할 때, 자기 자신도 Bean으로 만들기 때문에 이러한 동작이 가능하다.
*/
@Autowired
private ApplicationContext context;
@Before
public void setUp() {
this.dao = this.context.getBean("userDao", UserDao.class);
}
}
테스트 코드에서 운영용 설정 파일을 이용해 DI를 수행하고 테스트 하는 것은 문제가 되기 때문에 테스트 시에는 반드시 별도의 DI 설정 파일을 만들어 사용해야 한다.
기존에 운영용 DI 설정 파일을 applicationContext.xml
이라는 이름으로 만들어 사용했다면, 테스트용 DI 설정 파일은 test-applicationContext.xml
과 같이 다르게 만들어 운영 환경과 테스트 환경에서 사용할 DI 설정 파일을 분리하면 된다. (@ContextConfiguration 어노테이션의 locations만 테스트 환경의 설정 파일의 이름으로 변경해주면 된다.)
애플리케이션 코드 뿐만 아니라 테스트 코드도 리팩토링의 대상이 될 수 있다. 만약, 여러 테스트 메소드에서 중복된 코드가 존재할 경우 아래와 같이 리팩토링이 가능하다.