728x90
반응형
SMALL

Spring 프로젝트에서 테스트를 작성할 때 단위 테스트를 위한 Mockito를 사용해보자.

Mockito를 사용하면 좋은 점은 다음과 같다.

  • 빠르고 독립적이다.
  • 외부 서비스나 데이터베이스와의 상호작용을 실제로 하고 싶지 않을 때 유용하다.

왜 빠르냐, 스프링이 띄워질 때 처리되는 여러 자동화된 작업들이 필요없으므로 @SpringBootTest 애노테이션을 사용한 스프링을 띄운 테스트보다 훨씬 빠르다. 

 

그래서 일단 간단하게 테스트 코드를 만들기 위한 필요 코드들을 살펴보자.

 

DMakerService

@Slf4j
@Service
@RequiredArgsConstructor
public class DMakerService {

    /**
     * The repository for accessing and manipulating {@link Developer} data.
     */
    private final DeveloperRepository developerRepository;
    private final RetiredDeveloperRepository retiredDeveloperRepository;

    /**
     * Creates a new developer based on the provided {@link CreateDeveloperRequestDTO} payload.
     * It first validates the payload and then saves the developer to the database.
     *
     * @param createDeveloperRequestDTO The payload containing the developer's information.
     * @throws DMakerException If the payload validation fails or if a developer with the same memberId already exists.
     */
    @Transactional
    public CreateDeveloperResponseDTO createDeveloper(CreateDeveloperRequestDTO createDeveloperRequestDTO) {
        validationCreatePayload(createDeveloperRequestDTO);

        Developer developer = Developer.builder()
                .developerLevel(createDeveloperRequestDTO.getDeveloperLevel())
                .developerType(createDeveloperRequestDTO.getDeveloperType())
                .name(createDeveloperRequestDTO.getName())
                .age(createDeveloperRequestDTO.getAge())
                .experienceYears(createDeveloperRequestDTO.getExperienceYears())
                .memberId(createDeveloperRequestDTO.getMemberId())
                .statusCode(EMPLOYED)
                .build();

        developerRepository.save(developer);

        return CreateDeveloperResponseDTO.fromEntity(developer);
    }

    public List<DeveloperDTO> getAllEmployedDevelopers() {
        return developerRepository.findDevelopersByStatusCodeEquals(EMPLOYED).stream()
                .map(DeveloperDTO::fromEntity)
                .collect(Collectors.toList());
    }

    public DeveloperDetailDTO getDeveloperByMemberId(String memberId) {
        return developerRepository.findByMemberId(memberId)
                .map(DeveloperDetailDTO::fromEntity)
                .orElseThrow(() -> new DMakerException(NO_DEVELOPER));
    }

    public DeveloperDetailDTO updateDeveloperByMemberId(String memberId, UpdateDeveloperRequestDTO updateDeveloperRequestDTO) {
        Developer findDeveloper = developerRepository.findByMemberId(memberId)
                .orElseThrow(() -> new DMakerException(NO_DEVELOPER));

        validationUpdatePayload(updateDeveloperRequestDTO, findDeveloper);
        findDeveloper.changeDeveloperData(updateDeveloperRequestDTO);
        return DeveloperDetailDTO.fromEntity(findDeveloper);
    }

    @Transactional
    public DeveloperDetailDTO deleteDeveloperByMemberId(String memberId) {
        Developer findDeveloper = developerRepository.findByMemberId(memberId)
                .orElseThrow(() -> new DMakerException(NO_DEVELOPER));

        findDeveloper.changeStatusCode(RETIRED);

        RetiredDeveloper retiredDeveloper = RetiredDeveloper.builder()
                .memberId(findDeveloper.getMemberId())
                .age(findDeveloper.getAge())
                .name(findDeveloper.getName())
                .build();

        retiredDeveloperRepository.save(retiredDeveloper);
        return DeveloperDetailDTO.fromEntity(findDeveloper);
    }

    private void validationUpdatePayload(UpdateDeveloperRequestDTO updateDeveloperRequestDTO, Developer updateDeveloper) {
        DeveloperLevel developerLevel = updateDeveloperRequestDTO.getDeveloperLevel() != null ? updateDeveloperRequestDTO.getDeveloperLevel() : updateDeveloper.getDeveloperLevel();
        Integer experienceYears = updateDeveloperRequestDTO.getExperienceYears() != null ? updateDeveloperRequestDTO.getExperienceYears() : updateDeveloper.getExperienceYears();

        compareLevelWithExperienceYears(developerLevel, experienceYears);
    }

    /**
     * Validates the provided {@link CreateDeveloperRequestDTO} payload.
     * It checks if the developer's experience years match the developer level.
     * It also checks if a developer with the same memberId already exists.
     *
     * @param createDeveloperRequestDTO The payload containing the developer's information.
     * @throws DMakerException If the payload validation fails.
     */
    private void validationCreatePayload(CreateDeveloperRequestDTO createDeveloperRequestDTO) {
        DeveloperLevel developerLevel = createDeveloperRequestDTO.getDeveloperLevel();

        compareLevelWithExperienceYears(developerLevel, createDeveloperRequestDTO.getExperienceYears());

        Optional<Developer> findDeveloper = developerRepository.findByMemberId(createDeveloperRequestDTO.getMemberId());
        if (findDeveloper.isPresent()) {
            throw new DMakerException(DMakerErrorCode.DUPLICATE_MEMBER_ID);
        }
    }

    private static void compareLevelWithExperienceYears(DeveloperLevel developerLevel, Integer developerExperienceYears) {
        if (developerLevel.equals(SENIOR) && developerExperienceYears < 10) {
            throw new DMakerException(LEVEL_EXPERIENCE_YEARS_NOT_MATCHED);
        }
        if (developerLevel.equals(MIDDLE) &&
                (developerExperienceYears < 4 || developerExperienceYears > 10)) {
            throw new DMakerException(LEVEL_EXPERIENCE_YEARS_NOT_MATCHED);
        }
        if (developerLevel.equals(JUNIOR) && developerExperienceYears > 4) {
            throw new DMakerException(LEVEL_EXPERIENCE_YEARS_NOT_MATCHED);
        }
    }
}
  • 서비스 클래스 하나가 있다. 이 서비스 클래스는 두가지 의존성을 주입받는다.
    • DeveloperRepository
    • RetiredDeveloperRepository

DeveloperRepository

@Repository
public interface DeveloperRepository extends JpaRepository<Developer, Long> {

    Optional<Developer> findByMemberId(String memberId);

    List<Developer> findDevelopersByStatusCodeEquals(StatusCode statusCode);
}

 

RetiredDeveloperRepository

@Repository
public interface RetiredDeveloperRepository extends JpaRepository<RetiredDeveloper, Long> {
}

 

이런 관계를 가지고 있을 때, 테스트 코드를 작성해보자.

MockitoDMakerServiceTest

package cwchoiit.dmaker.service;

import cwchoiit.dmaker.dto.DeveloperDetailDTO;
import cwchoiit.dmaker.entity.Developer;
import cwchoiit.dmaker.repository.DeveloperRepository;
import cwchoiit.dmaker.repository.RetiredDeveloperRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static cwchoiit.dmaker.type.DeveloperLevel.SENIOR;
import static cwchoiit.dmaker.type.DeveloperType.BACK_END;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;

@ExtendWith(MockitoExtension.class)
class MockitoDMakerServiceTest {

    @Mock
    private DeveloperRepository developerRepository;
    @Mock
    private RetiredDeveloperRepository retiredDeveloperRepository;

    @InjectMocks
    private DMakerService dMakerService;

    @BeforeEach
    void setUp() {
        given(developerRepository.findByMemberId(anyString()))
                .willReturn(Optional.of(Developer.builder()
                        .developerLevel(SENIOR)
                        .developerType(BACK_END)
                        .experienceYears(15)
                        .name("Samira")
                        .age(30)
                        .memberId("samira")
                        .build()));
    }

    @Test
    void getAllEmployedDevelopers() {

        DeveloperDetailDTO samira = dMakerService.getDeveloperByMemberId("samira");

        assertThat(samira.getDeveloperLevel()).isEqualTo(SENIOR);
        assertThat(samira.getDeveloperType()).isEqualTo(BACK_END);
        assertThat(samira.getName()).isEqualTo("Samira");
        assertThat(samira.getAge()).isEqualTo(30);
        assertThat(samira.getMemberId()).isEqualTo("samira");
    }
}
  • 보통이라면 해당 서비스는 빈으로 등록된 서비스이기 때문에 주입을 받아야 하지만 Mockito를 사용해서 가짜로 주입받는다.
  • Mockito를 사용하려면 먼저 다음 애노테이션이 필요하다.
@ExtendWith(MockitoExtension.class)
class MockitoDMakerServiceTest {...}
  • @ExtendWith(MockitoExtension.class) 애노테이션을 클래스 레벨에 붙여준다.

그리고 아까 위에서 봤지만, DMakerService는 두가지 의존성을 주입 받는다. 그래서 이 DMakerServiceMock으로 주입받기 위해선다음 작업이 필요하다.

@Mock
private DeveloperRepository developerRepository;
@Mock
private RetiredDeveloperRepository retiredDeveloperRepository;

@InjectMocks
private DMakerService dMakerService;
  • 주입받을 의존성을 @Mock 애노테이션이 달린 상태로 선언한다. 
  • 실제로 이 테스트 클래스에 주입 받을 DMakerServiceMock으로 주입하는데 이때 사용하는 애노테이션은 @InjectMocks이다.
  • 그런데 만약, 저 두개의 의존성 중 이 테스트에서는 사용하지 않는 의존관계가 있다면 제거해도 무방하다. 다음 코드처럼.
@Mock
private DeveloperRepository developerRepository;

@InjectMocks
private DMakerService dMakerService;

 

 

이렇게 하면 가짜로 주입을 받을 수 있다. 이제 테스트를 위한 데이터가 필요하다. 어떻게 데이터를 가짜로 만들 수 있냐면 다음 코드를 보자.

@BeforeEach
void setUp() {
    given(developerRepository.findByMemberId(anyString()))
            .willReturn(Optional.of(Developer.builder()
                    .developerLevel(SENIOR)
                    .developerType(BACK_END)
                    .experienceYears(15)
                    .name("Samira")
                    .age(30)
                    .memberId("samira")
                    .build()));
}
  • given() 메서드는 `org.mockito.BDDMockito.given`에서 제공하는 메서드이고, 사전 조건을 정의할 수 있다. 이때 developerRepository가 가지고 있는 findByMemberId() 메서드가 어떤 값을 받을 때 반환하는 가짜 데이터를 임의로 생성할 수 있다. 저기서는 anyString() 이라는 `org.mockito.ArgumentMatchers.anyString`에서 제공하는 메서드를 사용해서 어떤 문자열이 들어와도 동일한 반환을 할 수 있게 정의했다.
  • findByMemberId는 반환값이 Optional이다. 그렇게 때문에 willReturn(Optional.of(...))을 사용한다.
  • 이제 적절한 데이터를 만들어서 이 메서드가 호출되면 어떤 문자열이 들어와도 항상 코드에서 정의한 객체가 반환되도록 하였다.

이제 실제 테스트는 아래와 같이 작성하면 된다.

@Test
void getAllEmployedDevelopers() {

    DeveloperDetailDTO samira = dMakerService.getDeveloperByMemberId("samira");

    assertThat(samira.getDeveloperLevel()).isEqualTo(SENIOR);
    assertThat(samira.getDeveloperType()).isEqualTo(BACK_END);
    assertThat(samira.getName()).isEqualTo("Samira");
    assertThat(samira.getAge()).isEqualTo(30);
    assertThat(samira.getMemberId()).isEqualTo("samira");
}
  • dMakerService.getDeveloperByMemberId() 메서드는 내부적으로 findByMemberId를 사용해서 데이터를 가져온다. 그렇게 되면 위 setUp() 메서드에서 정의한 가짜 데이터가 튀어나오게 될 것이다.

실행 결과

 

생성 관련 서비스 테스트

이번에는 좀 더 복잡한, 생성 관련 테스트를 진행해보자.

@Test
void createDeveloper_pass() {
    CreateDeveloperRequestDTO xayah = CreateDeveloperRequestDTO.builder()
            .developerLevel(SENIOR)
            .developerType(BACK_END)
            .experienceYears(15)
            .name("Xayah")
            .age(33)
            .memberId("xayah")
            .build();

    // DMakerService.createDeveloper()를 호출할 때 검증 단계에서 실행되는 findByMemberId()
    given(developerRepository.findByMemberId(anyString())).willReturn(Optional.empty());

    // DMakerService.createDeveloper()를 호출할 때 DeveloperRepository.save()가 실행되는데, 그때 save()가 받는 Developer 객체를 Captor에 capture
    ArgumentCaptor<Developer> captor = ArgumentCaptor.forClass(Developer.class);

    // 서비스 호출
    dMakerService.createDeveloper(xayah);

    // DeveloperRepository.save()가 1번 호출되었는지 확인, save()가 받은 Developer 객체를 Captor에 저장
    verify(developerRepository, times(1)).save(captor.capture());

    // Captor에 저장된 값을 Retrieved
    Developer newDeveloper = captor.getValue();

    // 검증
    assertThat(newDeveloper.getDeveloperLevel()).isEqualTo(SENIOR);
    assertThat(newDeveloper.getDeveloperType()).isEqualTo(BACK_END);
    assertThat(newDeveloper.getExperienceYears()).isEqualTo(15);
    assertThat(newDeveloper.getName()).isEqualTo("Xayah");
    assertThat(newDeveloper.getAge()).isEqualTo(33);
    assertThat(newDeveloper.getMemberId()).isEqualTo("xayah");
}
  • 먼저 생성할 CreateDeveloperRequestDTO로 저장할 객체를 만든다.
  • DMakerServicecreateDeveloper() 메서드는 다음과 같이 검증 로직이 존재한다.
private void validationCreatePayload(CreateDeveloperRequestDTO createDeveloperRequestDTO) {
    DeveloperLevel developerLevel = createDeveloperRequestDTO.getDeveloperLevel();

    compareLevelWithExperienceYears(developerLevel, createDeveloperRequestDTO.getExperienceYears());

    Optional<Developer> findDeveloper = developerRepository.findByMemberId(createDeveloperRequestDTO.getMemberId());
    if (findDeveloper.isPresent()) {
        throw new DMakerException(DMakerErrorCode.DUPLICATE_MEMBER_ID);
    }
}
  • 이 검증 로직에는 DeveloperRepositoryfindByMemberId()를 호출해서 생성하려는 DevelopermemberId가 이미 있는지 체크하는 부분이 있다. 그렇다는 것은 findByMemberId()에 대한 Mocking이 필요하다는 얘기이다.
  • 그렇기 때문에 위 테스트 코드에서 다음 코드가 존재한다.
given(developerRepository.findByMemberId(anyString())).willReturn(Optional.empty());
  • findByMemberId()가 호출되면, Optional.empty()를 반환하게 Mocking하여, 검증에 통과하도록 한다.

여기서 끝나면 안된다. 왜냐하면 생성 관련 테스트는 실제로 전달한 생성 데이터가 제대로 생성이 됐는지 확인을 해줘야 하기 때문이다. 그리고 제대로 생성된 데이터를 확인하기 위해 그 데이터를 보관할 객체를 Mockito는 제공한다.

// DMakerService.createDeveloper()를 호출할 때 DeveloperRepository.save()가 실행되는데, 그때 save()가 받는 Developer 객체를 Captor에 capture
ArgumentCaptor<Developer> captor = ArgumentCaptor.forClass(Developer.class);
  • 이렇게 ArgumentCaptor<T>로 생성 객체를 저장할 수 있다. 우리가 확인하길 원하는 생성 객체의 타입은 Developer이므로, ArgumentCaptor<Developer> 타입으로 지정한다.
// 서비스 호출
dMakerService.createDeveloper(xayah);
  • 서비스를 호출한다.
// DeveloperRepository.save()가 1번 호출되었는지 확인, save()가 받은 Developer 객체를 Captor에 저장
verify(developerRepository, times(1)).save(captor.capture());
  • 호출한 서비스에서 DeveloperRepositorysave() 메서드가 1번 실행됐음을 확인한다.
  • 그리고 해당 save()에 던져진 파라미터를 위에서 선언한 Captor에 저장한다.
// Captor에 저장된 값을 Retrieved
Developer newDeveloper = captor.getValue();
  • Captor에 저장한 객체를 꺼내온다.
// 검증
assertThat(newDeveloper.getDeveloperLevel()).isEqualTo(SENIOR);
assertThat(newDeveloper.getDeveloperType()).isEqualTo(BACK_END);
assertThat(newDeveloper.getExperienceYears()).isEqualTo(15);
assertThat(newDeveloper.getName()).isEqualTo("Xayah");
assertThat(newDeveloper.getAge()).isEqualTo(33);
assertThat(newDeveloper.getMemberId()).isEqualTo("xayah");
  • 해당 객체는 우리가 생성 테스트를 위해 만든 객체와 동일한 값들을 가지는지 확인한다.

생성 관련 테스트 (실패 케이스)

이번엔 생성 테스트 중 실패 케이스를 보자.

@Test
void createDeveloper_fail() {
    // DMakerService.createDeveloper()를 호출할 때 검증 단계에서 실행되는 findByMemberId()
    given(developerRepository.findByMemberId(anyString()))
            .willReturn(Optional.of(Developer.builder()
                    .developerLevel(SENIOR)
                    .developerType(BACK_END)
                    .experienceYears(15)
                    .name("Xayah")
                    .age(33)
                    .memberId("xayah")
                    .build()));

    CreateDeveloperRequestDTO xayah = CreateDeveloperRequestDTO.builder()
            .developerLevel(SENIOR)
            .developerType(BACK_END)
            .experienceYears(15)
            .name("Xayah")
            .age(33)
            .memberId("xayah")
            .build();

    // 서비스 호출 시 중복 memberId로 Developer 생성 시, DMakerException 발생
    assertThatThrownBy(() -> dMakerService.createDeveloper(xayah)).isInstanceOf(DMakerException.class);
}
  • 한 라인씩 알아보자.
// DMakerService.createDeveloper()를 호출할 때 검증 단계에서 실행되는 findByMemberId()
given(developerRepository.findByMemberId(anyString()))
        .willReturn(Optional.of(Developer.builder()
                .developerLevel(SENIOR)
                .developerType(BACK_END)
                .experienceYears(15)
                .name("Xayah")
                .age(33)
                .memberId("xayah")
                .build()));
  • 이번엔 DeveloperRepository.findByMemberId()Mocking을 우리가 생성하고자 하는 데이터와 동일하게 해서 반환하도록 설정한다.
CreateDeveloperRequestDTO xayah = CreateDeveloperRequestDTO.builder()
                .developerLevel(SENIOR)
                .developerType(BACK_END)
                .experienceYears(15)
                .name("Xayah")
                .age(33)
                .memberId("xayah")
                .build();

// 서비스 호출 시 중복 memberId로 Developer 생성 시, DMakerException 발생
assertThatThrownBy(() -> dMakerService.createDeveloper(xayah)).isInstanceOf(DMakerException.class);
  • 그리고 생성 테스트를 위해 서비스의 createDeveloper()를 호출하면, DMakerException 예외를 터트릴 것을 예상한다.
  • 왜냐하면, 검증 단계에서 같은 memberId가 있는 경우 다음과 같이 예외를 터트린다.
Optional<Developer> findDeveloper = developerRepository.findByMemberId(createDeveloperRequestDTO.getMemberId());
if (findDeveloper.isPresent()) {
    throw new DMakerException(DMakerErrorCode.DUPLICATE_MEMBER_ID);
}

컨트롤러 테스트

이번엔 아래와 같은 컨트롤러에 대한 테스트도 진행해보자.

DMakerController

package cwchoiit.dmaker.controller;

import cwchoiit.dmaker.dto.*;
import cwchoiit.dmaker.service.DMakerService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * This RESTful controller provides endpoints for retrieving developer names.
 */
@Slf4j
@RestController
@RequiredArgsConstructor
public class DMakerController {

    private final DMakerService dMakerService;

    /**
     * Retrieves a list of developer names.
     *
     * @return A list of developer names.
     */
    @GetMapping("/developers")
    public List<DeveloperDTO> getDevelopers() {
        log.info("GET /developers");
        return dMakerService.getAllEmployedDevelopers();
    }

    @GetMapping("/developers/{memberId}")
    public DeveloperDetailDTO getDeveloperByMemberId(@PathVariable String memberId) {
        log.info("GET /developers/{}", memberId);
        return dMakerService.getDeveloperByMemberId(memberId);
    }


    /**
     * Creates a new developer.
     *
     * @param createDeveloperRequestDTO The request object containing the details of the developer to be created.
     * @return A message indicating that the developer was created successfully.
     */
    @PostMapping("/developers")
    public CreateDeveloperResponseDTO createDeveloper(@Validated @RequestBody CreateDeveloperRequestDTO createDeveloperRequestDTO) {
        log.info("POST /developers");

        return dMakerService.createDeveloper(createDeveloperRequestDTO);
    }

    @PutMapping("/developers/{memberId}")
    public DeveloperDetailDTO updateDeveloperByMemberId(@PathVariable String memberId,
                                                        @Validated @RequestBody UpdateDeveloperRequestDTO updateDeveloperRequestDTO) {
        log.info("PUT /developers/{}", memberId);
        return dMakerService.updateDeveloperByMemberId(memberId, updateDeveloperRequestDTO);
    }

    @DeleteMapping("/developers/{memberId}")
    public DeveloperDetailDTO deleteDeveloperByMemberId(@PathVariable String memberId) {
        log.info("DELETE /developers/{}", memberId);
        return dMakerService.deleteDeveloperByMemberId(memberId);
    }
}

 

테스트 코드를 작성하자.

DMakerControllerTest

package cwchoiit.dmaker.controller;

import cwchoiit.dmaker.dto.DeveloperDTO;
import cwchoiit.dmaker.service.DMakerService;
import cwchoiit.dmaker.type.DeveloperLevel;
import cwchoiit.dmaker.type.DeveloperType;
import org.junit.jupiter.api.BeforeEach;
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.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import java.nio.charset.StandardCharsets;
import java.util.List;

import static org.mockito.BDDMockito.given;
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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(DMakerController.class)
class DMakerControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private DMakerService dMakerService;

    private final MediaType contentType =
            new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), StandardCharsets.UTF_8);

    @BeforeEach
    void setUp() {
        DeveloperDTO samira = DeveloperDTO.builder()
                .developerLevel(DeveloperLevel.SENIOR)
                .developerType(DeveloperType.BACK_END)
                .memberId("samira")
                .build();

        DeveloperDTO jin = DeveloperDTO.builder()
                .developerLevel(DeveloperLevel.MIDDLE)
                .developerType(DeveloperType.BACK_END)
                .memberId("Jin")
                .build();

        given(dMakerService.getAllEmployedDevelopers()).willReturn(List.of(samira, jin));
    }

    @Test
    void getAllDevelopers() throws Exception {
        mockMvc.perform(get("/developers").contentType(contentType))
                .andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.[0].memberId").value("samira"))
                .andExpect(jsonPath("$.[0].developerLevel").value("SENIOR"))
                .andExpect(jsonPath("$.[1].memberId").value("Jin"))
                .andExpect(jsonPath("$.[1].developerLevel").value("MIDDLE"));
    }
}
  • 컨트롤러 테스트만을 할건데, 모든 스프링 빈을 등록할 필요는 없을 것이다. 그래서 딱 원하는 빈만 스프링 컨테이너에 등록해서 테스트를 할 수 있게 해주는 기능이 있는데 바로 다음 애노테이션이다.
@WebMvcTest(DMakerController.class)
class DMakerControllerTest {...}
  • 이렇게 @WebMvcTest(DMakerController.class) 라는 애노테이션을 등록하면, 이 테스트를 실행할 때 저 컨트롤러만 빈으로 등록되어 사용할 수 있다.
@Autowired
private MockMvc mockMvc;

@MockBean
private DMakerService dMakerService;
  • HTTP REST 요청을 하기 위해 필요한 MockMvc
  • DMakerController가 주입 받아야 하는 DMakerServiceMock으로 빈 등록을 한다. 
  • 위에서 서비스 관련 테스트를 할때는 필요한 주입을 @Mock 애노테이션으로 사용했는데 여기서는 @MockBean으로 하는 이유는 이 테스트를 실행해보면 알겠지만 스프링이 띄워진다. @WebMvcTest는 스프링을 띄우긴 띄우는데 선언한 빈들만 등록하게 해서 띄우는 간단한 스프링이라고 생각하면 좋다. 그래서 스프링 Bean으로 등록될 Mock을 선언하는 @MockBean 애노테이션을 선언한다.
@BeforeEach
void setUp() {
    DeveloperDTO samira = DeveloperDTO.builder()
            .developerLevel(DeveloperLevel.SENIOR)
            .developerType(DeveloperType.BACK_END)
            .memberId("samira")
            .build();

    DeveloperDTO jin = DeveloperDTO.builder()
            .developerLevel(DeveloperLevel.MIDDLE)
            .developerType(DeveloperType.BACK_END)
            .memberId("Jin")
            .build();

    given(dMakerService.getAllEmployedDevelopers()).willReturn(List.of(samira, jin));
}
  • 테스트를 실행하기 앞서, 테스트 데이터가 필요하므로 테스트 데이터를 생성한다. 그래서 DMakerServicegetAllEmployedDevelopers()가 호출되면 반환되는 리스트를 가짜로 만든다.
@Test
void getAllDevelopers() throws Exception {
    mockMvc.perform(get("/developers").contentType(contentType))
            .andExpect(status().isOk())
            .andDo(print())
            .andExpect(jsonPath("$.[0].memberId").value("samira"))
            .andExpect(jsonPath("$.[0].developerLevel").value("SENIOR"))
            .andExpect(jsonPath("$.[1].memberId").value("Jin"))
            .andExpect(jsonPath("$.[1].developerLevel").value("MIDDLE"));
}
  • 실제 테스트 코드는 다음과 같다. 주입 받은 MockMvc를 사용해서 HTTP 요청을 날린다. 이때 Content-Type은 JSON으로 지정하기 위해 필드로 선언한 `contentType`을 사용한다. 그리고 그 응답 결과를 우리가 만든 가짜 데이터가 나올 것으로 예상한 테스트 코드를 작성한다.
  • andDo(print())는 이 요청에 대한 요청-응답 정보를 출력하는 메서드이다.

실행 결과

정리를 하자면

이렇게 간단하게 데이터베이스가 필요한 서비스지만, 데이터베이스를 사용하고 싶지 않을 때 Mockito를 사용해서 가짜로 데이터를 만들어 내는 방법을 배워봤다. 간단한 단위 테스트를 작성할 때 Mockito를 사용하면 코드 자체적인 문제를 잘 찾아낼 수 있을 것 같다.

728x90
반응형
LIST

'Spring Advanced' 카테고리의 다른 글

TestContainer를 이용한 Spring Boot 테스트  (2) 2024.10.03
Redis를 사용해서 캐싱하기  (0) 2024.10.02
AOP (Part. 3) - 포인트컷  (0) 2024.01.02
AOP (Part.2)  (0) 2024.01.02
AOP(Aspect Oriented Programming)  (0) 2023.12.29

+ Recent posts