테스트코드/작성방법

4. 외부 시스템과의 연계 테스트

feel2 2024. 3. 26. 07:56
반응형

지금부터는 외부 시스템과의 연계에서 어떻게 테스트를 진행해야 하는지에 대해서 알아보자.

4.1. Mock에 대한 고찰

https://feel2.tistory.com/35 여길 통해 먼저 테스트 대역으로의 Mock과 도구로서의 Mock의 차이,

테스트 대역에서의 Mock 과 Stub 의 차이를 알아보고 오자.

https://feel2.tistory.com/32 또한 테스트를 바라보는 관점을 이를 통해 미리 알아보고 오자.

앞에 두 글을 읽고 왔다고 가정하고 이 글을 계속해서 진행해 보겠다.

지금부터 필자가 말하는 Mock은 도구로서의 Mock이라는 것을 상기하자.

테스트를 바라보는 관점은 크게 Mock을 쓰지 않는 관점과 Mock을 쓰는 관점이 있다.

필자가 생각했을 때 제일 좋은 것은 두 가지 모두 적절하게 섞어서 테스트를 진행하는 것이다.

그럼 언제 Mock을 써서 테스트하고 언제 Mock없이 테스트를 진행할까?

 

 

다음과 같은 상황을 생각해 보자. A , B 가 각각 하나의 레이어 계층이라고 가정해보자. A 와 B를 각각 테스트를 했을 때는 모든 테스트가 성공하였다. 그렇다고 A 와 B 레이어를 같이 테스트를 했을 때 성공을 보장할 수 있을까?

 

런타임 시에는 어떤 변수가 존재할 지 모르기에 각각의 레이어를 따로 검증하거나, 같이 통합테스트를 진행하는 경우 Mock 객체를 쓰는게 위험할 수가 있다.

 

그래서 앞에서 각 계층 테스트나 통합 테스트를 한 것을 보면 알 수 있겠지만, Mock 을 사용하여 테스트를 하는 것을 최대한 지양하고 있다.

그럼 언제 Mock을 사용하는 것이 좋을까?

 

https://feel2.tistory.com/39 여길 보면 언제 Mock을 사용할 때 좋은지에 대한 얘기가 나와 있다.

결론만 말하면, 비관리 의존성에만 Mock을 사용하는 것이 Mock의 가치를 극대화하는 시작이라고 나와있다.

(물론, 저 글에서는 통합 테스트를 할 경우에도 Mock 을 써서 테스트를 하라고 나와있지만, 여기서 말하는 통합테스트는 컨트롤러 계층을 포함한 것이기 때문에 그렇게 표현을 했다고 필자는 생각한다.)

 

다른 말로 하면 내가 제어할 수 없는 외부 시스템 계의 경우 이에 해당한다고 얘기할 수 있다.

여기서 말하는 대표적인 외부 시스템이 SMTP 서버를 통해 메일을 전송하는 것이다.

여기에 하나 더 필자가 생각했을 때 필요한 경우가 서버 to 서버로 api 요청을 했을 때라고 생각한다.

다른 서버로부터 api 요청의 경우에도 우리가 제어할 수 없는 외부 시스템으로부터의 응답이라고 생각하기 때문이다.

여기서는 서버 to 서버로 api 요청을 하는 상황에 대해서만 Mock을 어떻게 사용하는지 알아보겠다.

 

4.2 Using Mockito

 

https://www.baeldung.com/spring-mocking-webclient 이 문서를 참고하여 외부 API 요청 테스트를 작성해보자.

물론 Webclient를 stubbing하여 테스트를 진행할 수 있지만, 테스트 과정도 번거롭고, 서비스 단의 테스트를 작성하기 위해 WebClient 내부 동작을 알고, stubbing을 해줘야한다는 점에서부터 테스트 메서드의  역할/목적이 명확하지 못하게 된다.

@ExtendWith(MockitoExtension.class)
public class EmployeeServiceTest {
   
    @Test
    void givenEmployeeId_whenGetEmployeeById_thenReturnEmployee() {

        Integer employeeId = 100;
        Employee mockEmployee = new Employee(100, "Adam", "Sandler", 
          32, Role.LEAD_ENGINEER);
        when(webClientMock.get())
          .thenReturn(requestHeadersUriSpecMock);
        when(requestHeadersUriMock.uri("/employee/{id}", employeeId))
          .thenReturn(requestHeadersSpecMock);
        when(requestHeadersMock.retrieve())
          .thenReturn(responseSpecMock);
        when(responseMock.bodyToMono(Employee.class))
          .thenReturn(Mono.just(mockEmployee));

        Mono<Employee> employeeMono = employeeService.getEmployeeById(employeeId);

        StepVerifier.create(employeeMono)
          .expectNextMatches(employee -> employee.getRole()
            .equals(Role.LEAD_ENGINEER))
          .verifyComplete();
    }

}

 

위에서 이야기한 단점 때문에 저런 방식은 잘 사용되지 않으며, spring team에서도 MockWebServer가 추천되고 있다.

 

4.3 Using MockWebServer

 

MockWebServer는 HTTP Request를 받아, Response로 반환하는 간단한 웹서버이다.
Http를 호출하는 메서드의 테스트코드를 작성할 때 MockWebServer를 이용하면 쉽게 테스트를 작성할 수 있다.

MockWebServer를 사용하여 테스트를 하기 앞서 몇가지 의존성을 추가해야 한다.

//build.gradle
dependencies {
...
  //mockwebserver 관련 의존성 추가
  testImplementation("com.squareup.okhttp3:mockwebserver:4.9.2")
  //값 검증을 위한 의존성 추가
  testImplementation("io.projectreactor:reactor-test")
...
}

이제 간단하게 외부 API 호출 검증을 위한 Production code 보자.

 

//User

package org.example.api.domain;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class User {
    private int id;
    private String name;

    @Builder
    private User(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

 

//UserService

package org.example.api.service;

import lombok.RequiredArgsConstructor;
import org.example.api.domain.User;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;


@RequiredArgsConstructor
@Service
public class UserService {

    private final WebClient webClient;

    public Mono<User> getUserById(Integer userId) {
        return webClient
                .get()
                .uri("/users/{id}", userId)
                .retrieve()
                .bodyToMono(User.class);
    }
}

 

보는 바와 같이 UserDto 가 있고, webClient를 사용하여 외부API call을 그대로 리턴하는 간단한 서비스다. 실제로는 db와 관련된 비즈니스 로직뿐만 아니라 여러 복잡한 과정을 거치겠지만, 비관리의존성 영역을 MockWebServer을 사용하여어떻게 활용하는지를 보기 위해 단순화 했다는 것을 감안해서 보자.

위의 작성된 service를 바탕으로 테스트를 진행해 보자.

 

// beforeAll -> beforeEach -> afterEach -> afterAll 순서로 동작한다.
class UserServiceTest {

    public static MockWebServer mockWebServer;
    private UserService userService;
    private final ObjectMapper objectMapper = new ObjectMapper();

    @BeforeAll
    static void setUp() throws IOException {
        //1. mockWebServer 생성 후 시작
        mockWebServer = new MockWebServer();
        mockWebServer.start();
    }

    @BeforeEach
    void initialize() {
        //2. WebClient 생성과 MockWebServer URL 설정을 해준다.
        String baseUrl = String.format("http://localhost:%s", mockWebServer.getPort());
        WebClient webClient = WebClient.create(baseUrl);

        //3. 서비스에 클라이언트 주입
        userService = new UserService(webClient);
    }

    @AfterAll
    static void tearDown() throws IOException {
        //4. 테스트가 종료되면 MockWebServer 도 종료시킨다.
        mockWebServer.shutdown();
    }

    @DisplayName("유저 id를 통해 유저 정보를 조회할 수 있다.")
    @Test
    void getUserById() throws Exception {
        // given
        User mockUser = User.builder()
                .id(1)
                .name("Man")
                .build();
        mockWebServer.enqueue(new MockResponse()
                .setBody(objectMapper.writeValueAsString(mockUser))
                .addHeader("Content-Type", "application/json"));

        // when
        Mono<User> findUser = userService.getUserById(1);

        // then
        StepVerifier.create(findUser)
                .expectNextMatches(user -> user.getName().equals("Man"))
                .verifyComplete();

        RecordedRequest recordedRequest = mockWebServer.takeRequest();
        assertAll(
                () -> assertEquals("GET", recordedRequest.getMethod()),
                () -> assertEquals("/users/1", recordedRequest.getPath())
        );
    }



}

 

위의 테스트를 돌려보면 아래와 같이 성공하는 것을 볼 수 있다.

 

 

이와 같이 Service Layer 에 대한 테스트를 진행할 때, 만약 다른 외부 API 를 호출하는 경우 MockWebServer 를 활용하여 stubbing 해서 테스트를 진행할 수 있다. 이를 활용하여 앞으로는 비관리의존성이라도 두려워하지 말고 함께 테스트에 포함해서 테스트를 진행해보도록 하자.

 

 

 

참조

반응형

'테스트코드 > 작성방법' 카테고리의 다른 글

6. Presentation Layer Test  (1) 2024.03.26
5. Business Layer Test  (1) 2024.03.26
3. Persistence Layer Test  (0) 2024.03.26
2. Layered Architecture  (0) 2024.03.26
1. Basic Unit Test  (0) 2024.03.26