Spring Test

Content

Integration Test

Spring 공식 문서는 테스트를 크게 둘로 나눠서 설명한다.

  • Unit Testing

  • Integration Testing

어떤 프레임워크에서 제공하는 클래스가 있고 이것을 상속받아 사용하는 경우가 이전에는 많았었지만 Spring은 그렇지 않다. XML과 같이 외부 설정(Config)를 통해 의존성 주입을 받는다. 따라서 개발자는 비즈니스 로직에 집중할 수 있다.

Spring은 코드에 구조적으로 개입하는 게 적어서, 단위 테스트를 쉽게 작성할 수 있다.

언제 Spring의 힘을 빌려서 테스트 할까? IoC 컨테이너를 적극적으로 활용하고 싶거나, Spring Web MVC로 구현된 부분을 테스트하고 싶을 때다. 또는 Spring Data JPA를 사용한다던가..? 이것의 공통점은 이 기능들이 Spring의 힘을 빌려서 작동한다는 것이다. 이런 기능을 테스트하기 위해서는 Spring의 힘을 빌려서 테스트해야 한다. 이런 테스트를 통합 테스트라고 부른다.


Spring Boot Test

Spring Boot 1.4부터 @SpringBootTest Annotation을 써서 쉽게 테스트할 수 있다. 관련 링크

Spring Boot Test 실습 관련 일반적인 내용

PostController을 테스트 해보자.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

테스트의 환경을 설졍할 수 있다. 이 때 WebEnvironment Enum 값으로 어떤 웹 환경에서 테스트할 것인지 결정할 수 있다.

Difference between webEnvironment = RANDOM_PORT and webEnvironment = MOCK

이번 실습은 통합 테스트를 하기 위해 RANDOM_PORT로 지정한다. 통합 테스트는 최대한 프로덕션 환경과 유사할 수록 좋다고 합니다.

테스트 코드 설정의 편의를 위해 필드 주입을 이용

@Value("${local.server.port}")

필드에 값을 대입해 쓸 때는 @Value를 이용하게 된다.

Spring의 프로퍼티(예: application.properties 또는 application.yml 파일)나 환경 변수에서 local.server.port 키에 해당하는 값을 찾는다. 그리고 이 값을 필드 변수에 할당하게 된다.

TestRestTemplate

객체의 경우 필드에 대입할 때 @Autowired을 활용하게 된다. TestRestTemplate 빈을 주입받는다. IoC 컨테이너가 이를 주입해준다.

테스트할 때만 의존성이 생기게 된다.

게시물 목록 조회 테스트

  • body의 값은 response body이다. 객체 배열을 나타내는 Json String이죠? 이런식으로 asserThat 메소드에서 지원하는 문자열을 포함하는 지 확인하는 함수를 통해 assertion 하게 된다.

게시물 목록 생성 테스트

postForLocation 사용법만 알면 특별할 것은 없다. DTO를 postForLocation의 두번째 함수 인자로 넘겨줘야 하는 것을 주의하자.

DTO의 목적이 Web layer와 서버 간의 데이터 전송 용도임을 기억!

게시물 목록 삭제 테스트

정규식을 처리하는 헬퍼 함수를 이용해 삭제 테스트를 해보자.

목록 조회해서 받아온 문자열(문자열의 값은 Json 형태)에서 정규식을 이용해 마지막 Post의 Id를 찾고 이 Id로 다시 delete 요청을 테스트하는 흐름이다.


MockMvc를 이용한 테스트

테스트 논리는 생성하고, 제거할 때 목록의 사이즈를 비교하는 것이다.

static import로 코드 가독성을 높일 수 있음

참고 : 한글 인코딩 문제가 발생한다면

인코딩 문제를 해결하기 위해 application.properties 파일에 관련 설정 추가.

MockMvc를 사용하면서 불편한 점은 알아야할 가정이 많다는 것이다. 기존에 레포지토리에 뭐가 있는지 알아야하며, 각각의 id, content와 같은 속성의 값까지 알아야한다...


SpyBean을 이용한 테스트

Spies are stubs that also record some information based on how they were called.

Post 개수를 세는 방식, 즉 post 목록의 길이를 조회하는 방식이 아니라, 정말로 PostRepository의 save를 호출했는지만 확인해 보자.

SpyBean을 이용하면 이를 확인할 수 있다. spy는 호출될 때 기록을 남기기 때문이다.

verify를 통해서 SpyBean으로 주입받은 postRepository에서 save라는 메서드를 호출했는지 확인했다. save에는 Post 타입의 인자가 들어가서 호출되야 한다.

argThat을 이용해서 인자로 들어오는 객체의 속성까지 확인할 수 있다.

argThat의 매개변수를 확인해보자. 메서드 구현은 다음과 같다.

ArgumentMatcher의 구현은 아래와 같다.

유일한 추상 메서드를 가진 함수형 인터페이스이다. 때문에 argThat에 람다식을 인자로 넘길 수 있다.

리플렉션 활용

getter을 사용하기 싫다면 reflection을 활용할 수도 있다. (오버 엔지니어링)


MockBean

위의 테스트를 보면 controller(웹 레이어)에서 repository까지 너무 멀리 본다(?). 물론 이렇게 하는 것이 완전한 통합 테스트에서는 유리하다. (넓은 계층 범위에서 테스트 하는 것이 통합 테스트에 가까움)

중간에 다른 레이어도 있는데 이를 활용하지 않는다. 이를 활용할 수 있는 방법을 찾아보자

SpyBean(여기서는 PostRepository)으로 테스트하면 이와 관련된 것들이 싹 다 객체가 생성되서 올라가게 된다.

MockBean은 가짜로 필요한 빈만 넣어주기 위해 사용한다. 이 빈은 실제 로직을 포함하지 않으며, 가짜 응답을 제공한다.

- 사용법

SpringBootTest는 모든 Bean들을 다 IOC 컨테이너에 넣어준다. 따라서 테스트에서 모든 빈을 다 얻을 수 있다.

어노테이션을 @WebMvcTest로 변경하면 MockMvc 빈만 제공하고 나머지 빈은 제공하지 않는다.

다른 코드는 똑같이 유지하고 클래스 레벨의 어노테이션만 변경하고 테스트를 실행해보자.

에러가 발생한다. 에러 로그를 확인해보자.

PostController을 얻는데 실패했다.

이제 어노테이션에 인자를 넘기고 다시 실행해보자

여전히 에러가 발생한다.

에러 내용은 postController 빈을 생성하는데 실패했으며, 이유는 postController의 생성자에 필요한 값들이 들어오지 않았다는 것이다. 즉 의존성 주입이 되지 않았다.

PostCotroller의 생성자를 보면 빈으로 postService와 postListService를 주입받아야 하지만 이를 주입받지 못했다. 이유는 클래스 레벨에 @WebMvcTest의 인자로 PostController을 전달함으로써, 테스트 환경에서 PostController 클래스와 관련된 컨트롤러, 필터, 인터셉터 등만 로드하고, 그 외의 전체 Spring 컨텍스트는 로드하지 않기 때문이다.

이 상황에서 MockBean을 사용해 PostController에 필요한 빈을 넣어주어야 한다.

그래서 아래와 같이 테스트 코드를 작성할 수 있다.

❗️ 여기서도 Mockito의 verify를 사용함

헷갈리면 안 되는게, spybean을 사용할 때 verify를 사용하는게 아니다. 애초에 독립적인 개념이다. verify 메서드는 테스트 중에 특정 메서드가 호출되었는지, 호출되었다면 몇 번이나 호출되었는지 검증하는 데 사용됩니다.

- 정리

controller의 역할만 확인하는 테스트다. 관심사를 웹 레이어로 집중한다. 결론적으로

memo

@MockBean vs mock() method

  • @MockBean Annotation

    • Context: @MockBean is specific to Spring Boot and is used in the context of Spring's application context. It's part of the Spring Boot Test framework.

    • Functionality: When you annotate a field with @MockBean in a test, Spring Boot replaces the bean of the same type in the Spring application context with a Mockito mock. This is very useful when you want to add or replace a bean in the Spring application context with a mock for testing.

    • Scope: The mock will replace any existing bean of the same type in the entire Spring application context. Therefore, any Spring component that autowires this bean will get the mocked instance.

    • Use Case: It is typically used in integration tests where you want to mock away external dependencies (like database repositories, web services, etc.) while testing your Spring components.

  • mock() Method

    • Context: The mock() method is part of the Mockito framework and is not specific to Spring Boot. It can be used in any Java application.

    • Functionality: This method creates a plain Mockito mock object of the given class or interface. It's not aware of the Spring context and doesn't affect the Spring application context.

    • Scope: The scope of a mock created with mock() is limited to the test class where it's used. It doesn't replace any Spring beans in the context, and it's up to you to inject it into the object under test.

    • Use Case: It is commonly used in unit tests where you are testing a class in isolation and want to control its dependencies.

  • Comparison

    • Integration Testing vs. Unit Testing: @MockBean is more suited for integration tests within a Spring Boot environment, whereas mock() is typically used for plain unit tests without Spring Boot's involvement.

    • Spring Context Awareness: @MockBean interacts with the Spring context, replacing beans globally, while mock() creates standalone mocks without any interaction with the Spring context.

  • Overhead: @MockBean can add more overhead to your tests since it involves the Spring context, making tests slower compared to plain unit tests using mock().

Mockito any vs eq

참고 자료

  • any : 주어진 타입에 대해 어떤 구체적인 value든 상관없이 타입만 일치하면 된다.

  • eq : 타입과 함께 정확히 값도 비교한다. 이 때 타입의 equals 메서드를 통해 값을 비교하게 된다.

참고

Testing

Testing improvements in Spring Boot 1.4

Testing the Web Layer

@Value

SpyBean

TestDouble

Last updated