2장은 테스트에 관련된 내용입니다.
실습은 아래에서 확인할 수 있습니다.
https://github.com/happysubin/book-study/commit/d31fcf6587d719e39cb28e62abc7ff7634b7c179
Feat: 토비의 스프링 2장 학습 마무리 · happysubin/book-study@d31fcf6
Show file tree Showing 4 changed files with 127 additions and 33 deletions.
github.com
스프링이 제공하는 가장 중요한 가치는 객체지향과 테스트다.
이번 2장에서는 스프링이 제공하는 가장 중요한 가치 중 하나인 테스트에 대해 살펴본다.
애플리케이션은 요구사항이 들어오면서 변하고 복잡해진다.
테스트 코드를 잘 작성하면 우리가 만들어놓은 코드에 대한 자신감을 바탕으로 변화에 유연하게 대처할 수 있다.
2.1 UserDaoTest 다시 보기
우리는 1장에서 계속 main 메서드에 작성한 테스트 코드를 기반으로 우리가 기대한 값이 맞는지 테스트를 계속 하며 리팩토링을 진행했다.
덕분에 설계와 코드를 개선했고, 스프링을 적용할 수 있었다.
테스트를 통해 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인해서, 만든 코드에 확신을 얻을 수 있었다.
테스트의 결과를 통해 코드나 설계에 결함을 파악하고 디버깅도 진행했다.
그러나 아직 UserDaoTest 클래스의 특징은 다음과 같다.
- main 메서드를 이용한다
- 테스트할 대상인 UserDao의 오브젝트를 가져와 메서드를 호출한다.
- 테스트에 사용할 입력 값(User 오브젝트)을 직접 코드에서 만들어 넣어준다.
- 테스트의 결과를 콘솔에 직접 출력한다.
- 각 단계의 작업이 에러 없이 끝나면 콘솔에 성공 메시지를 출력한다.
중요한 부분은 main 메서드를 이용해 쉽게 테스트를 수행했고 테스트할 대상인 UserDao를 직접 호출한다는 점이다.
웹을 통한 DAO 테스트 방법의 문제점
웹 DAO를 테스트하려면 보통 서비스, 프레젠테이션, 뷰 계층 코드까지 모두 작성하고 화면에 직접 값을 입력해 테스트한다.
우리는 DAO 테스트만 진행하고 싶은데 작성할 코드도 너무 많고 번거롭다. 이것은 큰 문제다.
작은 단위의 테스트
테스트하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트하는 것이 바람직하다.
테스트는 가능하면 작은 단위로 쪼개서 집중해서 해야 한다. 관심사의 분리가 여기서도 적용된다.
이렇게 작은 단위의 코드에 대해 테스트를 수행하는 것을 단위 테스트라고 한다.
단위는 작을수록 좋고, 단위를 넘어서는 다른 코드들은 신경 쓰지 않고, 참여하지도 않고 테스트가 동작할 수 있으면 좋다.
단위 테스트 장점
- 단위 테스트들을 미리 만들고 거대한 테스트를 진행하면 각 단위별로 충분한 검증을 마치고 오류를 잡았으므로 긴 테스트에서 검증할 요소들이 줄어든다.
- 단위 테스트를 진행하면 개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는지 개발자가 빨리 확인이 가능하다.
- 테스트를 만들어 놓으면 매우 작은 단계를 거쳐가더라도 검증을 진행하며 코드를 개선할 수 있다.
- 새로운 기능도 기대한 대로 동작하는지 확인할 수 있을 뿐 아니라, 기존에 만들어뒀던 기능들이 새로운 기능을 추가하느라 수정한 코드에 영향을 받지 않고 여전히 잘 동작하는지를 확인할 수 있다.
자동수행 테스트 코드
프레젠테이션, 뷰 계층과 연관되어 있는 거대한 테스트는 빠르게 테스트를 진행하기 힘들다.
서버를 띄우고 프로그램을 배치하고, 브라우저도 들어가서 값을 입력해야 한다.
그러나 우리가 만든 UserDaoTest 코드는 main 메서드만 수행하면 수 초 이내에 테스트를 수행하고 완료할 수 있다.
그러므로 테스트를 자주 수행해도 부담이 없다. 이렇게 테스트는 자동으로 수행되도록 코드로 만들어지는 것이 중요하다.
최고의 장점은 자주 반복할 수 있다는 것이다. 전체 프로그램의 로직을 바꾸는 1줄의 코드를 변경하더라도 자동 수행 테스트 코드를 작성해놓으면 빠르게 테스트 코드를 수행할 수 있다.
그리고 우리가 작성한 코드처럼 애플리케이션을 구성하는 클래스 안에 테스트 코드를 포함시키는 것보다는 별도로 테스트용 클래스를 만들어서 테스트 코드를 넣는 편이 낫다.
UserDaoTest의 문제점
- 수동 확인 작업의 번거로움이 존재한다. 출력된 성공 메시지를 눈으로 확인해야하기 때문이다.
- 실행 작업의 번거로움이 존재한다. main 메서드를 실행하면 되지만 만약 100개가 넘는 테스트가 존재한다면 모든 main 메서드를 하나하나 실행해야 한다.
2.2 UserDaoTest 개선
조건문을 통해 예상한 값과 다르면 예외를 던지고 맞다면 성공 메시지가 출력되도록 테스트 코드를 리팩토링했다.
즉 테스트 코드를 자동화한 것이다. 예외가 던져지지 않으면 성공이다.
그래도 아직 부족한 것 같다. 그러므로 Junit 테스트 프레임워크를 도입했다.
Junit을 도입하면서 @Test라는 애노테이션을 붙이고 assertThat라는 스태틱 검증 메서드를 사용했다.
한번에 모든 테스트를 실행하는 포괄적인 테스트가 가능해졌다. 더 깔끔하게 자동화된 테스트 코드를 만든 것이다.
2.3 개발자를 위한 테스팅 프레임워크 JUnit
JUnit은 사실상 자바의 표준 테스팅 프레임워크라고 말할 정도로 널리 사용된다.
일관성 있는 테스트를 진행하기 위해 기존의 DB 테이블 레코드를 지우는 deleteAll()이라는 메서드와
DB 테이블 레코드 갯수를 조회하는 getCount()라는 메서드를 만들었다.
deleteAll을 테스트 실행 전에 수행하며 단위 테스트마다 독립된 환경을 구축했다.
이후에는 테스트에서 시나리오를 구상하고 이를 애플리케이션 코드에 반영했다.
이렇게 만들고자 하는 기능의 내용을 담고 있으면서 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고,
테스트를 성공하게 해주는 코드를 작성하는 개발 방법을 테스트 주도 개발(TDD, Test Driven Development)이라고 한다.
기억해야할 부분
- 각 테스트 메서드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다.
- 즉 한 번 만들어진 테스트 클래스의 오브젝트는 하나의 테스트 메서드를 사용하고 나면 버려진다.
- 테스트 클래스가 @Test 테스트 메서드를 두 개 갖고 있다면, 테스트가 실행되는 중에 JUnit은 이 클래스의 오브젝트를 두 번 만드는 것이다. 왜 이렇게 동작하는 것일까?
- 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해 매번 새로운 오브젝트를 만들게 한 것이다.
픽스처
- 테스트를 수행하는 데 필요한 정보나 오브젝트를 픽스처(fixture)라고 한다.
- 일반적으로 픽스처는 여러 테스트에서 반복적으로 사용되기 때문에 @BeforeEach 메서드를 이용해 생성해 두면 편하다.
public class UserDaoTest {
private UserDao dao;
private User user1;
@BeforeEach
void beforeEach(){
//생략...
this.user1 = new User("id1", "root", "1234");
//생략...
}
}
2.4 스프링 테스트 적용
현재 코드에서는 @BeforeEach 메서드에 매번 애플리케이션 컨텍스트를 생성하고 있다.
지금은 매우 가볍지만 만약에 리소스가 큰 빈이 존재한다거나 빈이 많아지고 복잡해지면
애플리케이션 컨텍스트 생성에 많은 시간이 들어갈 것이다.
테스트는 가능한 한 독립적으로 매번 새로운 오브젝트를 만들어서 사용하는 것이 원칙이다.
하지만 애플리케이션 컨텍스트처럼 생성에 많은 시간과 자원이 소모되는 경우에는 테스트 전체가 공유하는 오브젝트를 만들기도 한다.
이때도 테스트는 일관성 있는 실행 결과를 보장해야 하고, 테스트의 실행 순서가 결과에 영향을 미치지 않아야 한다.
다행히도 애플리케이션 컨텍스트는 초기화가 되고 나면 내부의 상태가 바뀌는 일이 거의 없다.
싱글톤으로 만들었으므로 상태를 가지지 않는다. 만약에 상태를 가진다면 그것은 싱글톤으로 등록하면 안된다.
스프링은 Junit을 이용하는 테스트 컨텍스트 프레임워크를 제공한다.
필자의 실습은 Junit5와 스프링 2.78을 사용했으므로 @SpringBootTest 애노테이션을 활용했다.
@Autowired 애노테이션을 사용해 애플리케이션 컨텍스트를 필드 주입해서 편하게 사용했다.
테스트에서 사용되는 애플리케이션 컨텍스트의 주소를 출력해보면 모든 테스트에서 같은 주소가 출력된다.
태스트 클래스에서 애플리케이션 컨텍스트를 공유하는 것이다.
@SpringBootTest
public class UserDaoTest {
@Autowired
ApplicationContext ac;
UserDao userDao;
User user;
User user1;
User user2;
@BeforeEach
void beforeEach() throws SQLException {
userDao = ac.getBean(UserDao.class);
userDao.deleteAll();
user = new User("id1", "subin", "1234");
user1 = new User("id2", "yebin", "2234");
user2 = new User("id3", "bean", "3234");
System.out.println("ac = " + ac); //동일한 주소가 출력됨.
System.out.println("this = " + this); //다른 주소가 출력됨. 테스므 메서드 마다 테스트 클래스 인스턴스를 만들어 실행하기 때문이다.
}
@Test
void count() throws SQLException, ClassNotFoundException {
userDao.add(user);
assertThat(userDao.getCount()).isEqualTo(1);
userDao.add(user1);
assertThat(userDao.getCount()).isEqualTo(2);
userDao.add(user2);
assertThat(userDao.getCount()).isEqualTo(3);
}
@Test
void addAndGet() throws SQLException, ClassNotFoundException {
userDao.add(user);
userDao.add(user1);
assertThat(userDao.getCount()).isEqualTo(2);
User getUser = userDao.get("id1");
assertThat(getUser.getName()).isEqualTo("subin");
assertThat(getUser.getPassword()).isEqualTo("1234");
User getUser2 = userDao.get("id2");
assertThat(getUser2.getName()).isEqualTo("yebin");
assertThat(getUser2.getPassword()).isEqualTo("2234");
}
@Test
void getUserFailure(){
assertThatThrownBy(()->{
userDao.get("hello");
}).isInstanceOf(EmptyResultDataAccessException.class);
}
}
DirtiesContext
- 테스트 메서드에서 애플리케이션 컨텍스트의 구성이나 상태를 변경한다는 것을 테스트 컨택스트 프레임워크에 알려준다.
- 테스트 컨택스트는 이 애노테이션이 붙은 테스트 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않는다.
- 테스트 메서드를 수행하고 나면 매번 새로운 애플리케이션 컨텍스트를 만들어서 다음 테스트가 사용하게 해준다.
순수 애플리케이션 코드와 테스트 코드의 설정을 변경하고 싶다면 최고의 방식은 다른 설정파일, 프로퍼티 파일을 만들어 적용하는 것이다.
컨테이너 없는 DI 테스트
스프링 컨테이너를 사용하지 않고 테스트를 만드는 방식이다. 애플리케이션 컨텍스트 및 스프링과 관련된 코드는 존재하지 않는다.
UserDao는 사실 DAO로서 DB에 정보를 잘 등록하고 잘 가져오는지 확인하면 된다.
스프링 컨테이너에서 UserDao가 동작함을 확인하는 일은 USerDaoTest의 관심사가 아니다.
직접 DataSource를 만들어서 테스트를 진행하면 된다. 마지막 테스트도 깔끔하게 통과한다.
DI는 객체지향 스타일이지 컨테이너가 DI를 가능하게 해주는 것은 아니다.
DI를 이용한 테스트 방법 선택
- 기본적으로 스프링 컨테이너 없이 테스트할 수 있는 방법을 가장 우선적으로 고려하자. 이 방법이 최고로 간결하고 빠르다.
- 여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우가 있다면 스프링의 설정을 이용한 DI를 활용하자.
- 보통 개발환경과 테스트 환경, 운영 환경이 차이가 있기 때문에 각각 다른 설정 파일을 만들어 사용하는 경우가 일반적이다.
2.5 학습 테스트로 배우는 스프링
보통은 자신이 작성한 프로그램에 대한 테스트 코드를 작성한다.
그러나 자신이 만들지 않은 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리 등에 대해서도 테스트를 작성해야 한다.
이런 테스트를 학습 테스트라고 한다.
학습 테스트의 목적은 자신이 테스트를 만들려고 하는 기술이나 기능에 대해 얼마나 제대로 이해하고 있는지,
그 사용 방법을 바로 알고 있는지를 검증하려는 게 목적이다.
또, 테스트 코드를 작성해보면서 빠르고 정확하게 사용법을 익히는 것도 학습 테스트를 작성하는 하나의 목적이다.
학습 테스트의 장점
- 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.
- 학습 테스트 코드를 개발 중에 참고할 수 있다.
- 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.
- 테스트 작성에 대한 좋은 훈련이 된다.
- 새로운 기술을 공부하는 과정이 즐거워진다.
버그 테스트
- 버그 테스트란 코드에 오류가 있을 때 그 오류를 가장 잘 드러낼 수 있는 테스트를 말한다.
- 버그 테스트는 일단 실패하도록 만들어야 한다.
- 그러고 나서 버그 테스트가 성공할 수 있도록 애플리케이션 코드를 수정한다.
- 장점은 다음과 같다.
- 테스트의 완성도를 높여주고, 버그의 내용을 명확하게 분석하게 해주며, 기술적인 문제를 해결하는 데 도움이 된다.
이상으로 포스팅을 마칩니다. 감사합니다.
참고 자료
토비의 스프링 3.1 Vol. 1 (2장 테스트)
댓글