[MSA] Spring Feign Client Test Code

방승재·2023년 4월 7일
19

msa

목록 보기
1/1
post-thumbnail

처음으로 MSA로 토이 프로젝트를 진행하며 만난 문제에 대한 정리입니다.

 

spring cloud feign client란?

선언적 웹 서비스 클라이언트입니다.
HttpMessageConvertersSpring Cloud는 Spring MVC 주석에 대한 지원과 Spring Web에서 기본적으로 사용되는 동일한 사용에 대한 지원을 추가합니다 . Spring Cloud는 Eureka, Spring Cloud CircuitBreaker 및 Spring Cloud LoadBalancer를 통합하여 Feign을 사용할 때 부하 분산된 http 클라이언트를 제공합니다.

 

 

현재 상황은 북마크를 담당하는 서버가 북마크를 하기전에 Auth서버에게 해당 유저가 있는지 요청을 합니다.

 

📌 Feign Client 코드는 이러합니다.

@FeignClient(
        name = "${api.authentication-server.name}",
        url = "${api.authentication-server.url}",
        configuration = {FeignRetryConfiguration.class}
)
public interface MemberFeignClient {
    @RequestMapping(
            method = RequestMethod.GET,
            value = "/api/member/exist/{memberId}",
            consumes = MediaType.APPLICATION_JSON_VALUE
    )
    Boolean memberExistsByMemberId(@PathVariable("memberId") Long memberId);
}

member id로 해당 유저가 있는지 물어봅니다.

 

📌 feign client가 fallback을 사용할 수 있게 만들어 주었습니다.

@Service
public class MemberFeignClientService {

    private final MemberFeignClient memberFeignClient;

    public MemberFeignClientService(MemberFeignClient memberFeignClient) {
        this.memberFeignClient = memberFeignClient;
    }

    @CircuitBreaker(name = "existMember", fallbackMethod = "throwMemberFeignEx")
    public Boolean existByMemberId(Long memberId) {
        return memberFeignClient.memberExistsByMemberId(memberId);
    }

    private Boolean throwMemberEx(Throwable t) {
        tthrow new IllegalArgumentException("페인 클라이언트 에러!");
    }
}

 

북마크는 한명의 유저가 뉴스를 찜하는 기능입니다.
📌 북마크를 저장하는 코드입니다.

@Service
public class BookmarkCommandService implements BookmarkCommand {

    private final NewsJpaRepository newsJpaRepository;
    private final MemberFeignClientService memberFeignClientService;
    private final BookmarkJpaRepository bookmarkJpaRepository;

    public BookmarkCommandService(
            NewsJpaRepository newsJpaRepository,
            MemberFeignClientService memberFeignClientService,
            BookmarkJpaRepository bookmarkJpaRepository
    ) {
        this.newsJpaRepository = newsJpaRepository;
        this.memberFeignClientService = memberFeignClientService;
        this.bookmarkJpaRepository = bookmarkJpaRepository;
    }

    @Override
    public Bookmark saveBookmark(Long newsId, Long memberId) {
        if (!memberFeignClientService.existByMemberId(memberId)) {
            throw new IllegalArgumentException("해당 id의 멤버가 존재하지 않습니다.");
        }

        if (!newsJpaRepository.existsById(newsId)) {
            throw new IllegalArgumentException("해당 id의 뉴스가 존재하지 않습니다.");
        }

        Bookmark bookmark = new Bookmark(
                newsId,
                memberId
        );
        Bookmark savedBookmark = bookmarkJpaRepository.save(bookmark);
        return savedBookmark;
    }
}

 


이제는 테스트 코드를 작성해 보겠습니다.

제가 처음에 사용한 방법은 가짜 구현체를 빈으로 등록시켜 주는 것이였습니다.

 

 

📌 가짜 구현체

public class MemberFeignClientImpl implements MemberFeignClient {
    @Override
    public Boolean memberExistsByMemberId(Long memberId) {

        if (memberId == null) {
            return Boolean.FALSE;
        }

        if (memberId > 0) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }
}

 

📌 테스트 코드

    @Test
    @DisplayName("북마크를 저장하면 Bookmark객체의 id값이 생긴다.")
    public void given_news_id_and_member_id_when_then() {
        News save = newsJpaRepository.save(
                new News(
                        "title",
                        "des",
                        1L,
                        "com",
                        "com",
                        Instant.now(),
                        Instant.now()
                )
        );
        Bookmark bookmark = bookmarkCommand.saveBookmark(1L, save.getNewsId());

        assertThat(bookmark.getBookmarkId()).isGreaterThan(0L);
    }

 

음... 해결된거 같으면서도 테스트의 이치에는 맞지 않는것 같다.

🐳 두 번째로 사용한 방법은 WireMock을 사용하여 stub서버를 만들어 주는 것이였습니다.

 
gradle 추가

testImplementation 'com.github.tomakehurst:wiremock-jre8:2.30.1'

 

📌 WireMock을 활용한 서비스 통합 테스트 코드입니다.

@ActiveProfiles({"test"})
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@DisplayName("북마크 서비스레이어 통합 테스트")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class BookmarkServiceMockTest extends AbstractContainerTestBase {

    @Autowired
    private BookmarkCommandService bookmarkCommandService;

    static final int PORT = 8080;
    public static WireMockServer wireMockServer = new WireMockServer(options().port(PORT));

    @Autowired
    private PersistenceHelper persistenceHelper;

    @DynamicPropertySource
    public static void addUrlProperties(DynamicPropertyRegistry registry) {
        registry.add("api.authentication-server.url", () -> "localhost:" + PORT);
    }

    @BeforeAll
    public static void beforeAll() {
        wireMockServer.start();
        WireMock.configureFor("localhost", PORT);
    }

    @AfterAll
    public static void afterAll() {
        wireMockServer.stop();
    }

    @AfterEach
    public void afterEach() {
        wireMockServer.resetAll();
    }

    @Test
    @DisplayName("북마크 저장을 하면 북마크가 생긴다.")
    public void given_memberid_newsid_when_save_bookmark_then_is_not_null() {
        Long memberId = 1L;
        wireMockServer.stubFor(get(urlMatching("/api/member/exist/"+memberId))
                .willReturn(aResponse()
                        .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .withBody("true"))
        );

        News persist = persistenceHelper.persist(NewsFixture.createNews());
        Bookmark bookmark = bookmarkCommandService.saveBookmark(1L, memberId);

        assertThat(bookmark).isNotNull();
    }

    @Test
    @DisplayName("Auth서버랑 연결되지 않으면 에러를 반환한다.")
    public void when_bookmark_save_then_throw_error() {
        assertThatThrownBy(() -> bookmarkCommandService.saveBookmark(1L, 1L))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("페인 클라이언트 에러!");
    }
}
  • @BeforeAll로 Wiremock은 해당 테스트 클래스가 실행될 때 서버를 가동합니다.
  • @DynamicPropertySource를 통해 페인 클라이언트에서 사용할 url환경변수를 지정해 줍니다.
  • @AfterEach로 테스트 중에 사용한 stub은 테스트 마다 리셋 됩니다.
  • @AfterAll을 통해 모든 테스트가 끝나면 WireMockServer도 종료됩니다.

 


📌 다음으로는 API통합테스트 입니다.

@ActiveProfiles({"test"})
@DisplayName("북마크 API 통합 테스트")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class BookmarkControllerIntegrationTest extends AbstractContainerTestBase {

    private MockMvc mockMvc;

    @Autowired
    private BookmarkCommandAPI bookmarkCommandAPI;

    static final int PORT = 8080;
    public static WireMockServer wireMockServer = new WireMockServer(options().port(PORT));

    @DynamicPropertySource
    public static void addUrlProperties(DynamicPropertyRegistry registry) {
        registry.add("api.authentication-server.url", () -> "localhost:" + PORT);
    }

    @BeforeAll
    public static void beforeAll() {
        wireMockServer.start();
        WireMock.configureFor("localhost", PORT);
    }

    @AfterAll
    public static void afterAll() {
        wireMockServer.stop();
    }

    @AfterEach
    public void afterEach() {
        wireMockServer.resetAll();
    }

    @BeforeEach
    public void setup() {
        this.mockMvc = MockMvcBuilders
                .standaloneSetup(bookmarkCommandAPI)
                .build();
    }

    @Autowired
    private PersistenceHelper persistenceHelper;

    @Test
    @DisplayName("북마크를 저장하면 Status코드가 201이다.")
    public void given_news_id_when_save_then_status_created() throws Exception {
        wireMockServer.stubFor(get(urlMatching("/api/member/exist/1"))
                .willReturn(aResponse()
                        .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .withBody("true"))
        );

        News news = persistenceHelper.persist(NewsFixture.createNews());
        Long newsId = news.getNewsId();

        ResultActions response = mockMvc.perform(MockMvcRequestBuilders.post("/api/bookmark/save/1/"+newsId)
                .contentType(MediaType.APPLICATION_JSON)
        );

        response.andExpect(MockMvcResultMatchers.status().isCreated());
    }
}

 
서비스 통합테스트와 마찬가지로 WireMock을 사용하였습니다.

 

마치며

MSA를 구축하는건 모놀리식 보다 공수가 몇배는 드는것 같습니다...
물론 그럼에도 불구하고 팀 사이즈, 어플리케이션의 사이즈에 따라 MSA가 독이될 수도 약이 될 수도 있을것 같습니다.
그리고 제가 잘못 작성한게 있다면 지적해주시면 정말 감사하겠습니다!

 



Reference

https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/
https://www.baeldung.com/introduction-to-wiremock
https://kafcamus.tistory.com/55

profile
현재 대학교에 재학중입니다.

2개의 댓글

comment-user-thumbnail
2023년 4월 7일

너무 감동적이네요 ㅠㅠ

답글 달기
comment-user-thumbnail
2023년 4월 10일

오.. 이런 내용이 있었군요. 감사합니다!

답글 달기