-
토비의 스프링[2] 테스트Spring 2020. 12. 23. 19:11
서론
효율적인 테스트 방법과 기능에 대한 확신을 주는방법에 대해 알아보자.
** 본 예제는 SpringBoot JUnit5로 진행했습니다. **
DAO 테스트 문제점
DAO를 테스트 하기 위하여 JSP 뷰 등 모든 레이어의 기능을 다 만들고 테스트를 할 경우 어디서 문제가 발생 했는지를 찾아내는 수고도 필요하며, 너무 많은 코드를 구성했기에 비효율적이다.
단위 테스트
1. 한 가지 관심에 집중할 수 있는 작은 단위의 테스트다.
2. 크기가 어느정도인지 정확하게 정해진건 아니다.
3. 일반적으로 단위는 작을수록 좋다.
4. 매번 DB의 상태가 달라지고, 테스트를 위해 DB를 특정 상태로 만들 수 없다면 단위 테스트로서 가치가 없어진다.
자동수행 테스트 코드
1. 매번 웹 화면을 통한 테스트를 진행하는것에 비하여 main() 메소드를 실행하는 가장 간단한 방법만으로 테스트가 가능하기에 편리하다.
2. 별개의 클래스로 테스트 코드를 작성했기에 수정시에도 테스트외 문제가 발생하지 않는다.
3. 언제든 코드를 수정하고 테스트를 할 수 있기에 전체 기능에 영향없이 테스트 후 수정이 가능하다.
JUnit프레임워크
자바로 단위 테스트를 지원해주는 도구이자 프레임워크이다.
프레임워크(의 동작 원리는 IoC이다)는 개발자가 만든 클래스에 대한 제어 권한을 넘겨받아서 주도적으로 애플리케이션의 흐름을 제어한다. 따라서 프레임워크에서 동작하는 코드는 main() 메소드도 필요 없고 오브젝트를 만들어서 실행시키는 코드를 만들 필요도 없다.
요구 조건
1. public으로 선언(JUnit4의 경우 명시를 해주어야함)
2. return이 void
3. 파라메터 0개
4. @Test 어노테이션 적용
실행순서
1. @Test 메소드 검색(public, void, none parameter)
2. 테스트 클래스의 오브젝트 하나 생성
3. @BeforeEach가 붙은 메소드가 실행
4. @Test 메소드 실행
5. @After가 붙은 메소드가 있으면 실행
6. 나머지 테스트 메소드에 대하여 반복
7. 모든 테스트의 결과를 종합하여 반환
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; public class UserDaoTest { @Test public void addAndGet() throws SQLException, ClassNotFoundException { ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class); UserDao dao = context.getBean("userDao", UserDao.class); // given User user = new User(); user.setId("inwoo"); user.setName("전인우"); user.setPwd("jiw"); dao.add(user); // when User user2 = dao.get("inwoo"); // then // 검증코드인 assertThat()은 첫 번째 파라미터의 값을 뒤에 나오는 매처라고 불리는 조건으로 비교해서 //일치하면 넘어가고, 아니면 테스트가 실패하도록 만들어준다. is()는 매처의 일종으로 equals() 기능이다. assertThat(user2.getName(), is(user.getName())); assertThat(user2.getPwd(), is(user.getPwd())); }
assertThat의 첫 번째 파라미터와 매처의 조건이 일치하지 않는경우 AssertionError가 발생하며, 차이점을 확인할 수 있다.
테스트 결과의 일관성
별도 DB 서버에 문제가 있거나 외부 상태적 예외의 경우 일부 테스트가 실패할 수 있지만 그렇지 않은경우 동일한 테스트를 시도했을때 테스트의 결과가 성공하도 실패하기도 한다면 이는 좋은 테스트라 할 수 없다.
이처럼 테스트는 외부의 문제가 발생하지 않는이상 계속해서 일관적인 결과를 제공해주어야 한다.
@Test @DisplayName("토비의 스프링 예제 deleteAllAndGetCount") public void addAndGet2() throws SQLException, ClassNotFoundException { ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class); UserDao dao = context.getBean("userDao", UserDao.class); dao.deleteAll(); assertThat(dao.getCount(), is(0)); User user = new User(); user.setId("inwoo"); user.setName("전인우"); user.setPwd("jiw"); dao.add(user); assertThat(dao.getCount(), is(1)); User user2 = dao.get("inwoo"); assertThat(user.getName(), is(user2.getName())); assertThat(user.getPwd(), is(user2.getPwd())); }
테스트의 결과
JUnit은 특정한 테스트 메소드의 실행 순서를 보장해주지 않는다. 그렇기에 테스트의 결과가 실행 순서에 영향을 받는다면 테스트를 잘몬 만든 것이다.
테스트의 예외
get()메소드의 경우 id값에 해당하는 정보를 가져오지만 정보가 없는경우 2가지의 방법으로 해결할 수 있다.
1. null과 같은 특정 값을 return
2. Exception(예외처리)
// 2.excpetion 처리 예제 @Test @DisplayName("get예외 예제") public void getUserFailed() throws SQLException, ClassNotFoundException { ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class); UserDao dao = context.getBean("userDao", UserDao.class); dao.deleteAll(); assertThat(dao.getCount(), is(0)); // EmptyResultDataAccessException 이 발생 하게끔 Dao 를 구성 EmptyResultDataAccessException thrown = Assertions.assertThrows(EmptyResultDataAccessException.class, () -> { dao.get("??unknownId??"); }); Assertions.assertThrows(EmptyResultDataAccessException.class, () -> { dao.get("??unknownId??"); }); }
포괄적인 테스트
단순한 DAO 코드일수록 DAO에 대한 포괄적인 테스트를 만들어 두는 편이 안전하고 유용하다.
모든 테스트는 자신의 코드에서 발생할 수 있는 모든 경우의 수의 상황과 입력값을 생각하여 테스트를 제작해야 한다.
로드존슨(스프링 창시자) : "항상 네거티브 테스트를 먼저 만들라"
테스트 코드 리팩토링
애플리케이션 코드만이 리팩토링의 대상은 아니다. 필요하다면 테스트 코드도 언제든지 내부구조와 설계를 개선하여 깔끔하고 이해하기 쉽게 변경할 필요가 있다.
JUnit4 -> JUnit5
@Before -> @BeforeEach
@After -> @AfterEach
픽스처
테스트를 수행하는 데 필요한 정보나 오브젝트를 칭하는말(매 테스트 메소드마다 필요한 정보) ex)UserDao
이러한 픽스처는 테스트 코드마다 중복된 코드가 존재하기에 @BeforeEach로 추출할 수 있다.
public class UserDaoTest { private UserDao dao; // 테스트 메소드마다 주입되는 DAO 코드를 테스트 메소드 시작전에 주입을 해준다. @BeforeEach public void setUp() { ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class); this.dao = context.getBean("userDao", UserDao.class); } }
스프링 테스트 적용
@BeforeEach 메소드가 테스트 메소드만큼 반복되기에 반복되는 만큼의 애플리케이션 컨텍스트도 생성된다.
애플리케이션 컨텍스트가 생성이 될때 모든 싱글톤 빈 오브젝트를 초기화 하기에 특정 빈은 오브젝트가 생성될 때 자체적인 초기화 작업을 진행해서 많은 시간이 필요로 할 수 있다.
테스트는 가능한 독립적으로 매번 새로운 오브젝트를 생성해서 사용하는 것이 원칙이지만 시간과 자원이 많이 필요한 애플리케이션 컨텍스트 처럼 특정 오브젝트들은 공유해서 사용하기도 한다.
@SpringBootTest // 자동으로 만들어줄 애플리케이션 컨텍스트의 설정파일 위치 @ContextConfiguration(classes = { DaoFactory.class }) public class UserDaoTest { private UserDao dao; @Autowired private ApplicationContext context; @BeforeEach public void setUp() { System.out.println(this.context); System.out.println(this); this.dao = context.getBean("userDao", UserDao.class); }
**JUnit @SpringBootTest 어노테이션에는 JUnit4 @RunWith가 확장되어 사용되는 @ExtendWith가 포함되어 있습니다.**
context는 동일하지만 UserDaoTest 오브젝트는 매번 생성되어 사용이 된다. 그렇기에 JUnit이 애플리케이션 컨텍스트를 만들어두고 첫 번째 테스트에서 애플리케이션 컨텍스트의 생성하고 계속 동일한 객체를 사용하기에 두 번째 테스트부터는 실행시간이 감소된걸 볼 수 있다.
테스트 클래스의 컨텍스트 공유
여러 개의 테스트 클래스가 있어도 모두 같은 설정파일을 가진 애플리케이션 컨텍스트를 사용한다면 스프링은 서로 다른 테스트 클래스가 같은 애플리케이션 컨텍스트 파일을 공유하게 해준다.(성능향상)
@Autowired
스프링의 DI에 사용되는 어노테이션
어노테이션이 붙은 인스턴스 변수가 있으면, 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다. 타입이 일치하는 빈이 있으면 인스턴스 변수에 주입해준다. 생성자, 세터 주입 없이 주입이 가능하며 별도의 DI 설정 없이 필드의 타입정보를 이용해 빈을 자동으로 가져오는 자동와이어링을 지원한다.
@SpringBootTest @ContextConfiguration(classes = { DaoFactory.class }) public class UserDaoTest { @Autowired private UserDao userDao; }
스프링 애플리케이션 컨텍스트는 초기화 시 본인도 빈으로 등록하기에 어노테이션을 이용해 DI가 가능하다.
그렇기에 다른 빈도 직접 DI가 가능하다.
@Autowired는 변수에 할당 가능한 타입을 가진 빈을 자동으로 찾는다. 단 같은 타입의 빈이 2개 이상 있는 경우에는 타입만으로는 어떤 빈을 가져올지 결정할 수 없다. 이 경우 변수 이름과 같은 빈을 찾으며 그마저 해당하지 않는다면 예외가 발생한다.
DI와 테스트
테스트에서 운영용 DataSource를 사용할 경우 delete 메소드를 잘못 사용하는 것 만으로도 모든 데이터가 사라지는 일이 발생할 수 있다.
그렇기에 운영용 DataSource가 아닌 테스트용 DataSource를 사용하기 위한 DI 설정방법으로는 대표적으로 3가지가 존재한다.
1. 수동 DI
2. 테스트용 별도 DI 설정파일
3. 스프링 컨테이너를 사용하지 않는 DI
// 1. 수동 DI // ApplicationContext 공유해제 매번 새로운 ApplicationContext 객체 생성 @DirtiesContext @ContextConfiguration(classes = { DaoFactoryTest.class }) @SpringBootTest public class UserDaoTest2 { @Autowired UserDao userDao; @BeforeEach public void setUp() throws SQLException { DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost:3306/sys?serverTimezone=UTC&characterEncoding=UTF-8", "root", "1234", true); // 수동 DI 주입 userDao.setDataSource(dataSource); } } // 2. 별도 테스트용 DB 사용 @ContextConfiguration(classes = { DaoFactory.class }) // 운영용 @ContextConfiguration(classes = { DaoFactoryTest.class }) // 테스트용 @SpringBootTest public class UserDaoTest2 {} // 3. 스프링 컨테이너를 사용하지 않는 DI public class UserDaoTest3 { UserDao dao; @BeforeEach public void setUp() throws SQLException { dao = new UserDao(); dao.setDataSource(new SingleConnectionDataSource("jdbc:mysql://localhost:3306/sys?serverTimezone=UTC&characterEncoding=UTF-8", "root", "1234", true)); }
테스트에서 사용하는 3가지 DI 주입 방법은 모두 장단점이 존재한다. 하지만 테스트 수행 속도가 가장 빠르며 간결한 스프링 컨테이너 없이 테스트를 하는 방법을 우선적으로 고려해야 한다.
학습 테스트
개발자 자신이 만들지 않은 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리에 대한 테스트를 작성하는일
기능 테스트의 용도보다는 자신의 기술, 기능에 대한 지식을 점검하거나 학습하는 용도
장점
1. 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.
1. 자동화된 테스트의 모든 장점이 학습 테스트에도 그대로 적용된다. 학습 테스트는 자동화된 테스트 코드로 만들어지기 때문에 다양한 조건에 따라 기능이 어떻게 동작하는지 빠르게 확인할 수 있다.
2. 학습 테스트 코드를 개발 중에 참고할 수 있다.
1. 수동으로 예제를 만들며 코드를 계속 수정해가며 기능을 테스트할 경우 최종 수정한 예제 코드만 남아있는 반면 학습 테스트는 다양한 기능과 조건에 대한 테스트 코드를 개별적으로 만들고 남길 수 있어 실제 개발에서 샘플 코드로 참고할 수 있다.
3. 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.
1. 2번과 동일하게 업그레이드 이후 남아있는 테스트 코드를 통해 검증을 할 수 있다.
4. 테스트 작성에 대한 좋은 훈련이 된다.
5. 새로운 기술에 대한 학습과정이 즐거워진다.
JUnit 테스트 오브젝트 테스트
테스트를 매번 실행할 때마다 새로운 오브젝트를 만드는 JUnit 검증
@ContextConfiguration(classes = { DaoFactory.class }) @SpringBootTest public class JUnitTest { @Autowired ApplicationContext applicationContext; static ApplicationContext contextObject = null; static Set<JUnitTest> testObject = new HashSet<>(); @Test public void test1() { assertThat(testObject, not(hasItem(this))); testObject.add(this); assertThat(contextObject == null || contextObject == this.applicationContext, is(true)); contextObject = this.applicationContext; } @Test public void test2() { assertThat(testObject, not(hasItem(this))); testObject.add(this); Assertions.assertTrue(contextObject == null || contextObject == this.applicationContext); contextObject = this.applicationContext; } @Test public void test3() { assertThat(testObject, not(hasItem(this))); testObject.add(this); assertThat(contextObject, either(is(nullValue())).or(is(this.applicationContext))); contextObject = this.applicationContext; } }
버그 테스트
버그 테스트는 실패하게 만든 이후 해당 버그 테스트가 성공할 수 있도록 수정하는 테스트이다.
장점
1. 테스트의 완성도를 높여준다.
2. 버그의 내용을 명확하게 분석하게 해준다.
3. 기술적인 문제를 해결하는 데 도둠이 된다.
정리
1. 테스트는 자동화돼야 하고, 빠르게 실행할 수 있어야 한다.
2. main() 테스트 실행 보다는 JUnit 프레임워크를 사용하자
3. 테스트 결과는 일관성이 있어야 하며, 포괄적으로 작성해야 한다. (충분한 검증을 하지 않는 테스트는 오히려 없는 것보다 나쁘다.)
4. 테스트하기 쉬우며, 코드 작성과 테스트 수행의 간격은 짧을수록 좋다.
5. 테스트 주도 개발 TDD
6. 스프링 테스트 컨텍스트 프레임워크를 이용하면 테스트 성능을 향상시킬 수 있다.
7. 동일한 설정을 사용하는 테스트는 하나의 애플리케이션 컨텍스트를 사용한다.
8. @Autowired를 사용하여 컨텍스트의 빈을 테스트 오브젝트에 DI할 수 있다.
9. 학습 테스트, 버그테스트
'Spring' 카테고리의 다른 글
토비의 스프링[4] 예외처리 (0) 2021.01.27 토비의 스프링[3] 템플릿 (0) 2021.01.14 토비의 스프링[1] 오브젝트와 의존관계 (0) 2020.12.17 Spring Boot로 알아가는 Swagger (0) 2020.04.07 Spring JPA(5) 게시판 (0) 2020.02.18