토비의 스프링 vol.1 2장 테스트 내용을 정리하며
오래된 버전이라 예제를 따라서 실행하는 것보단
스프링이 추구하는 객체지향 설계에 대해 생각해본다.
자세한 내용이 궁금하면 읽는 걸 추천한다.
2장을 시작하며
스프링이 제공하는 중요한 가치 중에는 객체지향과 테스트가 있다.
애플리케이션은 꾸준히 변하고 커져간다. 그 변화에 대응하는 첫 번째가 바로 확장과 변화를 고려한 객체지향적 설계와 IoC/DI 기술이라면, 두 번째가 코드에 대한 확신과 변화에 유연하게 대처할 수 있게 해주는 테스트 기술이다.
이번 장에서는 테스트에 대해 알아볼 것이다.
테스트의 유용성
1장에서 초난감DAO 상태에서 시작하여 점차 개선 나가는 과정 동안 처음과 동일한 기능을 보장해주는 방법에는 직접 테스트 코드를 실행하는 방법밖에 없었다. 테스트란 의도했던 대로 코드가 정확히 작동하는지를 확인해서 코드에 대한 확신을 주는 작업이다.
웹을 통한 테스트의 문제점
테스트 코드가 없다면 UserDao 기능을 확인하기 위해 화면이 보이는 웹까지 구현 후 진행되어야 한다. 웹 화면을 통해 값을 입력하고, 기능을 수행하고, 확인하는 방법은 흔히 사용하지만 단점이 많다. 우선 서비스 클래스, 컨트롤러, JSP 등 모든 레이어의 구현이 필요하고 문제가 발생하면 어디서 발생했는지 찾아야 한다.
작은 단위의 테스트
테스트할 대상이 명확하다면 웹까지 구현할 필요 없이 대상만 테스트하는 것이 바람직하다. 테스트도 작은 단위로 쪼개서 하는 것이 좋다.
작은 단위 코드에 테스트 수행하는 것을 단위 테스트(unit test)라고 한다.
단위 테스트하는 이유
개발자가 설계하고 만든 코드가 제대로 작동하는지 확인받기 위해서 단위 테스트를 한다.
자동 수행 테스트 코드
한번 작성된 테스트 코드는 간단히 실행하는 것으로 작성된 단위 테스트를 모두 실행할 수 있다. 웹을 뛰우고 입력 폼에 값을 하나씩 입력할 필요가 없이 자동적으로 테스트 코드가 수행된다. 매우 짧은 시간에 테스트를 실행할 수 있어 자주 돌려볼 수 있다.
변화를 위한 테스트
처음의 초난감DAO 코드부터 스프링까지의 객체지향 코드로 발전하는 과정에는 테스트가 항상 따라다녔다. 수정할 때마다 테스트를 통해서 정상적으로 돌아가는지 확인하고 잘못된 점이 있는지 손쉽게 파악이 가능했다. 이런 이유로 테스트를 이용하면 신규 기능이나 수정된 코드에 대한 확인을 쉽게 할 수 있다.
UserDaoTest의 문제점
main()를 이용한 테스트는 UI보다 만족스럽지만 문제점이 있다.
- 수동 확인 작업의 번거로움 : 작성한 코드로 자동으로 수행하지만 직접 눈으로 확인해야 한다.
- 실행 작업의 번거로움 : main() 메소드를 이용하다 보니 실행하는 게 번거롭다.
테스트 검증의 자동화
우선 기존 main()의 테스트 코드 일부분을 확인해보자.
public static void main(String[] args) throws SQLException, ClassNotFoundException {
...
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " 조회 성공");
}
유저를 등록하고 조회해서 출력하지만 user와 user2가 동일한지 비교하지는 않는다. 직접 눈으로 출력 값으로 확인하기 때문이다.
이를 수정해보자.
public static void main(String[] args) throws SQLException, ClassNotFoundException {
...
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("조회 테스트 성공");
}
}
직접 출력 값을 하나씩 확인 안 해도 테스트 성공이란 출력으로 무사히 진행됨을 알 수 있다. 실패했더라도 어디서 실패하는 지 출력한다.
자동화된 테스트를 위한 xUnit 프레임워크를 만든 켄트 벡은 "테스트란 개발자가 마음 편하게 잠자리에 들 수 있게 해주는 것"이라 했다.
테스트의 효율적인 수행과 결과 관리
JUnit 테스트로 전환
메소드에 @Test 어노테이션을 붙이면 된다.
public class UserDaoTest {
@Test
public void addAndGet() throws Exception {
ApplicationContext context = new GenericXmlApplicationContext("toby02/applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
User user = new User();
user.setId("loop");
user.setName("반복");
user.setPassword("study");
userDao.add(user);
User user2 = userDao.get(user.getId());
assertThat(user.getName()).isEqualTo(user2.getName());
assertThat(user.getPassword()).isEqualTo(user2.getPassword());
}
}
이전에 조건문으로 했던 코드가 assertThat과 isEqualTo 방식으로 깔끔하게 변경되었다.
assertThat은 JUnit이 제공하는 검증 메소드다.
* 자세한 사용법은 공식 가이드를 확인해보자
테스트 결과의 일관성
테스트 코드를 확인해보면 DB에 넣고 조회하는 것으로 끝이 난다. 다시 실행하면 이미 등록된 데이터가 있어서 중복 문제가 생긴다.
이를 해결하기 위해선 테스트가 실행할 때마다 삭제 메소드를 만들어서 디비 값을 초기화할 수 있다.
@Test
public void addAndGet() throws Exception {
...
// 테이블 데이터 초기화
userDao.allDelete();
assertThat(userDao.getCount()).isEqualTo(0);
User user = new User();
user.setId("loop");
user.setName("반복");
user.setPassword("study");
userDao.add(user);
assertThat(userDao.getCount()).isEqualTo(1);
...
}
이런 식으로 동일한 결과를 보장하는 테스트가 되었다. 개선할 점이 많이 보이지만 변화의 흐름을 이해하기 위해 이대로 두자.
단위 테스트는 항상 일관성 있는 결과가 보장되어야 한다. 외부 환경 & 테스트 순서에 상관없이 동일한 결과가 나와야 한다.
실패하는 테스트
개발자가 테스트를 만들 때 자주 하는 실수가 있다. 성공하는 테스트만 한다는 것이다. 직접 짠 코드를 테스트를 해보면 무의식적으로 실패보단 성공하는 테스트 위주로 진행할 수 있으니 실패하는 테스트를 먼저 만드는 습관을 들이자
@Test
public void getUserFailure() throws SQLException, ClassNotFoundException {
ApplicationContext context = new GenericXmlApplicationContext("toby02/applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
userDao.allDelete();
assertThat(userDao.getCount()).isEqualTo(0);
assertThatThrownBy(() -> {
userDao.get("unknown_id");
}).isInstanceOf(EmptyResultDataAccessException.class);
}
JUnit에는 실패하는 테스트도 확인할 수 있다.
assertThatThrownBy 는 에러가 발생하면 isInstanceOf에 적힌 예외를 확인하고 일치하면 테스트가 성공한다.
예외가 발생 안 하거나 일치 안 하면 테스트를 실패 처리해준다.
테스트 주도 개발(TDD)
테스트 코드를 먼저 만들고, 테스트를 성공하는 코드를 구현하는 방법을 테스트 주도 개발이라고 한다.
TDD는 테스트를 먼저 만들기 때문에 실수로 테스트를 놓치지 않고 꼼꼼하게 작성할 수 있다.
TDD는 테스트 작성하고 성공하는 코드를 만드는 작업 주기를 짧게 가져가게 권장한다.
* 이규원의 현실 세상의 TDD 를 참고하자
테스트 코드 개선
초난감DAO 처럼 중복이 될만한 코드들이 보인다. 애플리케이션 컨텍스트와 테이블 초기화하는 로직으로 이를 분리하여 개선해보자.
public class UserDaoTest {
// DAO는 같은걸 사용하니 인스턴스 변수로 선언한다.
private UserDao dao;
// @Test 메소드가 실행되기전에 @BeforeEach 실행된다.
@BeforeEach
public void setUp() {
// 각 테스트 코드마다 중복되었던 코드를 @BeforeEach 로 옮긴다.
ApplicationContext context = new GenericXmlApplicationContext("toby02/applicationContext.xml");
dao = context.getBean("userDao", UserDao.class);
}
@Test
public void addAndGet() throws Exception {
...
}
@Test
public void getUserFailure() throws SQLException, ClassNotFoundException {
...
}
}
JUnit은 @Test를 실행 전 후에 @Before, @After를 자동으로 호출한다. 이런 기능을 이용하면 중복 코드를 하나로 효율적으로 관리할 수 있다. 참고할 점은 테스트 메소드를 실행할 때마다 오브젝트를 새로 만드는데 이유는 테스트가 서로에게 영향을 주지 않고 독립적으로 실행하기 위해서다.
만약 공용으로 사용되는 코드가 있다면 공용 메서드로 분리하거나 테스트 클래스 전체에 걸쳐 딱 한 번만 실행되는 @BeforeAll 스태틱 메서드를 사용한다.
테스트 코드 수행 순서
1. 테스트 클래스에서 @Test 붙은 메소드를 찾는다.
2. 테스트 클래스의 오브젝트를 하나 만든다.
3. @Before 메소드가 있으면 실행한다.
4. @Test 메소드를 호출하고 결과를 저장한다.
5. @After 메소드가 있으면 실행한다.
6. 나머지 테스트 메소드에 대해 2~5번을 반복한다.
7. 모든 테스트 결과를 종합해서 돌려준다.
스프링 테스트 적용
스프링은 JUnit을 이용하는 테스트 컨텍스트 프레임워크를 지원한다.
@RunWith(SpringJunit4ClassRunner.class)
@ContextConfiguration(location="/application.xml")
public class UserDaoTest {
@Autowired
private ApplicationContext context;
private UserDao dao;
@BeforeAll
public void setUp() {
dao = context.getBean("userDao", UserDao.class);
}
...
}
@RunWith는 JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 어노테이션이다.
@ContextConfiguration은 자동으로 만들어줄 애플리케이션 컨텍스트의 파일을 지정한다.
이를 사용하면 모든 테스트 클래스에서 같은 설정 파일을 갖는 애플리케이션 컨텍스트는 모두 공유가 된다.
@Autowired는 스프링 DI에 사용되는 특별한 애노테이션이다. vol.2 권에서 다뤄질 내용으로 넘어간다.
DI와 테스트
지금까지 초난감DAO를 개선해오면서 의존관계를 느슨하기 위해 인터페이스로 연결하고 DI를 적용했다. 거기엔 3가지 이유가 존재한다.
1. 소프트웨어 개발에서 변하지 않은 것은 없기 때문에 언젠가 변경이 필요할 때 시간과 비용을 부담을 줄여줄 수 있다.
2. 구현 방식이 변하지 않아도 인터페이스를 두고 DI를 이용하면 다른 서비스의 기능을 도입할 수 있기 때문이다.
3. 테스트 때문이다. DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들고 실행하는데 중요한 역할을 한다.
테스트를 위한 별도의 DI 설정
테스트 환경에서는 applicationContext.xml를 함부로 사용할 수 없는 경우가 많다. 테스트가 실환경에 영향을 주면 안 되기 때문이다. 테스트를 위한 애플리케이션 컨택스트를 만들어보자. 기존의 설정 파일 복사해서 이름을 변경하고 데이터베이스 경로를 변경한다.
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="dirverClass" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/testdb"/>
<property name="username" value="spring"/>
<property name="password" value="book"/>
</bean>
이제 테스트 클래스의 설정파일 경로를 테스트용 설정 파일로 바꾼다.
@RunWith(SpringJunit4ClassRunner.class)
@ContextConfiguration(location="/test-applicationContext.xml")
public class UserDaoTest {
학습 테스트 활용
테스트 코드는 개발자의 코드를 확인하는 용도에 그치지 않고 프레임워크나 라이브러리를 확인하는 테스트를 작성할 수 있는데, 이런 경우를 학습 테스트라고 한다.
학습 테스트는 프레임워크나 라이브러리의 사용 방법을 익히고, 제대로 사용하는지 검증하는 게 목적이다.
장점
- 다양한 조건에 따른 기능을 손쉽게 확인한다.
- 학습 테스트 코드를 개발 중에 참고할 수 있다.
- 프레임워크나 제품 버전을 변경할 때 호환성 검증을 할 수 있다.
- 테스트 작성에 좋은 훈련이 된다.
- 새로운 기술을 공부하는 과정이 즐거워진다.
버그 테스트 활용
코드에 오류가 있을 때 오류를 잘 드러내 줄 수 있는 테스트가 버그 테스트다. 버그 테스트는 실패하도록 작성한 테스트다. 그리고 버그 테스트가 성공할 수 있도록 코드를 수정한다 성공하면 버그는 해결된 것이다.
장점
- 테스트의 완성도가 높아진다
- 버그를 명확하게 분석한다
- 기술적인 문제 해결에 도움이 된다.
정리
테스트에 대해 간략히 알아본 시간이었다.
정리하면 다음과 같다.
- 테스트는 자동화되면서 빠르게 실행되어야 한다.
- JUnit 프레임워크를 이용한 테스트 작성이 편리하다
- 테스트 결과는 일관성이 있어야 한다.
- 테스트는 포괄적으로 작성해야 한다. 검증이 안된 테스트는 없는 것보다 못하다
- 코드 작성과 테스트 수행 간격이 짧을수록 좋다. (변경이 있을 때마다 테스트로 확인해보자)
- 테스트 주도 개발 방법론도 존재한다
- 테스트 편의성 기능 @Before, @After 등이 존재한다
- 동일한 설정 파일을 사용하는 테스트는 애플리케이션 컨택스트를 공유한다.
- 학습 테스트로 기술 학습에 도움이 된다.
- 버그 테스트로 버그를 분석할 수 있다.
'서적 > 토비의 스프링' 카테고리의 다른 글
토비의스프링 vol.1 - 1장, 오브젝트와 의존관계 (0) | 2021.06.25 |
---|---|
토비의 스프링 포스팅을 시작하며 (0) | 2021.06.19 |