커머스 회사에서 개발을 하다보면 테스트 코드의 중요성은 스스로 깨닫게 된다. 그래서 Spring Boot 테스트 기능에 대해 간단히 정리해 보려한다.
테스트 코드는 멋으로 하는거 아니냐?
이렇게 생각하신 분은 아직 테스트 코드를 짤 줄 모르거나 짜지 않아서라 생각한다. 내가 생각하는 테스트 코드의 장점은
- 코드가 자동으로 깔끔해진다. 테스트 코드를 짜려면 결국 한가지 기능을 가진 클래스를 짤 수 밖에 없다.
- 리팩토링에 대한 자신감이 생긴다.
- 다른 개발자들에게는 설명서가 된다.
- (가장크다고 생각하는 부분) 테스트 코드가 없다면 주문/결제 테스트를 위해서 웹사이트에 들어가 상품검색부터 정보 입력까지 다 한 후에 코드를 테스트해야 한다. 한번만 테스트하면 끝이 나는가?? 수정->테스트->수정->테스트의 무한 반복이다. 즉, 이 과정에서 엄청난 리소스가 낭비된다.
- (등등이 있겠지?)
Spring Boot에서 테스트를 해보자
Spring boot에서 테스트 모듈은 spring-boot-test, spring-boot-test-autoconfigure가 존재한다.
그리고 포함되어 있는 라이브러리는
- JUnit
- Spring Boot Test
- AssedrtJ
- Hamcrest
- Mockito
- JSONassert
- JsonPath
가 있다.
@SpringBootTest
spring boot test를 할 때 가장 기본적으로 사용하는 어노테이션이다. 이 부분은 모든 개발자가 잘 알고 있다.
@RunWith(SpringRunner.class)
@SpringBootTest
public class StayGolfOrderServiceImplTest {
SpringBootTest 너무 느려요??
개발하고 있는 프로젝트가 커져 빈이 많아지면, 당연히 IoC컨테이너에 빈을 등록하는데 오랜 시간이 걸린다. ( 뜨는데 2~5초인데 그것도 못참아? 라고 하시는 분들은 테스트 코드를 많이 돌리지 않거나 테스트 코드가 프로젝트에 많지 않아서 일것이다. 이 2~5초차이가 상당히 크다고 생각한다. )
그래서 내가 원하는 빈만을 등록하려면 어떻게 해야할까?
- SpringBootTest 어노테이션에 원하는 class 등록하기
- EnableConfigurationProperties 어노테이션에 원하는 Configuration 빈 적어주기
@RunWith(SpringRunner.class)
@ActiveProfiles("dev")
@SpringBootTest(classes = {StayGolfOrderServiceImpl.class, StayGolfApiServiceConfiguration.class})
@EnableConfigurationProperties(StayGolfConfiguration.class)
public class StayGolfOrderServiceImplTest {
@EnableConfigurationProperties 왜 사용하는걸까?
내가 원하는 빈만 등록해서 사용하지 해놓고, Configuration 컴포넌트가 application.yml에서 값을 못가져오는 문제를 발견하게 될 것이다. ( SpringBootTest(classes=...) 만 선언한 경우 )
무슨 차이 때문에 그렇게 된걸까? 바로 @SpringBootTest 에서 빈을 등록할 때 MainApplication 클래스에 선언된 @SpringBootApplication 어노테이션 때문이다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
여기에 @EnableAutoConfiguration을 들어가보면 AutoConfigurationImportSelector라는 어노테이션을 발견할 수 있다. 해당 어노테이션이 딱 봐도 리소스로더의 구현체라는 것을 알 수 있다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered
즉, 해당 어노테이션을 가지고 있던 MainApplication.class가 빈으로 등록되지 않아 application.yml의 리소스를 가지고 오지 못한것이다.
이를 해결하는 방법이 아래의 어노테이션이다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {
Properties
application.properties 에서 테스트 설정을 따로 만들고 싶은 경우 아래처럼 사용하면 된다.
@RunWith(SpringBoot.class)
@SpringBootTest(properties = "classpath:application-test.yml")
public class ApplicationTest {
...
}
@JsonTest
외부 API 호출과 관련하여 테스트를 할 때, JSON serialization과 deserialization 테스트를 많이 한다. 이때 @JsonTest를 사용하면 편하게 테스트를 할 수 있다. 테스트를 위한 빈으로는 JacksonTester, GsonTest, BasicJsonTester 등이 있다.
@RunWith(SpringRunner.class)
@JsonTest
public class ArticleJsonTest {
@Autowired
private JacksonTester<Article> json;
@Test
public void testSerialize() throws IOException {
Article article = new Article(
1,
"kwseo",
"good",
"good article",
Timestamp.valueOf((LocalDateTime.now())));
// assertThat(json.write(article)).isEqualToJson("expected.json"); 직접 파일과 비교
assertThat(json.write(article)).hasJsonPathStringValue("@.author");
assertThat(json.write(article))
.extractingJsonPathStringValue("@.title")
.isEqualTo("good");
}
@Test
public void testDeserialize() throws IOException {
Article article = new Article(
1,
"kwseo",
"good",
"good article",
new Timestamp(1499655600000L));
String jsonString = "{\"id\": 1, \"author\": \"kwseo\", \"title\": \"good\", \"content\": \"good article\", \"createdDate\": 1499655600000}";
assertThat(json.parse(jsonString)).isEqualTo(article);
assertThat(json.parseObject(jsonString).getAuthor()).isEqualTo("kwseo");
}
}
@DataJpaTest
Spring data JPA를 테스트하려는 경우 @DataJpaTest 기능을 사용하면 된다.
- in-memory embedded database를 기본으로 사용
- @Entity 클래스를 스캔 ( 다른 컴포넌트를 스캔하지 않음 )
- @Transactional 어노테이션을 포함하고 있기 때문에 따로 선언하지 않아도 됨
// 1. Transactional 기능이 필요하지 않은 경우
@RunWith(SpringRunner.class)
@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public class SomejpaTest {
...
}
// 2. TestEntityManager 빈 사용하기
@RunWith(SpringRunner.class)
@DataJpaTest
public class ArticleDaoTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private ArticleDao articleDao;
@Test
public void test() {
Article articleByKwseo = new Article(1, "kwseo", "good", "hello", Timestamp.valueOf(LocalDateTime.now()));
Article articleByKim = new Article(2, "kim", "good", "hello", Timestamp.valueOf(LocalDateTime.now()));
entityManager.persist(articleByKwseo);
entityManager.persist(articleByKim);
List<Article> articles = articleDao.findByAuthor("kwseo");
assertThat(articles)
.isNotEmpty()
.hasSize(1)
.contains(articleByKwseo)
.doesNotContain(articleByKim);
}
}
// 3. real database 사용하기
@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class SomeJpaTest {
...
}
MockBean
테스트 코드를 짤 때 중요한 부분 중 하나가 Mock 생성이라 생각한다. @MockBean 어노테이션을 사용해서 이름 그대로 Mock 객체를 빈으로써 등록할 수 있다.
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ArticleServiceImpl.class)
public class ArticleServiceImplTest {
@MockBean
private RestTemplate restTemplate;
@MockBean
private ArticleDao articleDao;
@Autowired
private ArticleServiceImpl articleServiceImpl;
@Test
public void testFindFromDB() {
List<Article> expected = Arrays.asList(
new Article(0, "author1", "title1", "content1", Timestamp.valueOf(LocalDateTime.now())),
new Article(1, "author2", "title2", "content2", Timestamp.valueOf(LocalDateTime.now())));
given(articleDao.findAll()).willReturn(expected);
List<Article> articles = articleServiceImpl.findFromDB();
assertThat(articles).isEqualTo(expected);
}
}
'Back-End > Spring' 카테고리의 다른 글
Spring DeleteAllBy...In 호출시 에러 ( TransactionRequiredException ) (1) | 2020.02.16 |
---|---|
트랜잭션 전파 속성 ( propagation ), 롤백 예외 (0) | 2020.01.30 |
Spring을 이용하여 개발할 때 고민, 클린코드 짜기, 코드리뷰 항목 (0) | 2020.01.02 |
Spring @Order 어노테이션 (0) | 2019.11.28 |
Spring Security - remember-me에 관하여 ( 로그인 유지하기 ) (2) | 2019.10.18 |