본문 바로가기
Back-End/Spring MVC

[Spring Boot] JUnit을 이용하여 테스트 코드 작성하기 (feat. Mockito)

by 달의 조각 2022. 9. 7.

  • 단위 테스트(Unit Test): 비즈니스 로직에서 사용하는 각각의 메서드를 독립적으로 테스트한다.
  • 슬라이스 테스트: 애플리케이션을 특정 계층으로 쪼개어 하는 테스트로, Mock 객체를 사용한다.
  • 통합 테스트: 클라이언트 툴 없이 개발자가 짜 놓은 테스트 코드를 실행시켜서 기대 결과를 확인한다. Controller의 API를 호출하면 서비스 계층과 데이터 액세스 계층을 거쳐 기대 결과를 확인한다.
  • 기능 테스트: 사용자 입장에서 애플리케이션의 기능이 올바르게 동작하는지 테스트한다. 일반적으로 테스트 전문 부서에서 진행하며, API 툴이나 데이터베이스까지 연관되어 있다.

 

🌳 F.I.R.S.T 원칙

  • Fast: 테스트 케이스는 빨라야 한다.
    ※ 테스트 케이스: 테스트를 위한 입력 데이터, 실행 조건, 기대 결과를 표현하기 위한 명세 = 테스트 코드
  • Independent: 각 테스트 케이스는 독립적이어야 한다. 서로 다른 테스트에 영향을 주지 않아야 한다.
  • Repeatable: 어떤 환경에서도 반복해서 실행이 가능해야 한다.
  • Self-validating: 성공 또는 실패라는 자체 검증 결과를 보여 줘야 한다.
  • Timely: 테스트하려는 기능 구현을 하기 직전에 작성해야 한다.

 


JUnit

 

Java 언어로 만들어진 애플리케이션을 테스트 하기 위한 오픈 소스 테스트 프레임워크이다. 요즘 단위 테스트는 주로 Given - When - Then 패턴의 세 단계로 작성된다.

  • @Test: JUnit은 테스트 패키지 하위에 해당 애너테이션이 붙은 메서드를 테스트 메서드임을 인식한다.
@DisplayName("회원 정보 조회 테스트")
@Test
void getMemberTest() {

    Given
    // 테스트에 필요한 전제 조건
    // 테스트 대상에 전달되는 입력 값

    When
    // 테스트 할 동작 지정

    Then
    // 테스트 결과 검증
    // 예상 값과 수행 결과를 비교하여 기대한 동작을 수행하는지 검증(Assertion)
}

 

🌳 Assertion 메서드

assertEquals(A, B)
assertNotNull(A, "실패 시 표시 메시지")
assertThrows(NullPointerException.class, () -> A("Hello")) // 람다: Executable 함수형 인터페이스

 

🌳 테스트 실행 전후처리

@BeforeEach // 각 테스트 직전에 실행: 초기화 작업
@BeforeAll // 클래스 레벨에서 한꺼번에 실행할 때, 실행 이전 한 번 초기화 작업 가능: 정적 메서드에 붙일 수 있다

@AfterEach
@AfterAll

@BeforeAll

라이프 사이클을 클래스 단위로 하는 @TestInstance(TestInstance.Lifecycle.PER_CLASS)를 붙이면
static이 아니어도 사용할 수 있다.
https://junit.org/junit5/docs/5.0.1/api/org/junit/jupiter/api/TestInstance

 

🌳 Assumption: 조건부 테스트

특정 환경에서만 실행되도록 할 수 있다.

// 시스템 운영체제가 Windows라면 아래 나머지 로직이 실행된다
assumeTrue(System.getProperty("os.name").startsWith("Windows"));

 


 

Hamcrest

 

JUnit 기반의 단위 테스트에서 사용할 수 있는 Assertion Framework

  • Assertion을 위한 매쳐(Matcher)가 자연스러운 문장으로 이어져서 가독성이 좋다.
  • 테스트 실패 메시지의 가독성이 좋다.
  • 다양한 Matcher를 제공한다.
  • http://hamcrest.org/JavaHamcrest/tutorial
// JUnit
assertEquals(expected, actual);

// Hemcrest
assertThat(actual, is(equalTo(expected)));

 


 

슬라이스 테스트


한 계층 영역에 대한 테스트에 집중하는 것이다. 이때, Mock 객체가 없으면 다른 계층을 넘나들어야 하기 때문에 통합 테스트에 가까워진다.

@SpringBootTest // 스프링 부트 기반 애플리케이션을 테스트 하기 위한 Application Context 생성
@AutoConfigureMockMvc // Controller 테스트를 위한 애플리케이션 자동 구성 작업
public class ControllerTestDefaultStructure {
    
    @Autowired
    private MockMvc mockMvc; // 서버 실행 없이 Controller 테스트 환경 지원
    
    @Autowired
    private Gson gson;
    
    @Test
    public void postMemberTest() {
        // given: 테스트용 request body 생성
        
        // when: MockMvc 객체로 테스트 대상 Controller 호출
        
        // then: Controller 핸들러 메서드에서 응답으로 수신한 HTTP Status 및 response body 검증
    }
}

 

🌳 API 계층 테스트

  • given: 테스트용 Request Body 생성
  • when: MockMvc 객체로 테스트 대상 Controller 호출
  • then: Controller 핸들러 메서드의 응답인 HTTP Status와 Request Body 검증
    @Test
    void postMemberTest() throws Exception {
        // given
        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com", "홍길동", "010-1234-5678");
        String content = gson.toJson(post); //JSON 변환 라이브러리

        // when
        ResultActions actions =
                mockMvc.perform( // Controller의 핸들러 메서드에 요청 전송
                        post("/v11/members")
                                .accept(MediaType.APPLICATION_JSON) // 클라이언트가 리턴받을 응답 타입
                                .contentType(MediaType.APPLICATION_JSON) // 서버에서 처리 가능한 타입
                                .content(content) //Request Body 데이터
                );

        // then
        MvcResult result = actions // perform()의 반환 타입으로 Request 검증 가능
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.data.name").value(post.getName()))
                .andReturn(); // Response 데이터 확인 가능, 디버깅 용도로 응답 데이터 출력 시 사용

//        System.out.println(result.getResponse().getContentAsString());
    }

 

🌳 데이터 액세스 계층 테스트

테스트 후에는 데이터베이스 상태를 실행 이전으로 되돌려서 깨끗하게 만드는 것이 좋다.

@DataJpaTest: @Transactional 애너테이션을 포함하여 한 테스트 케이스 실행이 종료되는 시점에 데이터베이스에 저장된 데이터가 Rollback 처리된다.

@DataJpaTest
public class MemberRepositoryTest {

    @Autowired
    private MemberRepository memberRepository;

    @DisplayName("save()가 잘 동작하는지 확인")
    @Test
    public void saveMemberTest() {
        // given
        Member member = new Member();
        member.setEmail("hgd@gmail.com");
        member.setName("홍길동");
        member.setPhone("010-1111-2222");

        // when
        Member savedMember = memberRepository.save(member);

        // then
        assertNotNull(savedMember);
        assertTrue(member.getEmail().equals(savedMember.getEmail()));
        assertTrue(member.getName().equals(savedMember.getName()));
        assertTrue(member.getPhone().equals(savedMember.getPhone()));
    }

    @DisplayName("findByEmail()이 잘 동작하는지 확인")
    @Test
    public void findByEmailTest() {
        
        ...
    }
}

 


 

Mockito

 

Spring Framework 자체적으로 지원하고 있는 Mocking 라이브러리이다. 테스트하고 싶은 대상을 다른 영역으로부터 단절시켜서 테스트 대상 계층에만 집중할 수 있도록 한다.

🌳 컨트롤러 핸들러 메서드

  • @MockBean : Application Context에 등록된 Bean에 대한 Mock 객체를 생성하고 주입한다.
  • Stubbing : 테스트를 위해 Mock 객체가 항상 일정한 동작을 하도록 지정한다.
  • Mockito.any( ... ) : 파라미터에 createMember()의 파라미터 타입을 넣는다.
  • willReturn( ... ) : createMember()가 리턴 할 Stub 데이터이다.
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private Gson gson;

    @MockBean // Application Context의 Bean에 대한 Mock 객체 생성, 주입
    private MemberService memberService;

    @Autowired // MockMemberService(가칭)의 메서드에서 리턴하는 Member를 리턴하기 위해 사용한다
    private MemberMapper mapper;
    @Test
    void postMemberTest() throws Exception {
        // given : 테스트용 Request Body 생성
        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com", "홍길동", "010-1234-5678");

        Member member = mapper.memberPostToMember(post);
        member.setStamp(new Stamp());

        // Mockito의 Stubbing 메서드
        given(memberService.createMember(Mockito.any(Member.class)))
                .willReturn(member);

        String content = gson.toJson(post);
더보기
// MemberController.java
// 진짜 로직에서는 service를 거친다!
Member createdMember = memberService.crateMember(member);

// MemberControllerMockTest.java
// 테스트에서는 위 코드가 아닌 아래 코드가 실행되어 createMember에 담긴다!
// stubbing
given(memberService.createMember(Mockito.any(Member.class)))
	.willReturn(member);
    // when : MvcMock 객체로 테스트 대상 Controller 호출
    ResultActions actions =
            mockMvc.perform(
                    post("/v11/members")
                            .accept(MediaType.APPLICATION_JSON)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(content)
            );
        // then
        MvcResult result = actions
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.data.email").value(post.getEmail()))
                .andExpect(jsonPath("$.data.name").value(post.getName()))
                .andExpect(jsonPath("$.data.phone").value(post.getPhone()))
                .andReturn();

//        System.out.println(result.getResponse().getContentAsString());
    }

 

🌳 서비스 계층 메서드

  • @Mock : 해당 필드 객체를 Mock 객체로 생성한다.
  • @InjectMocks : 생성한 Mock 객체를 해당 필드에 주입해 준다.
@ExtendWith(MockitoExtension.class) // Spring을 사용하지 않고 Mockito 기능 사용
public class MemberServiceMockTest {
    
    @Mock
    private MemberRepository memberRepository;
    
    @InjectMocks 
    private MemberService memberService;
@Test
public void createMemberTest() {
    // given
    Member member = new Member("hgd@gmail.com", "홍길동", "010-1111-1111");

    given(memberRepository.findByEmail(member.getEmail()))
            .willReturn(Optional.of(member));

    // when
    // then
    assertThrows(BusinessLogicException.class, () -> memberService.createMember(member));
}

댓글