[Spring] 레포, 서비스, 컨트롤러 단 단위테스트 작성하기

이원찬·2024년 1월 20일
0

Spring

목록 보기
8/13

참고자료
[Spring] JUnit과 Mockito 기반의 Spring 단위 테스트 코드 작성법 (3/3)

[Spring] JUnit & Mockito 기반 Spring 단위 테스트 코드 작성

단위 테스트란?

  • 단위테스트는 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트
  • 하나의 모듈이란 각 계층에서의 하나의 기능 또는 메소드로 이해할 수 있다.
  • 하나의 기능이 올바르게 동작하는지를 독립적으로 테스트하는 것

따라서 테스트코드에선 다른 모듈과 의존성과 별개로 독립적이어야 한다!!

이때 사용되는 것이 Mockito 프레임워크이다.

Mockito 는 개발자가 제어 가능한 가짜 객체의 의존성을 주입하게 도와준다.**

테스팅 초기 코드

  1. Member

    @Entity
    @Builder
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    public class Member {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(nullable = false, length = 20)
        private String username;
    }
  2. MemberController

    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/members")
    public class MemberController {
        private final MemberService memberService;
    
        @PostMapping
        public MemberResponseDTO.CreateResultDTO createMember(
                @RequestBody MemberRequestDTO.CreateDTO request
        ) {
            Member newMember = memberService.createMember(request);
            return MemberResponseDTO.CreateResultDTO.toCreateResultDTO(newMember);
        }
    
        @GetMapping("/{memberId}")
        public MemberResponseDTO.PreviewDTO getMember(
                @PathVariable Long memberId
        ) {
            Member findMember = memberService.getMember(memberId);
            return MemberResponseDTO.PreviewDTO.toPreviewDTO(findMember);
        }
    }
  3. MemberService

    @Service
    @RequiredArgsConstructor
    public class MemberService {
        private final MemberRepository memberRepository;
    
        public Member createMember(MemberRequestDTO.CreateDTO request) {
            return memberRepository.save(request.toMember());
        }
    
        public Member getMember(Long memberId) {
            return memberRepository.findById(memberId).get();
        }
    }
  4. MemberRepository

    @Repository
    public interface MemberRepository extends JpaRepository<Member, Long> {
    
    }
  5. build.gradle

    dependencies {
      implementation 'org.springframework.boot:spring-boot-starter'
      testImplementation 'org.springframework.boot:spring-boot-starter-test'
    
      // h2
      implementation 'com.h2database:h2'
    
      // jpa
      implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    
      //mysql
      implementation 'mysql:mysql-connector-java:8.0.33'
    
      // web
      implementation 'org.springframework.boot:spring-boot-starter-web'
    
      // lombok
      implementation 'org.projectlombok:lombok:1.18.22'
      annotationProcessor 'org.projectlombok:lombok:1.18.22'
    
      //swaggerDoc
      implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
    
          // Gson
      implementation 'com.google.code.gson:gson:2.8.9'
    }

레포지토리

레포지토리는 따로 의존성이 없고 직접 DB와 연결이 있어야 하기에

인메모리 H2 데이터 베이스를 이용한다.

여기서 사용되는 @DataJpaTest 어노테이션이 인메모리 h2데이터 베이스를 이용케 한다. 또한 테스트가 끝나면 자동으로 롤백해주어 테스트를 용이 하게 한다.

MemberRepositoryTest

@DataJpaTest
class MemberRepositoryTest {
    private final MemberRepository memberRepository;

    @Autowired
    public MemberRepositoryTest(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Test
    public void saveMemberTest() {
        // given
        String username = "test";
        // when
        memberRepository.save(Member.builder()
                .username(username)
                .build());
        // then
        List<Member> allMember = memberRepository.findAll();

        assertEquals(1, allMember.size());
        assertEquals(username, allMember.stream().findAny().get().getUsername());
    }

}

서비스

서비스 계층은 HTTP 통신과는 관계 없이 로직 검증만 하면 된다.

위 서비스 코드는 MemberService 코드는 MemberRepository 코드를 의존 하고 있다.

단위 테스트를 하기 위해서 Mockito를 이용하여 다른 계층과 의존관계를 단절 시켜 주어야한다.

서비스가 의존중인 MemberRepository를 Mock객체를 이용해 주입 시켜 주자

// Mockito를 사용하기 위한 어노테이션
@ExtendWith(MockitoExtension.class)
class MemberServiceTest {

    @Mock // 가짜 객체를 만들어줌
    private MemberRepository memberRepository;

    @InjectMocks // memberRepository를 memberService에 주입
    private MemberService memberService;

		...
}

위처럼 가짜 객체를 주입 시켜 주었다면 가짜 객체가 하는 일을 코드로 정의 하여야 한다.

어떤 결과를 반환하라고 정해진 답변을 준비시켜야 한다

이때 사용되는 방법중 두가지

  • when.thenReturn(리턴할 객체)
  • doReturn.when(메서드)

필자는 when.thenReturn 문법을 사용하였다.

CreateMember 서비스 테스팅

// Mockito를 사용하기 위한 어노테이션
@ExtendWith(MockitoExtension.class)
class MemberServiceTest {

    @Mock // 가짜 객체를 만들어줌
    private MemberRepository memberRepository;

    @InjectMocks // memberRepository를 memberService에 주입
    private MemberService memberService;

    @Test
    void createMember() {
        Long memberId = 1L;
        String username = "test";

        MemberRequestDTO.CreateDTO request = MemberRequestDTO.CreateDTO.builder()
                .username(username)
                .build();

        Member testMember = Member.builder()
                .id(memberId)
                .username(username)
                .build();

        // memberRepository.save(Member)가 호출되면 newMember를 리턴하라는 의미
        when(memberRepository.save(any(Member.class))).thenReturn(testMember);

        // 또는 
        // doReturn(testMember).when(memberRepository).save(any(Member.class));
        
        // memberService.createMember 호출
        Member newMember = memberService.createMember(request);

        assertNotNull(newMember);
        assertEquals(memberId, newMember.getId());
        assertEquals(username, newMember.getUsername());
    }
}

GetMember 서비스 테스팅

@Test
void getMember() {
    Long memberId = 1L;
    String username = "test";
    Member expectedMember = Member.builder()
            .id(memberId)
            .username(username)
            .build();

    // memberRepository.findById(memberId)가 호출되면 expectedMember를 리턴하라는 의미
    when(memberRepository.findById(memberId)).thenReturn(Optional.of(expectedMember));

    // memberService.getMember 호출
    Member findMember = memberService.getMember(memberId);

    // 검증
    assertNotNull(findMember);
    assertEquals(expectedMember.getId(), findMember.getId());
    assertEquals(expectedMember.getUsername(), findMember.getUsername());
}

컨트롤러

컨트롤러의 단위 테스트를 하기 위해서 Mockito를 이용하여 다른 계층과 의존관계를 단절 시켜 주어야한다

컨트롤러를 테스트 하기 위해서는 HTTP 호출이 필요

스프링 부트는 컨트롤러 테스트를 위한 @WebMvcTest 어노테이션을 제공

@WebMvcTest란?

WebMvcTest 어노테이션은 단위 테스트를 위한 Spring MVC 테스트에 사용된다.

인자로 Bean 객체를 받아서 (보통 Controller 객체)

전체 Spring Bean 구성중 테스트에 관련된 구성들만 적용한다! (테스트를 빠르게 해줄수 있는 이유!)

  • MockMvc 객체를 자동으로 생성 해준다!!
  • ControllerAdviceFilter, Interceptor 등 웹 계층 테스트에 필요한 요소들을 모두 빈으로 등록해 웹 테스트 환경을 자동으로 만들어준다.

나머지 종속성은 Mock 객체 (가짜 객체)를 통하여 제공해야 한다!!

💡 WebMvcTest는 SpringBoot가 제공하는 것이니
Mock → MockBean 을 사용한다.

가짜객체만 사용하는게 아닌 가짜 Bean 객체를 사용해야 하므로!
// 아래는 맨 밑 자료를 다시 한번 보고 사용할지 말지 결정하자
**@MockBean(JpaMetamodelMappingContext.class)**
// WebMvcTest는 @Controller, @RestController가 붙은 클래스를 테스트하기 위한 어노테이션
@WebMvcTest(MemberController.class)
class MemberControllerTest {

  @Autowired
  public MemberControllerTest(MockMvc mockMvc) {
      this.mockMvc = mockMvc;
  }

  private MockMvc mockMvc;

  @MockBean
  private MemberService memberService;
	
	// Gson은 JSON 형식의 문자열을 자바 객체로 변환해주는 라이브러리
	@Autowired
  private Gson gson;

	... 
}

@DisplayName("회원 생성 테스트")

@Test
@DisplayName("회원 생성 테스트")
void createMember() throws Exception {
    MemberRequestDTO.CreateDTO request = MemberRequestDTO.CreateDTO.builder()
            .username("이원찬").build();

    Member responseUser = Member.builder()
            .id(1L)
            .username(request.getUsername())
            .build();

    when(memberService.createMember(any(MemberRequestDTO.CreateDTO.class)))
            .thenReturn(responseUser);

    // post "/members" 요청을 보내서 회원을 생성한다.
    mockMvc.perform(MockMvcRequestBuilders.post("/members")
            .content(gson.toJson(request))
            .contentType("application/json"))
            .andExpect(status().isOk())
            .andExpect(content().json(gson.toJson(responseUser)));
}

@DisplayName("회원 조회 테스트")

void getMember() throws Exception {

    String username = "이원찬";
    Member responseUser = Member.builder()
            .id(1L)
            .username(username)
            .build();

    when(memberService.getMember(1L))
            .thenReturn(responseUser);

    // get "/members/1" 요청을 보내서 회원 정보를 가져온다.
    mockMvc.perform(MockMvcRequestBuilders.get("/members/" + responseUser.getId()))
            .andExpect(status().isOk())
            .andExpect(content().json(gson.toJson(responseUser)));
}

@MockBean(JpaMetamodelMappingContext.class) 는 안써도 되나...?

[Spring] Junit @WebMvcTest 의 JPA metamodel must not be empty! 에러!

profile
소통과 기록이 무기(Weapon)인 개발자

0개의 댓글