당연하겠지만, 모든 코드는 작성 후 의도에 맞게 정상 작동하는지 확인하는 과정을 거쳐야만 한다.
정말 간단한 코드라도 예상치 못 한 부분에서 문제가 발생할 수 있기에 이를 확인해주는 것이 정말 중요한데
Swagger 나, Postman 등을 활용하여 직접 값을 입력해 확인해보는 과정을 거쳐도 되지만 가능하면 테스트 코드를 통해 확인하는 것이 효율적이다.
테스트 코드를 작성하기 전, 어떠한 환경에서 어떠한 목적을 갖고 이를 작성할지 정해야만 한다.
기본적으로 코드의 전체적인 테스트를 한다는 목적은 변하지 않지만, 예를 들어
빠른 시간 내에 최소한의 테스트만을 위해 , 시간이 소요되더라도 완벽한 테스트를 위해 등
상세한 목적에 맞춰 작성법이 달라지기에 이를 확실히 하고 작성을 시작하면 되겠다.
이번 통합 테스트는,
하나의 서버를 사용하며, 실제 사용자가 이용하는 환경과 유사한 환경에서의 정확한 테스트를 목적으로 작성해보도록 하겠다.
통합 테스트를 위해서는 이를 도와줄 도구를 선택해야만 한다.
가장 대표적인 것은 아래 세 가지인데,
TestRestTemplate
- 실제 HTTP 서버로 테스트할 수 있다는 장점
- 테스트 속도가 비교적 느리다는 단점
MockMVC
- HTTP 서버를 시작하지 않고 테스트를 할 수 있어 빠르다는 장점
- 실제 환경과는 다르다는 단점
WebTestClient
- 비동기 처리 지원
- 설정 복잡 및 호환되지 않는 부분들이 많다는 단점
MockMVC는 효율적으로 빠른 시간 내에 최소한의 테스트를 위해 선택하는 경우가 많은 것같다.
다만, 이번 목적과는 부합하지 않기에 이는 배제하기로 하고
가능하다면 WebTestClient를 사용하면 좋겠지만, TestRestTemplate과 비교하여 고려해야할 부분이 많은 것에 비해, 현재 목적에서 WebTestClient를 사용하여 얻는 이점이 크지 않다고 생각하여 이번에는 TestRestTemplate를 사용하여 코드를 작성해보도록 하겠다.
아래는 이번 테스트 코드를 작성하기 위해 활용할
CreateMemberDto 로 name을 전달 받아 Member를 생성하는 코드와
memberId를 전달 받고 해당 Member가 존재하면 true를 반환하는 샘플 코드이다.
@RestController
@RequestMapping("/member")
@RequiredArgsConstructor
@Slf4j
public class MemberController {
private final MemberService memberService;
@PostMapping("/createMember")
public CreateMemberDto.Response createMember(@RequestBody CreateMemberDto.Request req) {
Member member = memberService.createMember(req);
CreateMemberDto.Response response = CreateMemberDto.Response.builder()
.name(member.getName())
.build();
return response;
}
@GetMapping("/getMember/{memberId}")
public boolean getMember(@PathVariable Long memberId) {
Member member = memberService.findMemberById(memberId);
return member != null;
}
}
public class CreateMemberDto {
@Builder
@Getter
public static class Request{
private String name;
}
@Builder
@Getter
public static class Response{
private String name;
}
}
전체 코드는 아래와 같다.
코드 설명은 최하단에 작성해두었으니 필요할 경우 읽어보자.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class MemberTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private MemberRepository memberRepository;
@Test
void createMember() {
// given
String name = "name";
CreateMemberDto.Request request = CreateMemberDto.Request.builder()
.name(name)
.build();
String url = "<http://localhost>:" + port + "/member/createMember";
// when
ResponseEntity<CreateMemberDto.Response> response = restTemplate.exchange(
url,
HttpMethod.POST,
new HttpEntity<>(request),
CreateMemberDto.Response.class
);
// then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().getName()).isEqualTo(name);
}
@Test
void getMember() {
// given
Member member = Member.builder()
.name("name")
.build();
memberRepository.save(member);
Long memberId = member.getId();
String url = "<http://localhost>:" + port + "/member/getMember/" + memberId;
// when
ResponseEntity<Boolean> response = restTemplate.exchange(
url,
HttpMethod.GET,
null,
Boolean.class
);
// then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().booleanValue()).isEqualTo(true);
}
통합 테스트는 단위 테스트와 다르게 실제 환경과 유사한 환경에서 진행하여야 하기에 @SpringBootTest 를 이용하여 실제로 서버를 띄워야만 한다.
이를 위해 추가되는 것이 webEnvironment = SpringBootTest.WebEnvironment.*RANDOM_PORT 이다.*
보면 알다시피, 포트 번호는 랜덤으로 지정해두었는데 이는 테스트 환경에서 포트 충돌로 인한 문제가 발생하지 않기 위함도 있기에 가능하면 랜덤 포트로 지정하여 사용하는 것이 좋다.
그리고 이렇게 지정된 포트 번호를 @LocalServerPort 를 활용하여 가져올 수 있다.
다음으로 사용할 의존성들을 @Autowired 을 활용해 주입한 후 @Test 어노테이션을 활용해 본격적으로 테스트 코드를 작성할 수 있다.
( 의존성은 이외의 방법으로도 주입할 수 있지만, 테스트 코드에서는 편의를 위해 @Autowired를 많이 활용하는 것 같다. )
테스트 코드를 작성할때는 확실한 구분을 위해 Given-When-Then 패턴을 활용하는 것을 추천한다.
Given : 테스트를 위해 필요한 값들을 준비(생성) 하는 단계
When : 실제 테스트를 진행하는 단계
Then : 테스트를 검증하는 단계
이를 활용해
given 단계에서는 request 값 준비를,
when 단계에서는 restTemplate 을 이용해 실제 테스트를, then 단계에서는 assertThat 을 이용해 테스트를 검증하였다.
이번 예시에서는 성공 케이스만을 작성하였지만, 실제로는 실패 케이스나 예외 처리가 필요한 부분 등의 테스트 코드를 함께 작성하는 것이 좋으며, 이렇게 테스트 코드를 한 번 작성해두면 이후 내부 로직이 변경이 되더라도 이전과 동일한 값이 반환이 된다는 보증이 되기에 유지보수나 리펙토링 등에서도 유용하게 활용할 수 있을 것이다.
'Springboot' 카테고리의 다른 글
Content-Type 'application/octet-stream' is not supported 원인 및 해결 방법 (0) | 2025.02.05 |
---|---|
Exception Handler ( 커스텀 익셉션 ) 사용 이유 및 작성 방법 (0) | 2024.11.25 |
fixtureMonkey, JakartaValidationPlugin 제약 조건 설정 (0) | 2024.10.06 |
fixtureMonkey 객체 값 자동 생성 / springboot 테스트 코드 (0) | 2024.10.04 |
스프링부트 Repository TestCode ( 테스트 코드 / mybatis ) (0) | 2024.09.25 |