테스트 코드 - 실습

ChoRong0824·2025년 3월 19일
0

Web

목록 보기
49/51
post-thumbnail

의존성

bom이 알아서 아래 junit의 버전을 bom 버전으로 맞춰주겠다는 뜻입니다.

dependencies {
    testImplementation platform('org.junit:junit-bom:5.9.1')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

bom이란?
Bill of Meterials(자재 명세서)라는 뜻인데, 의존성 버전 관리를 위해 쓴다.

Spring Boot

dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

스프링 부트에서는 이미 호환되는 버전의 테스트 관련 라이브러리들이 제공된다.

  • 테스트는 중요하기에 따로 테스트 관련 의존성을 넣지 않더라도 default로 포함되어있다.

Given-When-Then 패턴

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class BasicTest {
    @Test
    @DisplayName("더하기 테스트")
    public void calTest() {
        // given
        int a = 1;
        int b = 3;

        // when
        int sum = a + b;

        // then
        assertEquals(4, sum);
    }
}

Annotation 종류

1. @DataJpaTest

  • JPA와 관련된 Component들만 모두 가지고와 Repository Layer 단위 테스트를 위한 Annotation이다.
  • 기본적으로 In-memory DB(ex H2)를 사용하여 테스팅

요약: JPA 레포지토리 단위 테스트

2. @ExtendWith

  • Junit5 환경에서 확장기능을 사용할때 사용.
  • 주로 MockitoExtension 과 함께 Service Layer, 클래스 단위 테스트에 사용
    서비스만 하지만, @Component 되어있는 것들도 ExtendWith로 테스트 할 수 있습니다.
    (예를 들면, PasswordEncoder가 Component로 등록되어있다면 가능함)
    서비스를 타고 들어가보면 서비스에 @Component 어노테이션이 달려있는 것을 확인할 수 있습니다.

요약: 서비스 단위 테스트

3. @WebMvcTest

  • 스프링의 Web Layer(controller, filter 등)을 테스트하기 위한 Annotation이다.
  • @Controller, @ControllerAdvice 등 웹과 관련된 Bean만 로드하여 테스트를 수행한다.

요약: 컨트롤러 단위 테스트

4. @SpringBootTest

  • 스프링 부트 전체를 테스트 수행하기 위해서 사용하는 Annotation이다.
  • 서버를 실행하듯 모든 스프링 Context를 로드한다.

요약: 통합 테스트

사실 파란색 글씨만 보고 외워두는 것이 좀 더 편함.. ㅋㅋ


Mocking

테스트 코드를 작성하면 테스트를 하기 위한 코드 이외의 의존 객체들이 존재하는 경우가 있습니다.
예를 들면 Service 코드를 테스트 하는데 Repository가 필요한 경우이죠.
-> WhY?
서비스, 컨트롤러, 레포지토리는 서로 연관관계가 있습니다.
레포지토리를 변경하면 당연히 서비스에도 영향을 미칩니다.
이러한 것을 테스트하기 위해 "넌, 이런 놈이야"라고 MOCKING 하는 것입니다.
즉, 진짜를 테스트할건데 저런 놈이 방해를 하면 안되니까 가짜로 만들어 놓는 것입니다.

사용 이유

  1. 외부 의존성 제거
  2. 테스트 범위 준수
  3. 에러 상황 강제 발생

행위 검증(Behavior Validation), 상태 검증(State Validation)

  • 행위 검증

    • “테스트시 특정한 행위를 하였는가”를 확인하는 행위이다.
    • 테스트를 하려는 코드가 어떤 메소드를 호출하였는지 하지 않았는지, 몇번 수행했는지 확인.
    • Mockito의 verify() 메소드를 통해 검증
  • 상태 검증

    • “기능 수행 후 결과값이 기대값과 일치하는가?”를 확인하는 행위이다.
    • 반환값, DB 저장된 데이터 등 값에 대해 의도한대로 반환/저장이 되는지 확인.

  • application-test.properties

spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;
spring.datasource.username=root
spring.datasource.driver-class-name=org.h2.Driver

spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.database=h2 

직접 쿼리 적을 때 있는데, mysql로 짜는데 h2 등 다른 db엔 실행이 안될 수 있는데 ANSI 표준을 사용해서 비슷하긴 하지만 지원 문법이 다르기 떄문입니다.


테스트 코드 실습

🔥 RepositoryServiceController 순서로 테스트 코드 작성!

하는 것이 기본이지만, Repository 같은 경우는 과거에 비해 쿼리를 간단하게 가져가는 추세이기 때문에 그렇게 복잡한 로직이 잘 없다. 복잡한 로직이 없으므로 예외 경우의 수가 거의 없다. 따라서 중요도가 떨어진다 판단하여 우선순위에서 밀리는 경우도 있다.

특히, JPA의 단순 save(), findAll(), findById() 같은 메서드들은 이미 라이브러리에서 검증이 끝난 메서드이므로 테스트를 하는 것이 크게 의미 없을 수 있다.


Repository 테스트(@DataJpaTest)

테스트 하려는 해당 클래스에서 클래스 명에 커서를 올리고 커맨드 N을 누르면 Generate로 넘어간다.
이때 테스트 클래스를 만들면 알아서 해당 레포지토리랑 테스트 레포지토리랑 디렉토리 위치가 동일하게 된다.
(가독성 매우 좋아짐) 이는 매우 유용하기 때문에 알아두면 좋다.

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@DataJpaTest
class UserRepositoryTest {

  @Autowired
  private UserRepository userRepository;

  @Test
  void 이메일로_사용자를_조회할_수_있다() {
      // given
      String email = "asd@asd.com";
      User user = new User(email, "password", UserRole.USER);
      userRepository.save(user);

      // when
      User foundUser = userRepository.findByEmail(email).orElse(null);

      // then
      assertNotNull(foundUser);
      assertEquals(email, foundUser.getEmail());
      assertEquals(UserRole.USER, foundUser.getUserRole());
  }
}

@Autowired를 컨트롤러나 실제 사용하는 app 코드에는 지양해야합니다.
그러나, 자동으로 생기는 조건이 있습니다. 이는 무엇일까요 ?
생성자가 하나일경우 Autowired가 자동으로 생깁니다.
그래서 기본 생성자를 안써줘도 됐던 것입니다.


Service 테스트(@ExtendWith(MockitoExtension.class))

  • @Mock: 테스트 대상 외의 의존 객체
  • @InjectMocks: 테스트 대상 객체

케이스1

  @ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock // 테스트 대상 아님
    private UserRepository userRepository;

    @InjectMocks // 테스트 대상
    private UserService userService;

    @Test
    void User를_ID로_조회할_수_있다() {
        // given
        String email = "asd@asd.com";
        long userId = 1L;
        User user = new User(email, "password", UserRole.USER);
        ReflectionTestUtils.setField(user, "id", userId);

        given(userRepository.findById(anyLong())).willReturn(Optional.of(user));

        // when
        UserResponse userResponse = userService.getUser(userId);

        // then
        assertThat(userResponse).isNotNull();
        assertThat(userResponse.getId()).isEqualTo(userId);
        assertThat(userResponse.getEmail()).isEqualTo(email);
    }

    @Test
    void 존재하지_않는_User를_조회_시_InvalidRequestException을_던진다() {
        // Given
        long userId = 1L;
        given(userRepository.findById(anyLong())).willReturn(Optional.empty());

        // When & Then
        assertThrows(InvalidRequestException.class,
                () -> userService.getUser(userId),
                "User not found"
        );
    }


케이스2


(케이스1과 동일한 UserService)

    @Test
  void User를_삭제할_수_있다() {
      // given
      long userId = 1L;
      given(userRepository.existsById(anyLong())).willReturn(true);
      doNothing().when(userRepository).deleteById(anyLong());

      // when
      userService.deleteUser(userId);

      // then
      verify(userRepository, times(1)).deleteById(userId);
  }

  @Test
  void 존재하지_않는_User를_삭제_시_InvalidRequestException를_던진다() {
      // given
      long userId = 1L;
      given(userRepository.existsById(userId)).willReturn(false);

      // when & then
      assertThrows(InvalidRequestException.class, () -> userService.deleteUser(userId));
      verify(userRepository, never()).deleteById(userId);
  }
}

(케이스1과 동일한 UserServiceTest)


Controller 테스트(@WebMvcTest(XxxController.class))

Controller 테스트는 스프링 부트 3.4.x부터 업데이트 사항이 존재합니다. 이에 유의해주세요!
[Spring Boot 3.4 Release Notes]

@MockBean: Bean을 Mocking(3.4.x 이전. 3.4.x부터는 deprecated되어 사용 불가)
@MockitoBean: Bean을 Mocking(3.4.x 이후)
MockMvc: 스프링 MVC 컨트롤러를 테스트할 수 있게 해주는 모의 HTTP 요청/응답 도구
MockMvcTester: MockMvc의 래핑(상위 추상화) 버전, AssertJ와 더 잘 통합될 수 있음(3.4.x 이후)

AssertJ란

JUnit5을 보완하기 위해 나온 메서드 체이닝을 통한 fluent API 검증 라이브러리. 스프링부트에 공식 통합되어있지만, 아직 많이 쓰이는 느낌은 아님.

예시)

@Test
void test() {
  // JUnit 기본 검증
  User user = new User("asd@asd.com", "password", UserRole.USER);
  assertNotNull(user);
  assertEquals("asd@asd.com", user.getEmail());
  assertEquals("password", user.getPassword());
  assertEquals(UserRole.USER, user.getUserRole());

  // AssertJ 검증
  assertThat(user)
          .isNotNull()
          .extracting(User::getEmail, User::getPassword, User::getUserRole)
          .containsExactly("asd@asd.com", "password", UserRole.USER);
}

(스프링 공식 문서에도 AssertJ를 통한 검증을 더 추천하긴 합니다)

💁‍♂️ 과제 프로젝트는 3.3.x버전이기에 @MockBean과 MockMvc를 활용합니다.
실무에서도 아직 3.4를 도입한 회사가 없을 것이기에, 여러분들이 취업을 하셔도 한동안 @MockitoBean과 MockMvcTester를 사용할 일은 아직 없습니다.

다만, 여러분들이 직접 생성한 프로젝트는 3.4.x버전이므로 @MockitoBean과 MockMvc를 활용해주세요!

(테스트 대상)
UserResponse 참고해서 테스트 하면 좋을 것 같습니다.

@Getter
public class UserResponse {

  private final Long id;
  private final String email;

  public UserResponse(Long id, String email) {
      this.id = id;
      this.email = email;
  }
}
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserController.class)
class UserControllerTest {

  @Autowired
  private MockMvc mockMvc;

  @MockBean
  private UserService userService;

  @Test
  void User_목록_조회_빈리스트() throws Exception {
      // given
      given(userService.getUsers()).willReturn(List.of());

      // when & then
      mockMvc.perform(get("/users"))
              .andExpect(status().isOk())
              .andExpect(jsonPath("$").isEmpty());
  }

  @Test
  void User_목록_조회() throws Exception {
      // given
      long userId1 = 1L;
      long userId2 = 2L;
      String email1 = "user1@a.com";
      String email2 = "user2@a.com";
      List<UserResponse> userList = List.of(
              new UserResponse(userId1, email1),
              new UserResponse(userId2, email2)
      );
      given(userService.getUsers()).willReturn(userList);

      // when & then
      mockMvc.perform(get("/users"))
              .andExpect(status().isOk())
              .andExpect(jsonPath("$.length()").value(2))
              .andExpect(jsonPath("$[0].id").value(userId1))
              .andExpect(jsonPath("$[0].email").value(email1))
              .andExpect(jsonPath("$[1].id").value(userId2))
              .andExpect(jsonPath("$[1].email").value(email2));
  }

  @Test
  void User_단건_조회() throws Exception {
      // given
      long userId = 1L;
      String email = "a@a.com";

      given(userService.getUser(userId)).willReturn(new UserResponse(userId, email));

      // when & then
      mockMvc.perform(get("/users/{userId}", userId))
              .andExpect(status().isOk())
              .andExpect(jsonPath("$.id").value(userId))
              .andExpect(jsonPath("$.email").value(email));
  }
}

'$' 값을 넣어주는 이유: MockMVC에서 '$' 값은 Json 최상위 루트입니다.

통합 테스트

@SpringBootTest 사용
-> 진짜 스프링을 띄우는 것과 마찬가지기 때문에, 스프링에 썻던 것을 그대로 사용할 수 있습니다.

@ActiveProfiles("test")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserIntegrationTest {

  @LocalServerPort
  private int port;

  @Autowired
  private TestRestTemplate restTemplate;

  @Autowired
  private JwtUtil jwtUtil;

  @Test
  @DisplayName("회원가입 후 User 단건 조회를 한다.")
  void getUserIntegrationTest() {
      // 회원가입
      String signupUrl = "http://localhost:" + port + "/auth/signup";
      SignupRequest signupRequest = new SignupRequest("user@example.com", "password", "USER");
      ResponseEntity<SignupResponse> signupResponse = restTemplate.postForEntity(signupUrl, signupRequest, SignupResponse.class);

      // 회원가입 검증
      assertThat(signupResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
      SignupResponse signupBody = signupResponse.getBody();
      assertThat(signupBody).isNotNull();
      String token = signupBody.getBearerToken();
      assertThat(token).isNotBlank();

      // JWT 에서 userId 추출
      String rawToken = jwtUtil.substringToken(token);
      Claims claims = jwtUtil.extractClaims(rawToken);
      Long userIdFromToken = Long.valueOf(claims.getSubject());
      assertThat(userIdFromToken).isNotNull();

      // User 정보 단건 조회
      String url = "http://localhost:" + port + "/users/" + userIdFromToken;
      HttpHeaders headers = new HttpHeaders();
      headers.set("Authorization", token);
      HttpEntity<Void> requestEntity = new HttpEntity<>(headers);

      ResponseEntity<UserResponse> userResponse = restTemplate.exchange(
              url,
              HttpMethod.GET,
              requestEntity,
              UserResponse.class
      );

      // 검증
      assertThat(userResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
      UserResponse responseBody = userResponse.getBody();
      assertThat(responseBody).isNotNull();
      assertThat(responseBody.getId()).isEqualTo(userIdFromToken);
      assertThat(responseBody.getEmail()).isEqualTo("user@example.com");
  }
}

/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.test.web.servlet.assertj;

import java.net.URI;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.function.Function;

import jakarta.servlet.DispatcherType;
import org.assertj.core.api.AssertProvider;

import org.springframework.http.HttpMethod;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockMultipartHttpServletRequest;
import org.springframework.test.http.HttpMessageContentConverter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.AbstractMockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.AbstractMockMultipartHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder;
import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;

/**
* {@code MockMvcTester} provides support for testing Spring MVC applications
* with {@link MockMvc} for server request handling using
* {@linkplain org.assertj.core.api.Assertions AssertJ}.
*
* <p>A tester instance can be created from a {@link WebApplicationContext}:
* <pre><code class="java">
* // Create an instance with default settings
* MockMvcTester mvc = MockMvcTester.from(applicationContext);
*
* // Create an instance with a custom Filter
* MockMvcTester mvc = MockMvcTester.from(applicationContext,
*         builder -> builder.addFilters(filter).build());
* </code></pre>
*
* <p>A tester can be created in standalone mode by providing the controller
* instances to include:<pre><code class="java">
* // Create an instance for PersonController
* MockMvcTester mvc = MockMvcTester.of(new PersonController());
* </code></pre>
*
* <p>Simple, single-statement assertions can be done wrapping the request
* builder in {@code assertThat()} provides access to assertions. For instance:
* <pre><code class="java">
* // perform a GET on /hi and assert the response body is equal to Hello
* assertThat(mvc.get().uri("/hi")).hasStatusOk().hasBodyTextEqualTo("Hello");
* </code></pre>
*
* <p>For more complex scenarios the {@linkplain MvcTestResult result} of the
* exchange can be assigned in a variable to run multiple assertions:
* <pre><code class="java">
* // perform a POST on /save and assert the response body is empty
* MvcTestResult result = mvc.post().uri("/save").exchange();
* assertThat(result).hasStatus(HttpStatus.CREATED);
* assertThat(result).body().isEmpty();
* </code></pre>
*
* <p>If the request is processing asynchronously, {@code exchange} waits for
* its completion, either using the
* {@linkplain org.springframework.mock.web.MockAsyncContext#setTimeout default
* timeout} or a given one. If you prefer to get the result of an
* asynchronous request immediately, use {@code asyncExchange}:
* <pre><code class="java">
* // perform a POST on /save and assert an asynchronous request has started
* assertThat(mvc.post().uri("/save").asyncExchange()).request().hasAsyncStarted();
* </code></pre>
*
* <p>You can also perform requests using the static builders approach that
* {@link MockMvc} uses. For instance:<pre><code class="java">
* // perform a GET on /hi and assert the response body is equal to Hello
* assertThat(mvc.perform(get("/hi")))
*         .hasStatusOk().hasBodyTextEqualTo("Hello");
* </code></pre>
*
* <p>Use this approach if you have a custom {@link RequestBuilder} implementation
* that you'd like to integrate here. This approach is also invoking {@link MockMvc}
* without any additional processing of asynchronous requests.
*
* <p>One main difference between {@link MockMvc} and {@code MockMvcTester} is
* that an unresolved exception is not thrown directly when using
* {@code MockMvcTester}. Rather an {@link MvcTestResult} is available with an
* {@linkplain MvcTestResult#getUnresolvedException() unresolved exception}.
* Both resolved and unresolved exceptions are considered a failure that can
* be asserted as follows:
* <pre><code class="java">
* // perform a GET on /boom and assert the message for the exception
* assertThat(mvc.get().uri("/boom")).hasFailed()
*         .failure().hasMessage("Test exception");
* </code></pre>
*
* <p>Any attempt to access the result with an unresolved exception will
* throw an {@link AssertionError}:
* <pre><code class="java">
* // throw an AssertionError with an unresolved exception
* assertThat(mvc.get().uri("/boom")).hasStatus5xxServerError();
* </code></pre>
*
* <p>{@code MockMvcTester} can be configured with a list of
* {@linkplain HttpMessageConverter message converters} to allow the response
* body to be deserialized, rather than asserting on the raw values.
*
* @author Stephane Nicoll
* @author Brian Clozel
* @since 6.2
*/
public final class MockMvcTester {

	private final MockMvc mockMvc;

	@Nullable
	private final HttpMessageContentConverter contentConverter;


	private MockMvcTester(MockMvc mockMvc, @Nullable HttpMessageContentConverter contentConverter) {
		Assert.notNull(mockMvc, "mockMVC should not be null");
		this.mockMvc = mockMvc;
		this.contentConverter = contentConverter;
	}

	/**
	 * Create an instance that delegates to the given {@link MockMvc} instance.
	 * @param mockMvc the MockMvc instance to delegate calls to
	 */
	public static MockMvcTester create(MockMvc mockMvc) {
		return new MockMvcTester(mockMvc, null);
	}

	/**
	 * Create an instance using the given, fully initialized (i.e.,
	 * <em>refreshed</em>) {@link WebApplicationContext}. The given
	 * {@code customizations} are applied to the {@link DefaultMockMvcBuilder}
	 * that ultimately creates the underlying {@link MockMvc} instance.
	 * <p>If no further customization of the underlying {@link MockMvc} instance
	 * is required, use {@link #from(WebApplicationContext)}.
	 * @param applicationContext the application context to detect the Spring
	 * MVC infrastructure and application controllers from
	 * @param customizations a function that creates a {@link MockMvc}
	 * instance based on a {@link DefaultMockMvcBuilder}
	 * @see MockMvcBuilders#webAppContextSetup(WebApplicationContext)
	 */
	public static MockMvcTester from(WebApplicationContext applicationContext,
			Function<DefaultMockMvcBuilder, MockMvc> customizations) {

		DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(applicationContext);
		MockMvc mockMvc = customizations.apply(builder);
		return create(mockMvc);
	}

	/**
	 * Shortcut to create an instance using the given fully initialized (i.e.,
	 * <em>refreshed</em>) {@link WebApplicationContext}.
	 * <p>Consider using {@link #from(WebApplicationContext, Function)} if
	 * further customization of the underlying {@link MockMvc} instance is
	 * required.
	 * @param applicationContext the application context to detect the Spring
	 * MVC infrastructure and application controllers from
	 * @see MockMvcBuilders#webAppContextSetup(WebApplicationContext)
	 */
	public static MockMvcTester from(WebApplicationContext applicationContext) {
		return from(applicationContext, DefaultMockMvcBuilder::build);
	}

	/**
	 * Create an instance by registering one or more {@code @Controller} instances
	 * and configuring Spring MVC infrastructure programmatically.
	 * <p>This allows full control over the instantiation and initialization of
	 * controllers and their dependencies, similar to plain unit tests while
	 * also making it possible to test one controller at a time.
	 * @param controllers one or more {@code @Controller} instances or
	 * {@code @Controller} types to test; a type ({@code Class}) will be turned
	 * into an instance
	 * @param customizations a function that creates a {@link MockMvc} instance
	 * based on a {@link StandaloneMockMvcBuilder}, typically to configure the
	 * Spring MVC infrastructure
	 * @see MockMvcBuilders#standaloneSetup(Object...)
	 */
	public static MockMvcTester of(Collection<?> controllers,
			Function<StandaloneMockMvcBuilder, MockMvc> customizations) {

		StandaloneMockMvcBuilder builder = MockMvcBuilders.standaloneSetup(controllers.toArray());
		return create(customizations.apply(builder));
	}

	/**
	 * Shortcut to create an instance by registering one or more {@code @Controller}
	 * instances.
	 * <p>The minimum infrastructure required by the
	 * {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet}
	 * to serve requests with annotated controllers is created. Consider using
	 * {@link #of(Collection, Function)} if additional configuration of the MVC
	 * infrastructure is required.
	 * @param controllers one or more {@code @Controller} instances or
	 * {@code @Controller} types to test; a type ({@code Class}) will be turned
	 * into an instance
	 * @see MockMvcBuilders#standaloneSetup(Object...)
	 */
	public static MockMvcTester of(Object... controllers) {
		return of(Arrays.asList(controllers), StandaloneMockMvcBuilder::build);
	}

	/**
	 * Return a new instance using the specified {@linkplain HttpMessageConverter
	 * message converters}.
	 * <p>If none are specified, only basic assertions on the response body can
	 * be performed. Consider registering a suitable JSON converter for asserting
	 * against JSON data structures.
	 * @param httpMessageConverters the message converters to use
	 * @return a new instance using the specified converters
	 */
	public MockMvcTester withHttpMessageConverters(Iterable<HttpMessageConverter<?>> httpMessageConverters) {
		return new MockMvcTester(this.mockMvc, HttpMessageContentConverter.of(httpMessageConverters));
	}

	/**
	 * Prepare an HTTP GET request.
	 * <p>The returned builder can be wrapped in {@code assertThat} to enable
	 * assertions on the result. For multi-statements assertions, use
	 * {@link MockMvcRequestBuilder#exchange() exchange()} to assign the
	 * result. To control the time to wait for asynchronous request to complete
	 * on a per-request basis, use
	 * {@link MockMvcRequestBuilder#exchange(Duration) exchange(Duration)}.
	 * @return a request builder for specifying the target URI
	 */
	public MockMvcRequestBuilder get() {
		return method(HttpMethod.GET);
	}

	/**
	 * Prepare an HTTP HEAD request.
	 * <p>The returned builder can be wrapped in {@code assertThat} to enable
	 * assertions on the result. For multi-statements assertions, use
	 * {@link MockMvcRequestBuilder#exchange() exchange()} to assign the
	 * result. To control the time to wait for asynchronous request to complete
	 * on a per-request basis, use
	 * {@link MockMvcRequestBuilder#exchange(Duration) exchange(Duration)}.
	 * @return a request builder for specifying the target URI
	 */
	public MockMvcRequestBuilder head() {
		return method(HttpMethod.HEAD);
	}

	/**
	 * Prepare an HTTP POST request.
	 * <p>The returned builder can be wrapped in {@code assertThat} to enable
	 * assertions on the result. For multi-statements assertions, use
	 * {@link MockMvcRequestBuilder#exchange() exchange()} to assign the
	 * result. To control the time to wait for asynchronous request to complete
	 * on a per-request basis, use
	 * {@link MockMvcRequestBuilder#exchange(Duration) exchange(Duration)}.
	 * @return a request builder for specifying the target URI
	 */
	public MockMvcRequestBuilder post() {
		return method(HttpMethod.POST);
	}

	/**
	 * Prepare an HTTP PUT request.
	 * <p>The returned builder can be wrapped in {@code assertThat} to enable
	 * assertions on the result. For multi-statements assertions, use
	 * {@link MockMvcRequestBuilder#exchange() exchange()} to assign the
	 * result. To control the time to wait for asynchronous request to complete
	 * on a per-request basis, use
	 * {@link MockMvcRequestBuilder#exchange(Duration) exchange(Duration)}.
	 * @return a request builder for specifying the target URI
	 */
	public MockMvcRequestBuilder put() {
		return method(HttpMethod.PUT);
	}

	/**
	 * Prepare an HTTP PATCH request.
	 * <p>The returned builder can be wrapped in {@code assertThat} to enable
	 * assertions on the result. For multi-statements assertions, use
	 * {@link MockMvcRequestBuilder#exchange() exchange()} to assign the
	 * result. To control the time to wait for asynchronous request to complete
	 * on a per-request basis, use
	 * {@link MockMvcRequestBuilder#exchange(Duration) exchange(Duration)}.
	 * @return a request builder for specifying the target URI
	 */
	public MockMvcRequestBuilder patch() {
		return method(HttpMethod.PATCH);
	}

	/**
	 * Prepare an HTTP DELETE request.
	 * <p>The returned builder can be wrapped in {@code assertThat} to enable
	 * assertions on the result. For multi-statements assertions, use
	 * {@link MockMvcRequestBuilder#exchange() exchange()} to assign the
	 * result. To control the time to wait for asynchronous request to complete
	 * on a per-request basis, use
	 * {@link MockMvcRequestBuilder#exchange(Duration) exchange(Duration)}.
	 * @return a request builder for specifying the target URI
	 */
	public MockMvcRequestBuilder delete() {
		return method(HttpMethod.DELETE);
	}

	/**
	 * Prepare an HTTP OPTIONS request.
	 * <p>The returned builder can be wrapped in {@code assertThat} to enable
	 * assertions on the result. For multi-statements assertions, use
	 * {@link MockMvcRequestBuilder#exchange() exchange()} to assign the
	 * result. To control the time to wait for asynchronous request to complete
	 * on a per-request basis, use
	 * {@link MockMvcRequestBuilder#exchange(Duration) exchange(Duration)}.
	 * @return a request builder for specifying the target URI
	 */
	public MockMvcRequestBuilder options() {
		return method(HttpMethod.OPTIONS);
	}

	/**
	 * Prepare a request for the specified {@code HttpMethod}.
	 * <p>The returned builder can be wrapped in {@code assertThat} to enable
	 * assertions on the result. For multi-statements assertions, use
	 * {@link MockMvcRequestBuilder#exchange() exchange()} to assign the
	 * result. To control the time to wait for asynchronous request to complete
	 * on a per-request basis, use
	 * {@link MockMvcRequestBuilder#exchange(Duration) exchange(Duration)}.
	 * @return a request builder for specifying the target URI
	 */
	public MockMvcRequestBuilder method(HttpMethod method) {
		return new MockMvcRequestBuilder(method);
	}

	/**
	 * Perform a request using the given {@link RequestBuilder} and return a
	 * {@link MvcTestResult result} that can be used with standard
	 * {@link org.assertj.core.api.Assertions AssertJ} assertions.
	 * <p>Use only this method if you need to provide a custom
	 * {@link RequestBuilder}. For regular cases, users should initiate the
	 * configuration of the request using one of the methods available on
	 * this instance, for example, {@link #get()} for HTTP GET.
	 * <p>Contrary to {@link MockMvc#perform(RequestBuilder)}, this does not
	 * throw an exception if the request fails with an unresolved exception.
	 * Rather, the result provides the exception, if any. Assuming that a
	 * {@link MockMvcRequestBuilders#post(URI) POST} request against
	 * {@code /boom} throws an {@code IllegalStateException}, the following
	 * asserts that the invocation has indeed failed with the expected error
	 * message:
	 * <pre><code class="java">assertThat(mvc.post().uri("/boom")))
	 *       .failure().isInstanceOf(IllegalStateException.class)
	 *       .hasMessage("Expected");
	 * </code></pre>
	 * @param requestBuilder used to prepare the request to execute
	 * @return an {@link MvcTestResult} to be wrapped in {@code assertThat}
	 * @see MockMvc#perform(RequestBuilder)
	 * @see #method(HttpMethod)
	 */
	public MvcTestResult perform(RequestBuilder requestBuilder) {
		Object result = getMvcResultOrFailure(requestBuilder);
		if (result instanceof MvcResult mvcResult) {
			return new DefaultMvcTestResult(mvcResult, null, this.contentConverter);
		}
		else {
			return new DefaultMvcTestResult(null, (Exception) result, this.contentConverter);
		}
	}

	private Object getMvcResultOrFailure(RequestBuilder requestBuilder) {
		try {
			return this.mockMvc.perform(requestBuilder).andReturn();
		}
		catch (Exception ex) {
			return ex;
		}
	}

	/**
	 * Execute the request using the specified {@link RequestBuilder}. If the
	 * request is processing asynchronously, wait at most the given
	 * {@code timeToWait} duration. If not specified, then fall back on the
	 * timeout value associated with the async request, see
	 * {@link org.springframework.mock.web.MockAsyncContext#setTimeout}.
	 */
	MvcTestResult exchange(RequestBuilder requestBuilder, @Nullable Duration timeToWait) {
		MvcTestResult result = perform(requestBuilder);
		if (result.getUnresolvedException() == null) {
			if (result.getRequest().isAsyncStarted()) {
				// Wait for async result before dispatching
				long waitMs = (timeToWait != null ? timeToWait.toMillis() : -1);
				result.getMvcResult().getAsyncResult(waitMs);

				// Perform ASYNC dispatch
				RequestBuilder dispatchRequest = servletContext -> {
					MockHttpServletRequest request = result.getMvcResult().getRequest();
					request.setDispatcherType(DispatcherType.ASYNC);
					request.setAsyncStarted(false);
					return request;
				};
				return perform(dispatchRequest);
			}
		}
		return result;
	}


	/**
	 * A builder for {@link MockHttpServletRequest} that supports AssertJ.
	 */
	public final class MockMvcRequestBuilder extends AbstractMockHttpServletRequestBuilder<MockMvcRequestBuilder>
			implements AssertProvider<MvcTestResultAssert> {

		private final HttpMethod httpMethod;

		private MockMvcRequestBuilder(HttpMethod httpMethod) {
			super(httpMethod);
			this.httpMethod = httpMethod;
		}

		/**
		 * Enable file upload support using multipart.
		 * @return a {@link MockMultipartMvcRequestBuilder} with the settings
		 * configured thus far
		 */
		public MockMultipartMvcRequestBuilder multipart() {
			return new MockMultipartMvcRequestBuilder(this);
		}

		/**
		 * Execute the request. If the request is processing asynchronously,
		 * wait at most the given timeout value associated with the async request,
		 * see {@link org.springframework.mock.web.MockAsyncContext#setTimeout}.
		 * <p>For simple assertions, you can wrap this builder in
		 * {@code assertThat} rather than calling this method explicitly:
		 * <pre><code class="java">
		 * // These two examples are equivalent
		 * assertThat(mvc.get().uri("/greet")).hasStatusOk();
		 * assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk();
		 * </code></pre>
		 * <p>For assertions on the original asynchronous request that might
		 * still be in progress, use {@link #asyncExchange()}.
		 * @see #exchange(Duration) to customize the timeout for async requests
		 */
		public MvcTestResult exchange() {
			return MockMvcTester.this.exchange(this, null);
		}

		/**
		 * Execute the request and wait at most the given {@code timeToWait}
		 * duration for the asynchronous request to complete. If the request
		 * is not asynchronous, the {@code timeToWait} is ignored.
		 * <p>For assertions on the original asynchronous request that might
		 * still be in progress, use {@link #asyncExchange()}.
		 * @see #exchange()
		 */
		public MvcTestResult exchange(Duration timeToWait) {
			return MockMvcTester.this.exchange(this, timeToWait);
		}

		/**
		 * Execute the request and do not attempt to wait for the completion of
		 * an asynchronous request. Contrary to {@link #exchange()}, this returns
		 * the original result that might still be in progress.
		 */
		public MvcTestResult asyncExchange() {
			return MockMvcTester.this.perform(this);
		}

		@Override
		public MvcTestResultAssert assertThat() {
			return new MvcTestResultAssert(exchange(), MockMvcTester.this.contentConverter);
		}
	}

	/**
	 * A builder for {@link MockMultipartHttpServletRequest} that supports AssertJ.
	 */
	public final class MockMultipartMvcRequestBuilder
			extends AbstractMockMultipartHttpServletRequestBuilder<MockMultipartMvcRequestBuilder>
			implements AssertProvider<MvcTestResultAssert> {

		private MockMultipartMvcRequestBuilder(MockMvcRequestBuilder currentBuilder) {
			super(currentBuilder.httpMethod);
			merge(currentBuilder);
		}

		/**
		 * Execute the request. If the request is processing asynchronously,
		 * wait at most the given timeout value associated with the async request,
		 * see {@link org.springframework.mock.web.MockAsyncContext#setTimeout}.
		 * <p>For simple assertions, you can wrap this builder in
		 * {@code assertThat} rather than calling this method explicitly:
		 * <pre><code class="java">
		 * // These two examples are equivalent
		 * assertThat(mvc.get().uri("/greet")).hasStatusOk();
		 * assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk();
		 * </code></pre>
		 * <p>For assertions on the original asynchronous request that might
		 * still be in progress, use {@link #asyncExchange()}.
		 * @see #exchange(Duration) to customize the timeout for async requests
		 */
		public MvcTestResult exchange() {
			return MockMvcTester.this.exchange(this, null);
		}

		/**
		 * Execute the request and wait at most the given {@code timeToWait}
		 * duration for the asynchronous request to complete. If the request
		 * is not asynchronous, the {@code timeToWait} is ignored.
		 * <p>For assertions on the original asynchronous request that might
		 * still be in progress, use {@link #asyncExchange()}.
		 * @see #exchange()
		 */
		public MvcTestResult exchange(Duration timeToWait) {
			return MockMvcTester.this.exchange(this, timeToWait);
		}

		/**
		 * Execute the request and do not attempt to wait for the completion of
		 * an asynchronous request. Contrary to {@link #exchange()}, this returns
		 * the original result that might still be in progress.
		 */
		public MvcTestResult asyncExchange() {
			return MockMvcTester.this.perform(this);
		}

		@Override
		public MvcTestResultAssert assertThat() {
			return new MvcTestResultAssert(exchange(), MockMvcTester.this.contentConverter);
		}
	}

}

를 활용해서 테스트 코드를 작성하면 좋습니다.






🌟 해당 자료는 김선용 튜터님의 특강 자료를 토대로 만들어졌습니다

profile
백엔드를 지향하며, 컴퓨터공학과를 졸업한 취준생입니다. 많이 부족하지만 열심히 노력해서 실력을 갈고 닦겠습니다. 부족하고 틀린 부분이 있을 수도 있지만 이쁘게 봐주시면 감사하겠습니다. 틀린 부분은 댓글 남겨주시면 제가 따로 학습 및 자료를 찾아봐서 제 것으로 만들도록 하겠습니다. 귀중한 시간 방문해주셔서 감사합니다.

0개의 댓글