- Today
- Total
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 책
- 캐싱전략
- 젠킨스
- LazyInitialization
- 만들면서 배우는 클린 아키텍처
- 프로젝트
- jenkins
- 스프링
- redis
- 후기
- docker
- chrome80
- Kotlin
- spring
- JWT
- 팀네이버
- EntityTransaction
- infra
- 팀네이버 공채
- websocket
- 리뷰
- Spring Security
- SpringBoot
- container
- Project
- Java
- SPRING JWT
- JPA
- 브랜치전략
- network
PPAK
테스팅 본문
개요
지난 프로젝트를 진행하면서 테스트 코드를 작성하는 방법에 대해서 스스로 만족할 만큼 학습하지 못했다. 일종의 변명이지만 아직 테스팅하는 것에 익숙하지 않아 주어진 기간 내에 요구사항을 구현하면서도 체계적인 테스트 코드 작성을 병행하기에 시간적 여유가 부족했다고 생각한다.
개발을 하면서도 수시로 테스트 코드 작성에 대한 필요성을 느꼈다. 특히 테스팅에 대해서 아예 무지할 때는 구현된 기능의 사소한 변경사항(요청값의 변화, 로직 일부 수정 등등) 이 있을 때마다 애플리케이션을 실행하고 Swagger 나 Postman 을 통해서 요청을 전송하고 결과를 확인하는 과정은 굉장히 불편했다.
왜 그 과정이 불편한가에 대해 생각해봤다
애플리케이션 실행을 위한 모든 설정을 셋팅해야한다
서버가 외부 API(MySql, Redis, Kafka 등등) 와 연결되기 위한 설정을 애플리케이션 구동 시점에서 신경써야하고, 모든 프로세스가 동작중이어야 한다. -> 생산성이 굉장히 떨어지고, 서버 독립적인 테스트가 사실상 불가능한 구조다.
서로 다른 컨텍스트를 가진다
이것도 생각보다 피로도가 쌓이는 문제라고 생각한다. Swagger 는 요청 전송에 필요한 데이터 셋팅을 어느정도 편하게 할 수 있지만, Postman 의 경우 데이터 포맷부터, 전송 데이터의 형태까지 개발자가 신경써야 하는 영역이 많아진다. 또한 첫 번째 문제에서 벗어날 수 없다. 두 플랫폼 모두 어느정도의 환경 셋팅을 통해 불편함을 극복할 수 있지만, 여전히 개발자 코드를 다른 플랫폼에서 테스트 하는 상황이고 그 노력을 테스트 코드 작성에 시간을 쏟는다면 더욱 높은 효용을 얻을 수 있을거라 생각한다.
위에서 언급한 문제점들이 테스트 속도를 늦추고, 개발 생산성을 저하시킨다. 반면에 테스트 코드를 작성하면 위의 문제로부터 어느정도 자유로워 진다.
최근 진행하는 프로젝트에서는 틈틈히 테스팅에 대해 학습하고 완벽하진 않지만 기능 구현에 핵심이 되는 코드 영역에 대해 테스트 코드를 작성하고 있다. 실제로 Mocking 을 통해 외부 프로세스와 환경을 분리하니 애플리케이션 실행을 위해 모든 설정을 해야하는 불편함도 없어졌고, 같은 컨텍스트에서(자바 코드) 테스팅을 실행하니 속도도 확실히 빠른 것을 느낄 수 있었다. 또한 요구사항에 따라 추가된 기능이 기존의 기능 수행에 영향이 없는지 테스트를 돌려 확인할 수 있으니 프로젝트를 빌드하고 배포하는 과정에서 자신감을 얻을 수 있겠다는 생각이 들었다.
따라서 이번 포스팅에서는 테스팅에 대해서 학습한 내용을 간략하게 정리하고, 내가 테스트 코드를 작성하는 방법을 경우에 따라 나누어 설명해보고자 한다.
테스트 코드 작성의 필요성
이미 테스트 코드 작성의 필요성을 강조하는 수많은 칼럼들이 존재한다는 것을 안다.
설마 아직도 테스트 코드를 작성 안 하시나요? 에서는 살면서 욕을 먹고 싶을 때면 훌륭한 개발자를 잡아놓고 “저희는 테스트 코드 안 짜요.” 라고 한 마디를 건네보라고 한다.
사람들은 어떤 작업의 필요성을 느끼기 위해 트레이드오프를 고려한다. 테스트 코드 작성은 시간이 오래 걸린다. 이에 반해 효용을 얻기 위해서는 오랜 시간이 걸릴 수도, 혹은 프로덕션 단계까지 이어지지 않았다면 아예 불필요한 작업으로 판단될 수 있기 때문에 나를 포함한 테스팅을 처음 접하는 개발자들이 테스트 코드 작성의 효용성을 쉽게 느끼지 못하는 것이 아닌가 하는 생각이 들었었다.
하지만 테스트 코드가 일종의 품질 보증서의 역할을 수행한다고 생각하면 생각이 완전 달라질 수 있다고 생각한다. 테스트 코드를 모두 통과하면 (적어도 내가 예측하는 범위 내에) 시스템에 장애가 발생하지 않을 것이라고 보증할 수 있다. 통과하지 못하면 오히려 좋다. 좋은 품질 보증서로 거듭나기 위해 보완해야할 영역이 테스트 코드 결과에 드러나니 빠르게 피드백 받고 빠르게 수정할 수 있다.
처음에도 언급했듯이 무엇보다 테스트 코드를 작성하면 위의 작업을 더 철저하고, 빠르게 수행할 수 있다.
서비스마다, 팀마다 테스트 코드를 작성함으로써 얻는 효용은 저마다 다를 수 있지만 여러 가지 이점 중 몇 가지를 정리해봤다.
개발 과정 중 예상치 못한 문제를 미리 발견할 수 있다
테스팅의 근본적 목적이라고도 생각한다. 하기 이점들과 함께 테스팅을 통해 구현한 로직을 수행하면서 발생할 수 있는 문제를 사전에 찾아낼 수 있다.
작성한 코드가 의도한 대로 작동하는지 검증할 수 있다
개발자는 하나의 로직을 개발하기 위해 여러 컴포넌트와 협력할 수 있다. 그 과정에서 로직은 복잡해지고 실수가 발생할 수 있다. 테스팅은 작성된 로직에서 테스트 하고자 하는 영역을 한정시킬 수 있기 때문에 하나의 복잡한 로직을 테스트하더라도 로직 내에서 영역을 나눠 독립적인 테스트가 가능하다.
코드 변경 시, 변경 부분으로 인한 영향도를 쉽게 파악할 수 있다
굉장히 공감하는 이점 중 하나다. 코드 작성 간 개발자가 결합도를 고려하고 로직을 작성한다고는 하지만 놓치는 부분이 생길 수 있다. 가령 테스트 코드가 존재하지 않을 때 B 클래스의 Method_1 을 여러 클래스에서 호출하고 있다면 Method_1 의 로직이 수정될 때마다 일일이 호출부를 탐색해가며 변경에 영향이 없는지 확인해야할 수 있다. 하지만 테스트 코드가 존재한다면, 기존의 동작과 어느정도(이렇게 표현한 이유는 아래에서 다시 기술) 동일하다는 것을 보장한다. 따라서 테스트 코드가 모두 통과하면 개발자는 자신있게 배포할 수 있다.
아래는 테스트 코드 작성의 이점이라고 할 수는 없지만 개인적으로 생각하는 이점이고, 스스로 테스트 코드 작성을 독려하는 이유이기도 하다
더 나은 개발자가 되기 위해
지난 포스팅에서도 언급했듯 올해 나의 목표는 "현실적으로 운영가능한 시스템(혹은 코드)을 만들 수 있는 개발자가 되는 것" 이다. 지난 날의 개발 과정이 개발자를 흉내내는 모습이었다면, 이제는 지속성을 트레이드 오프 목록에 넣을 수 있는 더 나은 개발자가 되고 싶다.
테스트 코드 작성은 이러한 더 나은 개발자로 가는 하나의 단계라고 생각한다.
테스트 코드 작성법
테스트 코드를 작성하는 방법에는 여러 가지가 있는 것으로 알고 있다. 또 개개인의 스타일마다 다를 수 있는데 본문에서는 직접 사용하는 테스트 코드 작성법에 대해서 소개해보고자 한다.
FIRST
먼저 테스트 코드를 작성하기 위해 지켜야하는 규칙 중 유명한 FIRST 에 대해서 알아보자
Fast: 테스트는 빠르게 수행돼야 한다
통합 테스트는 필요할 때 이외에는 가급적 피하고, 유닛 테스트를 통해서 코드를 검증하는 것이 좋다는 것을 이야기한다.
유닛 테스트를 위해서는 하나의 로직에서 의존하는 외부 컴포넌트로부터 독립된 테스팅 환경을 구축해야 하는데 이를 Mockito 라이브러리를 통해서 구현할 수 있다.
Independent: 각각의 테스트는 독립적이며 서로 의존해서는 안된다
테스트 간 여러 테스트에 사용되는 객체의 상태가 변경되는 경우에 그 상태가 다음 테스트를 실행할 때는 다시 원래 상태로 되돌아 가야하는 것을 의미한다.
즉 어느 테스트가 다른 테스트에 영향을 주어서는 안된다는 것이다. @BeforeEach, @AfterEach 와 같은 어노테이션을 통해서 테스트의 시작과 끝에 삽입될 연산을 추가해서 구현할 수 있다.
Repeatable: 어느 환경에서도 반복 가능해야 한다
테스트를 여러번 반복해도 같은 결과를 내야 하는 것을 의미하는데, 나는 테스트 과정에서 테스트에 사용될 데이터 영역에 변경 사항을 남기면 안되는 것으로 이해했다.
가령, 데이터베이스와 통신해야하는 코드에서는 트랜잭션이 종료되는 시점에 테스트 이전 시점으로 롤백을 수행하는 것을 의미한다.
테스트 메소드에 @Transactional 을 추가하거나, @DataJPATest 와 같은 어노테이션을 활용할 수 있다.
Self-Validating: 테스트는자체적으로 검증되고 bool 과 같은 형태로 결과를 산출해야한다.
모든 테스트 과정은 테스트 메소드 내에서 검증되고 결과가 산출돼야한다.
Assertion 을 통해서 테스트 코드 내에서 검증과 결과를 산출할 수 있다.
Timely: 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다
프로덕션(실제 로직을 수행하는) 코드가 작성되기 직전에 테스트 코드를 작성하라는 것으로, TDD 방식으로 개발하는 경우 따라야하는 규칙이다.
Unit Test 그리고 Integration Test
유닛 테스트와 통합 테스트로 불리는 두 테스팅 방법은 목적에 따른 수행방식의 차이에 의해 나뉜다고 볼 수 있다
Unit Test(유닛 테스트)
보통 클래스 내의 단일 함수를 테스트하는 것을 의미한다. 때문에 그 속도가 빠르고 테스트 범주가 좁기 때문에 코드를 보고 명확하게 무엇을 테스팅 하는지 파악하기가 쉽다. 작성하기 비교적 쉽고 테스팅 속도가 빠르기 때문에 통합 테스트 수보다 유닛 테스트의 수가 훨씬 많다
단일 함수 내에서 동작하는 로직에 대해서 검증을 수행하는 경우가 대다수기 때문에 보통 외부로 보내는 요청에 대한 응답을 Mocking 해서 테스트를 진행한다.
Integration Test(통합 테스트)
단위 테스트가 불가능한 외부 API 를 직접 호출하고 데이터의 흐름이 의도한 바와 같이 흘러가는지 확인하는 테스트 방식을 의미한다. 쉽게 이야기하면 단위 테스트 시 사용했던 Mocking 방식이 아니라 직접 외부 API 나 다른 객체로 요청을 보낸 것의 응답을 사용해 테스트 한다.
따라서, 단위 테스트보다 테스트 환경을 셋팅하는 시간도 오래 걸리고, 실제 테스팅 하는 시간 자체도 유닛 테스트보다 훨씬 길다.
애플리케이션을 실행한 뒤 Swagger 나 Postman 을 통해 요청-응답을 통해 테스트 했던 것도 한편으로 보면 (비효율적인)통합 테스트로 볼 수 있겠다.
정리
유닛 테스트와 통합 테스트는 처음 이야기한 것처럼 목적에 따라 나뉘는 테스팅 방식으로 볼 수 있다.
단위 테스트는 테스트 코드 실행 속도가 매우 빠르다는 것 자체로 유닛 테스트를 작성하는 목적이 될 수 있다. 의도한 바대로 원하는 영역의 테스트를 수행할 코드를 작성할 수 있다면 빠른 시간 내로 로직을 테스트할 수 있다.
통합 테스트는 느리지만, 운영 환경과 거의 동일한 상태에서 테스팅을 수행하기 때문에 유닛 테스트 코드 작성 간 개발자가 놓칠 수 있는 의존 함수의 동작 또한 테스팅할 수 있기 때문에 더 견고한 테스팅이 가능하다.
실제로 위와 같은 특성 때문에 구글에서는 단위 테스트와 통합 테스트의 비율을 7:2 정도로 유지하면서 테스트 코드를 작성할 것을 추천한다.
테스트 코드
아래 기술할 테스트 코드 작성 방법은 아직 테스트 코드 초기 학습 단계에서 작성한 것으로 참고만 해주시면 감사하겠습니다.
given - when - then
단일 테스트 코드를 작성하는 기본 틀 중에 하나다. 이 틀을 유지하면서 테스트 코드를 작성하면 스스로 코드를 작성하기도 편리하고, 다른 사람과 테스트 코드를 공유하기도 좋다고 생각한다.
given(준비): 테스트에 필요한 데이터를 정의한다. (요청값, Mocking 한 객체의 행동 등)
when(실행): 테스트 하고자 하는 메소드를 호출한다.
then(검증): 그 때 예측된 결과를 실제값과 비교, 검증한다.
테스트 코드를 작성하다 보면 여러 형태의 로직에 대해 테스팅을 수행해야 하는데, 아래와 같은 패턴을 가질 때가 많다고 느껴서 정리해보고자 한다.
외부 API 만 호출하는 경우
로직 내에서 외부 API 와 호출에 필요한 코드를 작성한 경우 호출이 정상적으로 수행되는지 테스트를 위해서 직접 외부 API 를 호출해보는 방법밖에 없다.
나는 이 때 외부 API 로 인해 상태가 변경되는 요청과 그렇지 않은 요청으로 구분하고, 통합 테스트를 수행한다.
이 때 원격 환경에서 테스트가 가능한 데이터를 준비해놓고, 해당 데이터에 대해 테스트를 수행한다.
최근 진행한 프로젝트에서 NFT 발행을 위해 블록체인 네트워크를 사용한 적이 있는데, 블록체인 네트워크 특성상 한번 전송된 트랜잭션은 롤백이 되지 않는다. 이 경우 최대한 정확한 데이터를 구성해 테스트를 진행할 수 있도록 테스트 코드 내부에 전송 데이터를 한 곳에서 관리할 수 있도록 메소드를 추가해 어떤 데이터인지 나타내는 것도 좋아보인다.
@SpringBootTest
@DisplayName("블록체인 네트워크 내 NFT 전송/조회 테스트")
public class NftTest {
@Autowired
NftServiceImpl nftService;
@Test
@DisplayName("블록체인 네트워크 통신, 존재하는 NFT 를 조회합니다")
public void NFT_조회() {
//given
Long nftId = getTestNftId();
//when
QueryNftRes res = nftService.queryNft(nftId);
//then
assertEquals(getTestDesc(), res.desc());
}
@Test
@DisplayName("블록체인 네트워크 통신, 존재하지 않는 NFT 를 조회합니다")
public void 존재하지않는_NFT_조회() {
//given
Long nftId = getNotExistNftId();
//when
IllegalStateException res = assertThrows(IllegalStateException.class,
() -> nftService.queryNft(nftId));
//then
assertEquals(res.getMessage(), "NFT Not Found");
}
@Test
@DisplayName("블록체인 네트워크 통신, NFT 를 전송합니다")
public void NFT_전송() {
//given
String sendAddr = getTestNftAddr();
Long sendNftId = getTestNftId();
//when
nftService.transferNft(sendAddr, sendNftId);
QueryNftRes res = nftService.queryNft(sendNftId);
//then
assertEquals(sendAddr, res.owner());
}
}
통합 테스트는 이미 스프링에서 제공되는 DI 나 AOP 와 같은 기능을 함께 사용해 실제 런타임에서 메소드를 호출하는 것과 크게 다르지 않기 때문에 의외로 테스트 코드가 단순할 수 있다. 하지만 여러 메소드와 결합된 로직의 경우 테스트 실패 원인을 확정하는데 다소 시간이 걸릴 수 있다.
완벽한 테스트는 불가능하다
일곱 테스트(Seven Testing Principles)의 원칙 중 하나로, 테스트는 내가 관측하는 영역에서의 동작만을 보장한다는 것인데 가령 위의 테스트가 모두 통과한다 할지라도 내가 생각하지 못한 경우의 수에서 예측하지 못하는 동작을 수행할 수 있다.
위 코드를 작성할 당시에 블록체인 네트워크가 멈춘다면? 이라는 상황에서 핸들링할 방법에 대해서는 고민하지 않았는데 아래와 같이 블록체인 네트워크를 강제로 내리면 모든 테스트 코드가 실패하는 것을 볼 수 있다. 따라서 최대한 여러 가지 경우의 수를 고려해서 메인 로직 및 테스트 코드를 작성하는 역량을 키워야 한다고 생각했다.
일부 로직(특히 private method) 만 테스트하고 싶다
하나의 로직에서 데이터에 대한 연산을 검증하는데 불필요한 데이터를 호출해야하는 경우가 있다. 나는 이 경우 검증하고자 하는 로직을 private method 로 분리하고, ReflectionTestUtils 의 invokeMethod 를 통해 결과를 검증한다.
아래 코드에서 findUserArtistNotSelectedCurrently, findUserArtistNotSelectedPreviously 메소드가 수행하는 연산에 대한 검증이 필요한 상황이다.
이 때 prevUserArtistList, selectArtistIds 데이터는 직접 생성해서 넣으면 되고, User 데이터를 포함해 deleteAll, saveAll 의 동작은 큰 관심사가 아니다.
사실 원래 findUserArtistNotSelectedCurrently, findUserArtistNotSelectedPreviously 는 chooseArtist 로직 내에 인라인되어 있었지만, 테스트를 위해 분리했다. 이와 같이 테스트를 위해 기존의 로직을 리팩토링 하는 것은 나쁘지 않다고 생각한다. (테스트를 수행한다는 것이 이미 의미있는 단위로 보는 것이기 때문에)
@Transactional
public List<UserArtist> chooseArtist(List<ChooseArtistReq> selectArtistIds, String nickname) {
User user = userRepository.findByNickname(nickname)
.orElseThrow(() -> new KnuException(ResultCode.BAD_REQUEST, "해당 닉네임의 유저를 찾을 수 없습니다"));
List<UserArtist> prevUserArtistList = userArtistRepository.findAllByUserId(user.getId());
List<UserArtist> toDelete = findUserArtistNotSelectedCurrently(prevUserArtistList, selectArtistIds);
List<UserArtist> toSave = findUserArtistNotSelectedPreviously(prevUserArtistList, selectArtistIds, user);
userArtistRepository.deleteAll(toDelete);
return userArtistRepository.saveAll(toSave);
}
검증하고자 하는 연산 로직은
유저가 1, 2, 3 번 데이터를 기존에 선택했고, 현재 2, 3, 4 번 데이터를 다시 선택했을 때 삭제할 데이터는 1번이고, 추가할 데이터는 4번인 것을 검증하고자 할 때
로직을 수행한 이후 결과는 데이터의 크기가 각각 1이어야 하고, 데이터의 Id 가 예측값과 동일하면 되는 것으로 검증했다.
/**
* 기존 관심 아티스트 1,2,3 번
* 수정하려는 관심 아티스트 2,3,4 번인 경우
*/
@DisplayName("유저 아티스트 테스트")
@ExtendWith(MockitoExtension.class)
public class UserArtistTest{
@InjectMocks
UserArtistService userArtistService;
@Mock
ArtistServiceImpl artistService;
@Test
@DisplayName("관심 아티스트 목록에서 삭제할 아티스트 찾기")
public void 관심_아티스트_목록에서_삭제할_아티스트_찾기() {
// given
User user = User.builder().id(1L).build();
List<UserArtist> prev = getPreUserArtists(user);
List<ChooseArtistReq> req = getChooseArtistReq();
// when
List<UserArtist> res = ReflectionTestUtils.invokeMethod(
userArtistService, "findUserArtistNotSelectedCurrently", prev, req);
// then
assertTrue(res != null && res.size() == 1);
assertEquals(1L, res.get(0).getArtist().getId());
}
@Test
@DisplayName("관심 아티스트 목록에서 추가할 아티스트 찾기")
public void 관심_아티스트_목록에서_추가할_아티스트_찾기() {
// given
User user = User.builder().id(1L).build();
List<UserArtist> prev = getPreUserArtists(user);
List<ChooseArtistReq> req = getChooseArtistReq();
when(artistService.findBy(4L)).thenReturn(Artist.builder().id(4L).build());
// when
List<UserArtist> res = ReflectionTestUtils.invokeMethod(
userArtistService, "findUserArtistNotSelectedPreviously", prev, req, user);
// then
assertTrue(res != null && res.size() == 1);
assertEquals(4L, res.get(0).getArtist().getId());
}
}
테스트 코드는 리팩토링에 도움이 된다
위와 같이 하나의 연산 로직에 대해서 검증하는 테스트 코드를 작성하면, 내부 구현이 변경되었을 때 로직이 정상적으로 수행되는지 기존에 작성한 테스트 코드를 돌려보면서 확인할 수 있다는 장점이 있다. 이를 통해 리팩토링 핵심인 "변경 전 후 동작이 동일해야한다" 의 조건을 자연스럽게 만족시킬 수 있다.
@InjectMocks 와 @Mock
단위 테스트를 수행하기 위해서는 테스팅 하기 위한 로직을 제외한 로직을 테스팅 영역에서 완벽하게 격리를 시켜야한다. 이 때 사용할 수 있는 것이 Mockito 라이브러리의 InjectMocks 와 Mock 어노테이션이다.
@InjectMocks 의 경우 테스트하고자 하는 실제 객체를 생성하고 명시된 의존 객체가 있으면 자동으로 주입해준다. InjectMocks 의 대상 객체의 메소드를 호출하면 실제 구현된 메소드가 호출된다.
@Mock 의 경우 테스트하고자 하는 로직은 아니지만 의존하고 있는 객체를 가짜로 주입하는 방법을 의미한다. 처음에 이 가짜라는 것이 무슨 뜻인지 잘 이해가 안됐는데, 사용자가 원하는 방식대로 결과를 돌려줄 수 있도록 실제 동작 여부와 상관없이 "사용자가 객체의 행동을 제어할 수 있는 것을 의미한다.
InjectMocks 를 통해 테스트하고자 하는 클래스의 메소드를 호출하고, 해당 메소드 내에서 관심 영역 밖의 동작은 Mock 을 통해 제어한다.
아래의 코드는 artistService(Mock 객체) 의 findBy 메소드의 파라미터로 4를 넣어 호출하면, ID 값이 4인 Artist 객체를 반환한다는 로직을 나타낸다.
when(artistService.findBy(4L)).thenReturn(Artist.builder().id(4L).build());
DB 호출 없이 쿼리 결과를 받아 테스트하고 싶다
이 방법은 로직에 사용되는 데이터를 한눈에 확인할 수 있기 때문에 굉장히 유용하다고 생각한다. 또한 DB 와 직접적인 통신이 없기 때문에 테스트를 구성하기 쉽고 속도가 매우 빠르다.
단, DB 쿼리에 대한 결괏값을 자바 코드로 손쉽게 표현할 수 있을 때 사용하고 그렇지 않다면 데이터를 준비하고, 직접 쿼리를 전송해 결괏값을 받아 테스트를 수행하는 것도 좋아보인다.
아래 코드는 아티스트를 조회하기 위한 필터링 로직이 예측한 순서로 호출되는지 검증한다. 이를 위해 Repository 에서 호출한 데이터가 자바 코드로 구현된 코드에 따라 필터링 되어 조회된다고 가정하고, 메소드의 결괏값을 검증하고 메소드가 정상적으로 호출(아래 verify 함수) 된지 확인한다.
@ExtendWith(MockitoExtension.class)
@DisplayName("아티스트 조회 성공 테스트")
public class ArtistServiceTest {
@InjectMocks
ArtistServiceImpl artistService;
@Mock
ArtistRepository artistRepository;
@Test
@DisplayName("그룹이름으로 아티스트찾기")
public void 그룹이름으로_아티스트찾기() {
// given
List<Artist> btsArtists = getBTSArtists();
doReturn(btsArtists).when(artistRepository).findAllByGroup(Group.BTS);
// when
List<Artist> findByGroupName = artistService.findAllBy(Group.BTS, null);
// then
assertEquals(getBTSArtists().size(), findByGroupName.size());
verify(artistRepository, times(1)).findAllByGroup(Group.BTS);
}
@Test
@DisplayName("아티스트이름으로 아티스트찾기")
public void 아티스트이름으로_아티스트찾기() {
// given
List<Artist> btsArtists = getBTSArtists();
doReturn(btsArtists.stream()
.filter(artist -> artist.getName().equals("RM")).collect(Collectors.toList()))
.when(artistRepository).findAllByNameContaining("RM");
// when
List<Artist> findByArtistName = artistService.findAllBy(null, "RM");
// then
assertEquals(1, findByArtistName.size());
assertEquals("RM", findByArtistName.get(0).getName());
verify(artistRepository, times(1)).findAllByNameContaining("RM");
}
@Test
@DisplayName("그룹이름과 아티스트이름으로 아티스트찾기")
public void 그룹이름과_아티스트이름으로_아티스트찾기() {
// given
List<Artist> btsArtists = getBTSArtists();
doReturn(btsArtists.stream()
.filter(artist -> artist.getName().equals("RM") && artist.getGroup().equals(Group.BTS)).collect(Collectors.toList()))
.when(artistRepository).findAllByGroupAndNameContaining(Group.BTS, "RM");
// when
List<Artist> findByGroupNameAndArtistName = artistService.findAllBy(Group.BTS, "RM");
// then
assertEquals(1, findByGroupNameAndArtistName.size());
assertEquals("RM", findByGroupNameAndArtistName.get(0).getName());
verify(artistRepository, times(1)).findAllByGroupAndNameContaining(Group.BTS, "RM");
}
List<Artist> getBTSArtists() {
return List.of(
Artist.builder().group(Group.BTS).name("RM").build(),
Artist.builder().group(Group.BTS).name("Jin").build(),
Artist.builder().group(Group.BTS).name("SUGA").build(),
Artist.builder().group(Group.BTS).name("j-hope").build(),
Artist.builder().group(Group.BTS).name("Jimin").build(),
Artist.builder().group(Group.BTS).name("V").build(),
Artist.builder().group(Group.BTS).name("Jung Kook").build()
);
}
}
단일 객체(도메인 객체) 에 대한 테스트를 하고 싶다
캡슐화된 데이터의 메소드를 호출해 객체의 상태가 예측한 대로 변경되는지 테스트하고 싶을 때 사용한다.
역시 리팩토링할 때 매우 유용하게 사용될 수 있는 테스트코드라고 생각한다.
@ExtendWith(MockitoExtension.class)
@DisplayName("토큰 상태 변경 테스트")
public class PaymentServiceTest{
@Test
@DisplayName("토큰 발행 후 상태값 초기화")
public void 토큰_생성_후_상태값_초기화(){
//given
Token before = Token.builder().nftId(1L).owner("before").build();
//when
//then
assertEquals("before", before.getOwner());
assertEquals(PaymentState.ON_SALE, before.getPaymentState());
}
@Test
@DisplayName("토큰 구매 후 판매상태 및 보유자 갱신")
public void 토큰_구매_후_판매상태_및_보유자갱신(){
//given
Token before = Token.builder().nftId(1L).owner("before").build();
//when
before.soldTo("after");
//then
assertEquals("after", before.getOwner());
assertEquals(PaymentState.SOLD_OUT, before.getPaymentState());
}
}
MVC 테스트를 하고 싶다
클라이언트의 요청이 WAS 로 도착해 Filter 및 Interceptor 를 거쳐 Controller 에 도착하는 과정(MVC) 및 응답값도 테스팅이 가능하다.
MockMvc 를 이용하면 HTTP 요청을 간접적으로 전송할 수 있는데, 아래는 하나의 Get 요청을 보내는 예시다.
이와 같은 테스트를 통해 요청이 컨트롤러로 잘 전달되는지, 응답값이 잘 변환되어 클라이언트로 전송되는지 테스팅이 가능하다.
@ExtendWith(MockitoExtension.class)
public class ArtistControllerTest {
@InjectMocks
ArtistController artistController;
@Mock
ArtistServiceImpl artistService;
Gson gson;
MockMvc mockMvc;
@BeforeEach
public void init() {
gson = new Gson();
mockMvc = MockMvcBuilders.standaloneSetup(artistController).build();
}
@Test
@DisplayName("아티스트 조회 API")
public void 아티스트_조회_API() throws Exception {
// given
String group = "BTS";
String name = "RM";
Artist artist = Artist.builder().id(1L).name("RM").build();
when(artistService.findAllBy(Group.BTS, name)).thenReturn(List.of(artist));
// when
ResultActions resAction = mockMvc.perform(
MockMvcRequestBuilders.get("/artist?group=" + group + "&name=" + name));
// then
MvcResult mvcRes = resAction
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
assertEquals(200, mvcRes.getResponse().getStatus());
Type type = new TypeToken<ApiResponse<List<Artist>>>(){}.getType();
ApiResponse<List<Artist>> res = gson.fromJson(mvcRes.getResponse().getContentAsString(), type);
List<Artist> data = res.getData();
assertTrue(data != null && data.size() == 1);
assertEquals("RM", data.get(0).getName());
}
}
마무리
본 포스팅에서 제시한 방법 이외에도 더 다양한 테스팅 방법이 존재한다. 나 역시 학습 과정에서 반복적으로 작성했던 패턴의 테스트 코드를 나열해봤지만 여전히 그 가짓수가 부족하고, 더 복잡한 상황에서 테스트 코드를 작성해볼 필요가 있다고 느꼈다.
처음 테스트 코드를 작성하기 시작했을 때, 이렇게 하는게 맞나? 이게 과연 효용이 있을까? 하는 생각이 가장 먼저 들었던거 같다. 하지만 실제로 개발 간 리팩토링 할 때나, 배포하기 전 테스트가 모두 통과하는 걸 보며 자신감이 생겼던 경험을 되돌아보면 결코 도움이 되지 않는다고 할 수 없다. 그리고 이제 다시 Swagger 나 Postman 으로 테스트를 한다고 생각하면... 정말 말이 안된다. (물론 이러한 API 테스트가 불필요하다는 이야기는 아니다)
테스트 코드를 작성하는 것은 비용이다. 하지만 결코 낭비되는 비용이 아니라고 생각한다. 또한, 오히려 비용이라면 내가 효과적으로 테스트 코드를 작성하는 능력을 키워 들이는 비용을 줄여야겠다는 생각을 한다.
혹시라도 내용에 오류가 있거나 더 좋은 방법이 있다면 알려주시면 감사하겠습니다!
'프로젝트' 카테고리의 다른 글
알림 전송 모듈 개발기 (5) | 2023.03.10 |
---|---|
Redis 캐싱 전략 개선 및 스케줄링 (3) | 2023.01.31 |
[프로젝트 리뷰] 주어진 시간은 단 5일 (feat.핀플레인) (1) | 2022.10.08 |
[프로젝트 리뷰] 공개SW 개발자 대회 참여를 하면서 (feat. 이웃사이) (0) | 2022.09.10 |
[Spring/WebSocket] WebSocket 도입과 STOMP subscribe, send 인가 구현 (0) | 2022.08.31 |