Notice
Recent Posts
Recent Comments
Link
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

임도현의 성장

[Spring-Boot] JUnit & Mockito기반테스트 코드 작성 본문

Spring Boot

[Spring-Boot] JUnit & Mockito기반테스트 코드 작성

림도현 2025. 3. 22. 13:23

📌테스트 코드 왜 작성해야 해?

축구 게임을 만들었다고 생각해 보자, 우리 공격수가 상대방 골대에 골을 넣었는데 우리팀 점수가 올라가야 하는데 상대팀 점수가 올라가는 버그가 생겼어 이 문제를 수정하기 위해 매번 직접 실행해서 확인 하면 시간도 많이 걸리고, 실수도할 수 있잖아 이때 테스트 코드는 한번의 실행으로 우리팀 점수가 올랐는지 확인 할 수 있고, 리팩토링이나 코드를 수정했을 때 동작이 잘 됬는지 확인도 할 수 있어

😧테스트 코드의 특징

  1. 버그 예방 및 조기 발견: 개발 중 작성한 코드가 예상대로 작동하는지 즉시 확인할 수 있으며, 버그를 조기에 발견하고 수정할 수 있습니다.
  2. 리팩토링의 안전성 확보: 코드 리팩토링을 할 때 기존 기능의 동작이 변하지 않았는지 확인할 수 있어, 코드 변경이 다른 부분에 미치는 영향을 최소화할 수 있습니다.
  3. 문서화 역할: 단위 테스트는 코드의 사용법을 명확히 설명하는 역할을 합니다. 테스트 메서드는 메서드를 어떻게 사용해야 하는지, 어떤 입력이 필요하며 어떤 결과가 나와야 하는지를 명확히 보여줍니다.
  4. 자동화된 테스트: 단위 테스트는 일반적으로 자동으로 실행되며, CI/CD 파이프라인에 통합되어 코드 변경 시마다 자동으로 테스트가 실행됩니다. 이를 통해 변경된 코드가 예상대로 동작하는지 확인할 수 있습니다.

🌐JUnit란?

JUnit는 Java에서 가장 널리 사용되는 테스트 프레임워크로, 단위 테스트를 작성하고 실행하는 데 사용됩니다. JUnit은 테스트 메서드 작성, 실행, 결과 보고 등의 기능을 제공합니다. 특히 테스트 코드의 구조를 잘 정의할 수 있게 도와주고, 테스트 결과를 쉽게 확인할 수 있도록 합니다.

🚀단위 테스트란?

단위 테스트(Unit Test)는 소프트웨어 개발에서 개별적인 함수나 메서드 등의 "단위"가 올바르게 동작하는지 확인하기 위해 작성하는 테스트 코드입니다. 주로 프로그램의 각 기능을 독립적으로 검증하여, 각 부분이 제대로 작동하는지 확인하는 데 사용됩니다. 단위 테스트는 보통 개발자가 코드를 작성하면서 병행하여 작성합니다.

🍸Mockito란?

Mockito는 실제 객체를 사용하기 어려운 상황에서 "가짜 객체"(mock 객체)를 생성하여, 서비스나 레포지토리와 같은 의존성을 테스트 대상 객체에 주입하는 역할을 합니다. 이렇게 하면 실제 객체 없이도 컨트롤러, 서비스, 레포지토리 등 여러 계층의 동작을 독립적으로 테스트할 수 있습니다.

💽단위 테스트 작성 기준

  • 단위 테스트는 JUnit과 assertThat 조합을 사용합니다.
  • Given/When/Then 패턴
    • Given : 어떠한 데이터가 주어질 때.
    • When : 어떠한 기능을 실행하면.
    • Then : 어떠한 결과를 기대한다.
@DisplayName("")
@Test
void test() {
    // given

    // when

    // then
}

🙊테스트 코드 작성을 위한 준비

  • CreateMember : 회원 정보를 받아 새 회원을 생성하고, 생성된 회원 정보를 반환합니다.
  • SelectMember : 전달 받은 id 값을 이용해 특정 회원 정보를 조회하고 반환합니다.
@RequiredArgsConstructor
@RestController
public class MemberController {

    private final MemberService memberService;

    @PostMapping("/member")
    public ApiResponse<MemberResponse> CreateMember(@Valid @RequestBody CreateMember request){
        LocalDateTime dateTime = LocalDateTime.now();
        return ApiResponse.ok(memberService.CreateMember(request, dateTime));
    }

    @GetMapping("/member/{id}")
    public ApiResponse<MemberResponse> SelectMember(@PathVariable Long id){
        return ApiResponse.ok(memberService.SelectMember(id));
    }
}

@RequiredArgsConstructor
@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberResponse CreateMember(CreateMember request, LocalDateTime dateTime) {
        Member entity = request.toEntity(request, dateTime);
        Member res = memberRepository.save(entity);
        return MemberResponse.of(res);
    }

    public MemberResponse SelectMember(Long id) {
        Optional<Member> member = memberRepository.findById(id);
        return MemberResponse.of(member);
    }
}

📚Conroller 계층 단위 테스트

  1. @WebMvcTest(controllers = MemberController.class)
    • @WebMvcTest는 Spring MVC 관련 컴포넌트만 로드하고, 컨트롤러 계층만 테스트하는 어노테이션입니다.
    • Service, Repository 계층은 실행되지 않습니다. 그래서 data가 null이 될 수 있습니다.
  2. @Autowired
    • 의존성 주입(DI)을 통해 객체를 자동으로 주입받기 위해 사용합니다.
  3. MockMvc
    • MockMvc는 Spring MVC 테스트를 위한 객체로, 실제 HTTP 요청을 보내지 않고도 웹 레이어의 동작을 테스트할 수 있습니다.
    • MockMvc를 사용하면 실제 서버를 실행하지 않고도 HTTP 요청을 모의로(mock), 컨트롤러가 제대로 동작하는지 확인할 수 있습니다. 이 객체를 통해 GET, POST, PUT, DELETE 등의 요청을 보내고 그 응답을 검증할 수 있습니다.
  4. @MockitoBean
    • @MockitoBean은 이제 가짜 객체(mock)를 Spring 컨텍스트에 주입하여, 테스트에서 실제 객체 대신 모킹된 객체를 사용할 수 있게 해주는 어노테이션입니다.
    • 실제 MemberService 객체 대신 가짜 MemberService 사용되어 컨트롤러에서 의존성 주입을 받게되어 "컨트롤러의 동작만 테스트하고, MemberService의 실제 비즈니스 로직을 수행하지 않습니다."
  5. ObjectMapper
    • ObjectMapper는 객체와 JSON 간의 변환을 처리하는 Jackson 라이브러리의 주요 클래스입니다.
    • ObjectMapper는 Java 객체를 JSON으로 직렬화하거나, JSON을 Java 객체로 역직렬화할 때 사용됩니다.
    • 예를 들어, CreateMember 객체를 JSON 형식으로 변환해 HTTP 요청에 포함시키거나, 응답을 ObjectMapper를 통해 Java 객체로 변환하여 검사할 수 있습니다.
@WebMvcTest(controllers = MemberController.class)
class MemberControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockitoBean
    private MemberService memberService;    
}

📜회원 등록 성공시 200 OK 검증 테스트

  • given 준비 단계에서 CreateMember객체를 생성
  • mockMvc.perform() : MockMvc를 사용하여 HTTP 요청을 실행
  • post("/member") : POST 방식으로 /member 엔드포인트를 호출
  • objectMapper.writeValueAsString(request) : CreateMember 객체를 JSON 문자열로 변환, 실제 요청 시 JSON 형태로 전달해야 하기 때문
  • content() : 변환된 JSON 데이터를 HTTP 요청의 Body에 담음

실제 응답 값을 보면 data에 회원 정보가 잘 들어오지만, 현재 테스트 코드에서는 Mockito를 사용하여 MemberService를 가짜(Mock) 객체로 대체했기 때문에, 실제 DB에 회원 정보가 등록되지 않습니다. 따라서 data 값이 null로 나오는 것입니다.

@DisplayName("신규 회원을 등록한다.")
@Test
void CreateMember() throws Exception {
    // given
    CreateMember request = CreateMember.builder()
            .userName("이름")
            .address("주소")
            .phone("010-1111-1111")
            .build();

    // when // then
    mockMvc.perform(post("/member")
                    .content(objectMapper.writeValueAsString(request))
                    .contentType(MediaType.APPLICATION_JSON) // 요청 타입을 application/json으로 설정
            )
            .andDo(print()) // 용청과 응답 내용을 콘솔에 출력 (디버깅용)
            .andExpect(status().isOk()) // 응답 상태 코드가 200 OK인지 검증
    ;
}

// 실제 응답 값
{
    "status": "OK",
    "message": "OK",
    "data": {
        "id": 1,
        "userName": "이름",
        "address": "주소",
        "phone": "010111111",
        "createdAt": "2025-03-21T22:02:52.9993217"
    },
    "code": 200
}

💩회원 등록시 Vaild 검증 테스트

  • jsonPath()는 JSON 응답에서 특정 필드를 찾아 값을 검증하는 역할
  • $ → 최상위 JSON 객체를 의미
  • $.code → "code" 필드 값을 가져 옴
  • jsonPath("$.code").value("400") : "code" 필드 값 과 value값 비교
  • jsonPath("$.data").isEmpty() 응답 JSON의 data 필드가 비어 있는지 검증

이 처럼 테스트 코드는 응답 값이 어떻게 나오는지 알고 있어야 하고, 잘못된 요청에 대한 응답도 예상 결과 값을 미리 알고 있어야 테스트 코드를 작성 할 수 있습니다.

@DisplayName("신규 회원을 등록할 때 회원 이름은 필수입니다.")
@Test
void CreateMemberWithNotUserName() throws Exception {
    // given
    CreateMember request = CreateMember.builder()
            .userName("")
            .address("주소")
            .phone("010-1111-1111")
            .build();

    // when // then
    mockMvc.perform(post("/member")
                    .content(objectMapper.writeValueAsString(request))
                    .contentType(MediaType.APPLICATION_JSON)
            )
            .andDo(print())
            .andExpect(status().isBadRequest()) // HTTP 상태 코드가 400인지 확인
            .andExpect(jsonPath("$.code").value("400")) 
            .andExpect(jsonPath("$.status").value("BAD_REQUEST"))
            .andExpect(jsonPath("$.message").value("회원 이름은 필수 입니다."))
            .andExpect(jsonPath("$.data").isEmpty());
}

// 실제 응답 값
{
    "status": "BAD_REQUEST",
    "message": "회원 이름은 필수 입니다.",
    "data": null,
    "code": 400
}

🤓PathVariable 을 통한 테스트 작성

앤드포인에 요청을 할 때 PathVariable이나 QueryParam같은 경우는 아래 코드처럼 작성하면 됩니다.

@DisplayName("회원을 조회한다.")
@Test
void SelectMember() throws Exception {
    // given
    Long memberId = 1L;

    // when // then
    mockMvc.perform(
                    get("/member/{id}", memberId)
                //  .queryParam("name", "이름") Parm은 이렇게 작성
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value("200"))
            .andExpect(jsonPath("$.status").value("OK"))
            .andExpect(jsonPath("$.message").value("OK"))
    ;
}

🌟Service 계층 단위 테스트

  • @SpringBootTest : Spring 컨텍스트를 로드하여 실제 Bean들이 주입됩니다. Mock이랑 다르게 실제로 동작함
  • deleteAllInBatch() : 각 테스트가 끝날 때마다 deleteAllInBatch()를 호출하여 테스트 데이터 삭제, 즉 DB에 데이터가 저장되었다가 테스트가 끝나면 롤백 대신 삭제 됨
  • extracting() : 객체에서 특정 조회 필드만 선택해서 추출
  • containsExactly() : 추출한 결과 값 들이 기대한 값과 순서까지 일치하는지 비교
@ActiveProfiles("test")
@SpringBootTest
class MemberServiceTest {

    @Autowired
    private MemberService memberService;
    
    @Autowired
    private MemberRepository memberRepository;

    @AfterEach
    void tearDown() {
        memberRepository.deleteAllInBatch();
    }

    @DisplayName("회원 생성 성공")
    @Test
    void CreateMember() {
        // given
        LocalDateTime now = LocalDateTime.now();
        CreateMember request = new CreateMember("이름", "주소", "010-1111-1111");

        // when
        MemberResponse response = memberService.CreateMember(request, now);
        
        // then
        assertThat(response)
                .extracting("userName", "address", "phone", "createdAt")
                .containsExactly("이름", "주소", "010-1111-1111", now);
    }
}

💀서비스 계층 테스트에서 예외 터트리기

  • assertThatThrownBy() : 코드 실행 시 예외가 발생하는지 검증
  • isInstanceOf() : 발생한 예외가 IllegalArgumentException 인지 검즘
  • hasMessage() : 예외 메시지가 기대한 값과 일치하는지 검

assertThat()는 기대한 값과 일치하는지 검증이고 assertThatThrownBy()는 예외가 일치하는지 검증하는 메서드이다.

@DisplayName("없는 회원Id로 조회한다..")
@Test
void SelectMemberWhitNotId() {
    // given
    Long id = 100L;

    // when // then
    assertThatThrownBy(() -> memberService.SelectMember(id))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessage("없는 회원입니다.");
}