티스토리 뷰

반응형
https://spring.io/guides/gs/testing-web/ 문서를 기반으로 작성되었습니다.
원본을 보고 싶다면 위 링크를 참고해주세요.

 

1. Create a Simple Application (간단한 애플리케이션 만들기)

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HomeController {

    /**
     * 기본적으로 @RequestMapping은 모든 HTTP 작업(GET, POST, PUT, DELETE..)을 매핑합니다.
     * @GetMapping 또는 @RequestMapping(method=GET)을 사용하여 매핑 범위를 좁힐 수 있습니다.
     * @return
     */
    @RequestMapping("/")
    @ResponseBody
    public String greeting() {
        return "Hello, World";
    }
}

2. Run the Application (애플리케이션 실행)

Spring Initializr를 이용해서 프로젝트를 생성하게 되면 아래와 같은 애플리케이션 클래스가 생성되어있는 것을 확인할 수 있습니다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TestingApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestingApplication.class, args);
    }
}
  • @SpringBootApplication을 사용하면 스프링 부트의 자동 설정(auto-configuration), 스프링 Bean 읽기와 생성을 모두 자동으로 설정해줍니다. @SpringBootApplication은 아래의 어노테이션들을 포함하고 있습니다.
    • @EnableAutoConfiguration: Spring Boot의 자동 구성(auto-configuration) 메커니즘을 활성화합니다. 일반적으로 클래스 경로와 정의된 빈을 기반으로 적용되며, 예를 들어 spring-boot-starter-web가 추가되어 있다면 auto-confituraion에서 이름 감지하고 웹 애플리케이션을 개발함을 가정한다. 그리고 톰캣과 Spring Mvc 등 이와 관련된 스프링 설정을 진행한다.
    • @EnableWebMvc: Spring Boot는 클래스 경로에서 spring-webmvc가 포함되어있을 경우 자동으로 추가(@EnableAutoConfiguration)되기 때문에 따로 선언을 안 해도 됩니다. 애플리케이션에 웹 애플리케이션으로 플래그를 지정하고 DispatcherServlet 설정과 같은 주요 동작을 활성화합니다.
    • @SpringBootConfiguration: 컨텍스트의 빈을 등록하거나 추가 구성 클래스를 가져올 수 있습니다. 통합 테스트에서 구성 감지(configuration detection)를 지원하는 Spring의 표준 @Configuration 대체 어노테이션 입니다.
    • @ComponentScan: Spring이 TestingApplication이 생성되어있는 패키지 기준으로 다른 구성 요소, 구성 및 서비스를 찾도록 스캔합니다. (TestingApplication의 상위에 있는 폴더는 스캔하지 않습니다.)

main() 메서드는 Spring Boot의 SpringApplication.run() 메서드를 사용하여 애플리케이션을 시작합니다. 에러 없이 로그가 정상적으로 올라간다면 스프링 애플리케이션 설정은 완료되었습니다.

3. Test the Application (애플리케이션 테스트)

이제 애플리케이션이 실행 중이므로 테스트할 수 있습니다. http://localhost:8080으로 접속하면 Hello, World가 표시되는 것을 확인할 수 있습니다. 그러나 코드를 변경할 때마다 테스트를 하려면 매번 애플리케이션을 재실행하고, 해당 url에 접속해서 확인해야 합니다. 이러한 번거로움에서 벗어나기 위해 우리는 테스트를 자동화해주려 합니다.

Spring Boot에서 테스트를 하려면 빌드 파일(build.gradle 또는 pom.xml)에 필요한 종속성을 추가해줘야 합니다.

bulid.gradle

implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

pom.xml
<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
</dependency>

가장 먼저 할 수 있는 일은 애플리케이션 컨텍스트를 시작할 수 없는 경우 실패하는 간단한 온전성 검사(sanity check; 동작이나 계산 결과가 정상인지 여부를 빠르게 평가하기 위한 기본 테스트) 테스트를 작성하는 것입니다.

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

@SpringBootTest
public class TestingApplicationTests {

    @Test
    public void contextLoads() {
    }
}

@SpringBootTest 어노테이션은 Spring Boot가 기본 구성 클래스(ex. 앞에서 만들었던 @SpringBootApplication 어노테이션이 붙어있던 TestingApplication)를 찾고 이를 사용하여 Spring 애플리케이션 컨텍스트를 시작하도록 지시합니다. IDE 또는 명령줄에서 이 테스트를 실행할 수 있으며(./mvnw test 또는 ./gradlew test를 실행) 통과해야 합니다.

컨텍스트가 컨트롤러(HomeController)를 제대로 생성하고 있는지를 확인하기 위해 다음 예제(SmokeTest.java에서)와 같이 Assertions(assertThat(controller).isNotNull())을 추가할 수 있습니다.

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class SmokeTest {

	@Autowired
	private HomeController controller;

	@Test
	public void contextLoads() throws Exception {
		assertThat(controller).isNotNull();
	}
}

Spring은 @Autowired 어노테이션를 스캔하여 테스트 메서드가 실행되기 전에 의존성을 주입합니다. 그 후 AssertJ(assertThat()등 여러 테스트 메서드를 제공합니다.)를 사용하여 controller가 null 이 아닌지 테스트를 진행합니다.

 

 💡 Spring Test에서는 애플리케이션 컨텍스트가 테스트 간에 캐시되는 좋은 기능을 제공합니다. 이 기능 덕분에 테스트 케이스에 여러 메소드가 있거나 동일한 구성의 여러 테스트 케이스가 있는 경우 애플리케이션을 한 번만 시작하는 비용만 발생됩니다. @DirtiesContext 어노테이션을 사용하여 캐시를 제어할 수 있습니다.

 

온전성 검사를 하는 것은 좋지만 애플리케이션의 동작을 확인하는 다른 몇 가지 테스트도 작성해야 합니다. 그렇게 하려면 수신을 받을 수 있는 상태로 애플리케이션을 시작하고(실제 배포 환경처럼) HTTP 요청을 보내고 응답을 확인합니다. 다음 예제(HttpRequestTest.java)에서 HTTP 응답을 확인해보도록 하겠습니다.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HttpRequestTest {

	@LocalServerPort
	private int port;

	@Autowired
	private TestRestTemplate restTemplate;

	@Test
	public void greetingShouldReturnDefaultMessage() throws Exception {
		assertThat(this.restTemplate.getForObject("<http://localhost>:" + port + "/",
				String.class)).contains("Hello, World");
	}
}

webEnvironment=RANDOM_PORT를 사용하여 임의의 포트로 서버를 시작하고(테스트 환경에서 포트 충돌을 방지하는 데 유용) @LocalServerPort로 포트 정보를 주입합니다. Spring Boot는 TestRestTemplate(REST 방식으로 개발한 API 테스트를 하는데 사용)을 제공하므로 @Autowired를 붙여 주입 받을 수 있도록 해줍니다.

INFO 18940 --- [    Test worker] com.study.testing.HttpRequestTest        : Starting HttpRequestTest using Java 11.0.15 on My-PC with PID 18940 
INFO 18940 --- [    Test worker] com.study.testing.HttpRequestTest        : No active profile set, falling back to 1 default profile: "default"
INFO 18940 --- [    Test worker] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 0 (http)
INFO 18940 --- [    Test worker] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
INFO 18940 --- [    Test worker] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.65]
INFO 18940 --- [    Test worker] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
INFO 18940 --- [    Test worker] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 978 ms
INFO 18940 --- [    Test worker] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 14339 (http) with context path ''
INFO 18940 --- [    Test worker] com.study.testing.HttpRequestTest        : Started HttpRequestTest in 1.94 seconds (JVM running for 3.738)
INFO 18940 --- [o-auto-1-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
INFO 18940 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
INFO 18940 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms

 

또 다른 테스트 방식으로는 서버를 시작하지 않고 그 아래 계층만 테스트하는 것입니다. 여기서 Spring은 들어오는 HTTP 요청을 처리하고 이를 컨트롤러에 전달합니다. 그렇게 하면 거의 전체 스택이 사용되며 서버 시작 비용 없이 실제 HTTP 요청을 처리하는 것과 똑같은 방식으로 코드가 호출됩니다. 그렇게 하려면 Spring의 MockMvc를 사용하고 테스트 케이스에서 @AutoConfigureMockMvc 주석을 사용하여 주입 되도록 추가해 주어야 합니다. 다음 예제(TestingWebApplicationTest.java에서)은 확인해보록하겠습니다.

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;

import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
public class TestingWebApplicationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void shouldReturnDefaultMessage() throws Exception {
        this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk())
                .andExpect(content().string(containsString("Hello, World")));
    }
}

이 테스트에서는 전체 Spring 애플리케이션 컨텍스트가 시작되지만 별도의 서버가 시작되지 않았습니다.

[    Test worker] c.s.testing.TestingWebApplicationTest    : Starting TestingWebApplicationTest using Java 11.0.15 on My-PC with PID 24928
[    Test worker] c.s.testing.TestingWebApplicationTest    : No active profile set, falling back to 1 default profile: "default"
[    Test worker] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
[    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : Initializing Servlet ''
[    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : Completed initialization in 2 ms
[    Test worker] c.s.testing.TestingWebApplicationTest    : Started TestingWebApplicationTest in 1.432 seconds (JVM running for 2.742)

 

다음 예제(WebLayerTest.java)의 @WebMvcTest를 사용하여 테스트 범위를 웹 레이어로만 좁힐 수 있습니다.

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

import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest
public class WebLayerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void shouldReturnDefaultMessage() throws Exception {
        this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk())
                .andExpect(content().string(containsString("Hello, World")));
    }
}

 

테스트 어설션은 이전 경우와 동일합니다. 그러나 이 테스트에서 Spring Boot는 전체 컨텍스트가 아닌 웹 계층만 인스턴스화합니다. 여러 컨트롤러가 있는 응용 프로그램에서는 예를 들어 @WebMvcTest(HomeController.class)를 사용하여 하나만 인스턴스화하도록 요청할 수도 있습니다. (클래스를 지정하지 않는다면 컨트롤러와 연관된 어노테이션을 모두 빈으로 등록합니다.)

 

HomeController는 간단하고 의존성 주입도 하지 않았습니다. 다음 예제에서는 의존성 주입이 사용된 컨트롤러를 테스트 하는 방식을 알아보겠습니다

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class GreetingController {

	private final GreetingService service;

	public GreetingController(GreetingService service) {
		this.service = service;
	}

	@RequestMapping("/greeting")
	public @ResponseBody String greeting() {
		return service.greet();
	}

}
import org.springframework.stereotype.Service;

@Service
public class GreetingService {
	public String greet() {
		return "Hello, World";
	}
}

GreetingController를 작성하고 GreetingService에 대한 의존성을 주입 받을 수 있도록 추가해주었습니다.

Spring은 (생성자 주입) 자동으로 서비스 의존성을 컨트롤러에 주입합니다. 다음 예제(WebMockTest.java에서)에서 @WebMvcTest로 이 컨트롤러를 테스트하는 코드를 작성해보겠습니다.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.Matchers.containsString;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(GreetingController.class)
public class WebMockTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private GreetingService service;

    @Test
    public void greetingShouldReturnMessageFromService() throws Exception {
        when(service.greet()).thenReturn("Hello, Mock");
        this.mockMvc.perform(get("/greeting")).andDo(print()).andExpect(status().isOk())
                .andExpect(content().string(containsString("Hello, Mock")));
    }
}

@MockBean을 사용하여 GreetingService에 대한 mock을 생성(@WebMvcTest는 @Controller와 관련된 설정들만 읽기 때문에 @Service가 적용되어있는 GreetingService에 의존 주입없이 controller 코드를 테스트하기 위해 @MockBean을 추가함)하고 주입하고(그렇지 않으면 애플리케이션 컨텍스트를 시작할 수 없음) Mockito를 사용하여 기대치를 설정(andExpect())합니다.

4. 요약

@SpringBootTest

  • 통합 테스트 용도로 사용됨
  • @SpringBootApplication을 찾아가 하위의 모든 Bean을 스캔하여 로드함
  • 그 후 Test용 Applicatoin Context를 만들어 Bean을 추가하고, MockBean을 찾아 교체함

@ExtendWith

  • Junit4의 @RunWith와 같음
  • 메인으로 실행될 Class를 지정할 수 있음
  • @SpringBootTest에 @ExtendWith(SpringExtension.class)로 포함되어있다.(junit < 5버전을 사용한다면 @RunWith(SpringRunner.class))

@WebMvcTest(Class명.class)

  • ()에 작성된 클래스만 로드하여 테스트 진행
  • 매개변수를 지정해주지 않으면 @Controller, @RestController, @RestControllerAdvice 등 컨트롤러와 연관된 Bean이 모두 로드됨
  • 스프링의 모든 Bean을 로드하는 @SpringBootTest 대신 컨트롤러 관련 코드만 테스트 할 경우 사용 (@Service, @Component, @Repository 등은 사용할 수 없습니다.)

@Autowired about Mockbean

  • Controller의 API를 테스트하는 용도인 MockMvc 객체를 주입 받음
  • perform() 메서드를 활용하여 컨트롤러의 동작을 확인할 수 있음. andExpect(), andDo(), andReturn() 등의 메서드를 같이 활용함

@Mockbean

  • 테스트할 클래스에서 주입 받고 있는 객체에 대해 가짜 객체를 생성해주는 어노테이션
  • 해당 객체는 실제 행위를 하지 않음
  • given() 메서드를 활용하여 가짜 객체의 동작에 대해 정의하여 사용할 수 있음

@AutoConfigureMockMvc

  • spring.test.mockmvc의 설정을 로드하면서 MockMvc의 의존성을 자동으로 주입
  • MockMvc 클래스는 REST API 테스트를 할 수 있는 클래스

@Import

  • 필요한 Class들을 Configuration으로 만들어 사용할 수 있음
  • Configuration Component 클래스도 의존성을 설정할 수 있음
  • Import된 클래스는 주입으로 사용 가능

 


반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2025/02   »
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
글 보관함