본문 바로가기
실습/리눅스 서버 + 스프링 부트

JUnit Test로 컨트롤러 API 테스트 (RestTemplate, MockMVC)

by 이민우 2024. 11. 6.
728x90
반응형

테스트를 할 때는 서비스단을 호출하는 경우도 있지만, 종종 컨트롤러에 있는 API를 호출하는 경우도 있다.

 

이는 너무 당연한 거라서 굳이 따로 포스팅을 하지 않았었다.

 

그런데 오늘도 PM 부장님의 요청으로 한 가지 테스트를 하기로 했다.

 

테스트는 별 건 아니고, 특정 API를 여러 번 찔러서 평균적으로 속도가 얼마나 걸리는지 확인해보는 일이었다.

 

굳이 JMeter까지 사용할 건 아닌 것 같아서, 간단하게 Test 코드를 작성하고 돌리려고 했다.

 

우선 테스트 대상으로 아래와 같은 간단한 API가 있다고 해보자.

private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");

@GetMapping("/date")
public String date() {
	return sdf.format(new Date());
}

 

위 코드는 당연히 인터넷 브라우저로 들어가면 아주 잘 출력된다.

 

오늘의 목표는 클라이언트 입장에서 여러 번 API를 찔렀을 때를 측정하기 위함이니,

테스트를 위해 대충 아래와 같이 테스트용 코드를 작성했다.

package com.example.demo;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.web.client.RestTemplate;

@SpringBootTest
class DemoApplicationTests {

	RestTemplate restTemplate = new RestTemplate();
	
	@Test
	void contextLoads() {
		
		String targetApi = "/date";

		// 동시에 10개의 요청을 보내기 위해 10개의 스레드풀 작성
		int targetCall = 10;
		
		ExecutorService executorService = Executors.newFixedThreadPool(targetCall);
		List<Callable<Void>> tasks = new ArrayList<>();
		
		
		// 10개의 요청 전송
		for(int i=0; i<targetCall; i++) {
			tasks.add(() -> {
				// targetApi로부터 String으로 된 Response 받기
				restTemplate.getForEntity(targetApi, String.class);
				return null;
			});
		}
		

		try {
			// 모든 작업을 병렬로 실행하고 결과 대기
	        List<Future<Void>> futures = executorService.invokeAll(tasks);
	
	        for (Future<Void> future : futures) {
	        	// 예외 발생 시 처리
	            future.get();
	        }
		}
		catch(Exception e) {
			e.printStackTrace();
		}
		finally {
			executorService.shutdown();
		}
		
	}
	
}

 

위 코드는 동작할까?

당연한 말이지만 아래와 같은 에러가 뜨면서 동작을 하지 않는다.

java.lang.IllegalArgumentException: URI is not absolute

 

위 에러는 RestTemplate은 URI를 절대URI로 입력해야 하기 때문에 발생하는 에러이다.

즉, /date 처럼 일반적으로 입력을 하는게 아니라, http://localhost:8080/date 처럼 전체 URI를 입력해야 한다는 것이다.

 

그러면 전체 URI를 입력하고 돌리면 정상적으로 돌아갈까?

물론 그럴 리가 없다.

org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8080/date": Connection refused: connect

 

절대 URI를 입력했음에도 불구하고 돌아가지 않는 건 왜일까? 그야 당연히 Spring Boot 서버가 실행되지 않아서이다.

 

Spring Boot을 실행하면 내장 서버도 자동으로 켜진다. 그런데 테스트는 아니다. @SpringBootTest 만으로는 서버가 자동으로 실행되지 않는다.

 

즉 서버가 실행되지 않아 8080 포트에 아무것도 떠있지 않음에도 불구하고 8080 포트에 연결을 하려고 시도했다. 그러니 에러가 나는 것은 당연한 일이다.

 

모르고 있던 사실은 아니다. 오히려 알고 있던 사실이었다. 그래서 별로 시덥지 않게 "잠깐 깜박했고, 테스트 코드 다시 짜야겠다" 라고 생각을 했다.

 

그런데 코드를 짜려고 하니, 손가락이 움직이지 않았다.

... API용 테스트 코드는 어떻게 작성하는 거더라?

 

나는 항상 아는 걸 까먹으면 블로그를 뒤져본다. 하지만 너무 당연하게 생각하고 넘어갔던 부분이라서 API 테스트 코드 작성 방법은 따로 포스팅을 하지 않았었다.

물론 몇몇 포스트 내에 테스트하는 코드에 포함되어 있기는 하지만, 어떻게 사용할지를 까먹어서 어떤 키워드로 검색을 해야하는지도 까먹었다.

 

결국 테스트 코드 작성 방법을 찾지 못해 구글링을 통해 다시 한 번 테스트 코드 작성 방법을 찾아 테스트 코드를 작성했고, 이제 잊지 않기 위해 혹은 잊더라도 바로바로 찾을 수 있게 하기 위해 테스트 진행 방법을 포스팅해볼까 한다.

 

우선 위에서 이미 언급했지만, 위 테스트 코드가 돌아가지 않는 이유는 간단하다.

@SpringBootTest는 자동으로 내장 서버를 켜주지 않는다.

 

이 말에서 저 "자동"이라는 단어가 핵심이다.

자동으로 켜주지 않는다는 말은, 수동으로는 내장 서버를 켤 수 있다라는 말이기도 하다.

 

그러면 수동으로 내장 서버를 켜려면 어떻게 해야할까?

간단하다. Test 클래스 위 @SpringBootTest 어노테이션에 아래와 같이 간단하게 설정만 기입하면 된다.

webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT

 

이제 저 설정을 넣었으니 다시 실행시켜보자.

 

아까와 다르게 어떠한 포트에 내장 서버가 실행되었음을 확인했다.

 

그러면 저 랜덤한 포트를 어떻게 알아내고 절대 URI를 설정해야 할까?

 

방법은 @LocalServerPort 어노테이션을 사용하는 것이다.

 

@LocalServerPort 어노테이션을 사용한 변수는 위에서 RANDOM_PORT로 생성된 포트를 자동으로 잡아준다.

 

전체 코드는 아래와 같다.

package com.example.demo;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.web.client.RestTemplate;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DemoApplicationTests {

	RestTemplate restTemplate = new RestTemplate();
	

    @LocalServerPort
    private int port;
	
	@Test
	void contextLoads() {
		
		String targetApi = "http://localhost:"+port+"/date";

		// 동시에 10개의 요청을 보내기 위해 10개의 스레드풀 작성
		int targetCall = 10;
		
		ExecutorService executorService = Executors.newFixedThreadPool(targetCall);
		List<Callable<Void>> tasks = new ArrayList<>();
		
		
		// 10개의 요청 전송
		for(int i=0; i<targetCall; i++) {
			tasks.add(() -> {
				// targetApi로부터 String으로 된 Response 받기
				restTemplate.getForEntity(targetApi, String.class);
				return null;
			});
		}
		

		try {
			// 모든 작업을 병렬로 실행하고 결과 대기
	        List<Future<Void>> futures = executorService.invokeAll(tasks);
	
	        for (Future<Void> future : futures) {
	        	// 예외 발생 시 처리
	            future.get();
	        }
		}
		catch(Exception e) {
			e.printStackTrace();
		}
		finally {
			executorService.shutdown();
		}
		
	}
	
}

 

이제 한 번 테스트를 해보자.

 

 

테스트가 정상적으로 완료되었음을 확인할 수 있을 것이다.

 

그런데 만약 랜덤한 포트를 사용하고 싶지 않다면 어떻게 할까?

그러면 포트를 지정해주면 된다.

 

포트를 지정하는 방식은 간단하다.

webEnvironment를 SpringBootTest.WebEnvironment.DEFINED_PORT로 두고, properties에 application.properties에 들어가는 포트 설정을 두면 된다.

8080 포트 지정

 

그런데 만약 이 마저도 귀찮다면?

 

그냥 PORT를 지정하거나 하는 등의 작업이 번거롭다면 어떻게 할까?

 

사실 아주 간단한 방법이 있다. 그것은 바로 MockServer를 이용하는 것이다.

MockMVC

Spring에서 제공하는 테스트 도구로, 실제 서버를 실행하지 않고도 컨트롤러 계층의 동작을 테스트할 수 있게 도와준다.

MockMvc 사용 시 달라지는 부분. 위 @AutoConfigureMockMvc 미설정 시 @Autowired가 동작하지 않는다.

 

전체 코드는 아래와 같다.

package com.example.demo;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

@AutoConfigureMockMvc
@SpringBootTest
class DemoApplicationTests {

    @Autowired
    private MockMvc mockMvc;
	
	@Test
	void contextLoads() {

		// 동시에 10개의 요청을 보내기 위해 10개의 스레드풀 작성
		int targetCall = 10;
		
		ExecutorService executorService = Executors.newFixedThreadPool(targetCall);
		List<Callable<Void>> tasks = new ArrayList<>();
		
		
		// 10개의 요청 전송
		for(int i=0; i<targetCall; i++) {
			tasks.add(() -> {
				// targetApi로부터 String으로 된 Response 받기
                mockMvc.perform(get("/date"))
                	.andExpect(status().isOk());
				return null;
			});
		}
		

		try {
			// 모든 작업을 병렬로 실행하고 결과 대기
	        List<Future<Void>> futures = executorService.invokeAll(tasks);
	
	        for (Future<Void> future : futures) {
	        	// 예외 발생 시 처리
	            future.get();
	        }
		}
		catch(Exception e) {
			e.printStackTrace();
		}
		finally {
			executorService.shutdown();
		}
		
	}
	
}
728x90
반응형