(15)

테스트코드 실습하기 (단위, 통합 테스트) - TestCode(14)

서론 인강은 끝이 났고, 인강 내용을 바탕으로 첫 테스트 코드를 적용한 토이 프로젝트 내 코드들을 조금 리뷰해보려고 한다. 프로젝트를 간단히 소개하자면, 그냥 롤 챔피언 정보를 JSON으로 받아와서 무언가를 하는 서비스를 구현 할 예정이다. 우선 돌아가게는 작성했는데 테스트 코드를 처음 직접 활용하다보니 부족한 부분이 많을 수 있다. 혹여 지나가다 보시는 고수분들께서 틀린 점이 있다면 지적좀 해주시면 감사할 따름.. 단위 테스트 Champion의 Repository에 대한 테스트를 작성했다. package com.example.lolchampionsinvestment.domain.champion; import org.junit.jupiter.api.DisplayName; import org.junit.j..

private method test를 해야하는가? / TestCode(13)

서론 본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다. Practical Testing: 실용적인 테스트 가이드 - 인프런 | 강의 이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강 www.inflearn.com private method를 테스트해야 하는가 강의에서는, private 메서드를 테스트할 필요가 없으며, private 메서드를 테스트 고려해야 할 때를 객체를 분리해야하는 시점인가? 라는 질문을 던져보라고 하였다. 아래의 코드는, 다음 상품번호를 생성해주는 ProductService에서 발췌한 private 메서..

더 좋은 테스트 코드 작성하기 4 - 테스트 환경 통합 / TestCode(12)

서론 본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다. Practical Testing: 실용적인 테스트 가이드 - 인프런 | 강의 이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강 www.inflearn.com 테스트 환경 통합하기 (참고) IntelliJ에서 전체 테스트 수행 시 Gradle - Tasks - verification - test 강의 내 작성했던 코드를 가지고 전체 테스트를 돌려보았다. 작성한 단위테스트와 통합테스트 모두 돌아갔으며 로그가 남았다. 로그를 확인해보니 Spring Boot가 6번 등장했다. ..

더 좋은 테스트 코드 작성하기 3 - Dynamic Test / TestCode(11)

서론 본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다. Practical Testing: 실용적인 테스트 가이드 - 인프런 | 강의 이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강 www.inflearn.com Dynamic Test 환경을 설정해놓고, 중간중간 변화를 주며 검증을하는 등의 시나리오를 테스트하고 싶을 때 사용한다. @TestFactory Stream exampleDynamicTest() { return Stream.of( dynamicTest("First Dynamic Test", () -> { // tes..

더 좋은 테스트 코드 작성하기 2 - Test Fixture 구성하기 / TestCode(10)

서론 본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다. Practical Testing: 실용적인 테스트 가이드 - 인프런 | 강의 이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강 www.inflearn.com Test Fixture 구성하기 중복 발생이 되는 상황들을 한 곳에 묶어 관리하는 개념으로 JUnit에서는, 동일하거나 유사한 개체 집합에 대해 작동하는 두 개 이상의 테스트가 있는 경우 Test Fixture 메서드의 사용을 권장한다. 굳이 메서드를 사용하지 않더라도 픽스처를 구성할 때, 원하는 파라미터만 명확히 ..

더 좋은 테스트 코드 작성하기 1 - TestCode(9)

서론 본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다. Practical Testing: 실용적인 테스트 가이드 - 인프런 | 강의 이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강 www.inflearn.com 하나의 주제에 대한 테스트 수행 한 가지의 테스트에서는 한 가지 목적의 검증만을 수행하여야 한다. DisplayName을 한 문장으로 치환할 수 있는지에 대한 고민을 해보는 것이 좋다. 예를 들어, 조건문, 반복문이 들어간 테스트는 포괄적인 테스트가 될 수 있다. @DisplayName("동물이 동물원에 있는지 확인..

Mock / Test Double - TestCode (8)

서론 본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다. Practical Testing: 실용적인 테스트 가이드 - 인프런 | 강의 이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강 www.inflearn.com Mock 사전적인 뜻은 모조품, 가짜 라는 뜻을 가지고 있으며, 테스트 진행 시 진행하고자 하는 실제 객체와 동일한 가짜 객체(Mock)를 만들어서 사용한다. 언제 사용해야 할까? 테스트 작성 환경 구축이 어려울때 특정 경우에 의존적일때 테스트 시간이 오래걸릴 때 Mock과 Stub 자주 사용되는 Mock과 Stub..

@AfterEach, @BeforeEach - TestCode(7)

서론 본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다. Practical Testing: 실용적인 테스트 가이드 - 인프런 | 강의 이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강 www.inflearn.com 인강을 쭉 따라가다보니 어느새 Business Layer Test를 진행중이다. 해당 테스트에는, 주문을 생성하는 두 개의 테스트 메서드가 있고. 각각 red, green, refactor 과정을 거쳐 테스트를 수행했다. 하지만 위 사진처럼, 두 메서드를 통합하여 테스트 할 경우 테스트 에러가 발생하게 되었고 강의에..

Layered Architecture란?

Architecture - 아키텍쳐 아키텍쳐란 목표하는 대상에 대하여 그 구성과 동작원리, 구성 요소 간의 관계 및 시스템 외부 환경과의 관계 등을 설명하는 설계도 혹은 청사진을 말한다. 시스템에서의 아키텍쳐는 시스템의 구조, 행위, 더 많은 뷰를 정의하는 개념적 모형으로 시스템 목적을 달성하기 위해 시스템의 각 컴포넌트가 무엇이며 어떻게 상호작용하는지, 정보가 어떻게 교환되는지를 설명한다. Layerd Architecture 소프트웨어 시스템의 구성요소 간의 관심사 분리(관심사 별로 계층화)한 아키텍처로, 특정 계층 내의 구성 요소는 해당 계층과 관련된 논리만 처리한다. 관심사 분리를 통해 계층의 응집도를 높이고 결합도를 낮출 수 있으며 이를 통한 재사용성과 유지보수의 향상을 기대할 수 있다. 서비스의..

테스트코드 실습하기 (단위, 통합 테스트) - TestCode(14)

Tech/Test 2023. 7. 30. 20:28
728x90
728x90

서론

인강은 끝이 났고, 인강 내용을 바탕으로 첫 테스트 코드를 적용한 토이 프로젝트 내 코드들을 조금 리뷰해보려고 한다.

 

프로젝트를 간단히 소개하자면, 그냥 롤 챔피언 정보를 JSON으로 받아와서 무언가를 하는 서비스를 구현 할 예정이다.

 

우선 돌아가게는 작성했는데 테스트 코드를 처음 직접 활용하다보니 부족한 부분이 많을 수 있다.

 

혹여 지나가다 보시는 고수분들께서 틀린 점이 있다면 지적좀 해주시면 감사할 따름..

 

 

 

단위 테스트

Champion의 Repository에 대한 테스트를 작성했다.

package com.example.lolchampionsinvestment.domain.champion;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.LocalDateTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class ChampionRepositoryTest {

    @Autowired
    private ChampionRepository championRepository;

    @DisplayName("챔피언을 등록한다.")
    @Test
    void test(){
        //given
        Champion Aatrox = createChampion("Aatrox", 1000, "설명", LocalDateTime.now());
        Champion Teemo = createChampion("Teemo", 10, "설명", LocalDateTime.now());
        int beforeSize = championRepository.findAll().size();

        championRepository.saveAll(List.of(Aatrox, Teemo));

        //when
        int currentSize = championRepository.findAllByNameIn(List.of("Aatrox","Teemo")).size();

        //then
        assertEquals(beforeSize + 2, currentSize);

    }

    private static Champion createChampion(String name, int price, String description, LocalDateTime createDateTime) {
        return Champion.builder()
                .name(name)
                .price(price)
                .description(description)
                .createDateTime(createDateTime)
                .build();
    }

}

 

빌더 패턴을 given에 작성했었고, green 테스트가 끝나고 난 후 리팩토링을 통해 메서드화하였다.

 

 

개선 및 학습 필요

RED, GREEN, REFACTOR의 과정을 통해 테스트를 작성하려고 했으나 아래 사항을 학습하여 지식을 보완하여야 한다.

 

1. RED의 경우를 정확히 어떤 식으로 두어야 할지 모르겠어서 두 경우를 모두 작성했었다.

  - Champion 객체를 build 시에 예외를 발생 시킬 수 있는 값을 던져주었다. (description = null)

  - 정상 입력 후 등록한 뒤 then에서 Size비교를 검증 실패의 값으로 던져줬다 (before = after)

 

 

 

 

 

 

통합 테스트

1. Service test

Repository에 등록하기 위한 로직을 작성한 Service를 테스트했다.

package com.example.lolchampionsinvestment.api.service.champion;

import com.example.lolchampionsinvestment.domain.champion.Champion;
import com.example.lolchampionsinvestment.domain.champion.ChampionRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@Transactional
@RequiredArgsConstructor
public class ChampionDataParsingService {

    private final ChampionRepository championRepository;

    private static String path = "/static/json/championFull.json";
    private static String imgURL = "http://ddragon.leagueoflegends.com/cdn/13.14.1/img/champion/";

    public void championsInsertTable() {
        List<Map<String, Object>> championList = championsMapping();
        List<Champion> champions = new ArrayList<>();

        for(Map<String, Object> map : championList) {
            Champion champion = Champion.builder()
                    .name((String) map.get("name"))
                    .price((int) (Math.random() * 9 + 1) * 1000)
                    .img(imgURL + (String) map.get("id") + ".png")
                    .description((String) map.get("description"))
                    .createDateTime(LocalDateTime.now())
                    .build();

            champions.add(champion);
        }

        championRepository.saveAll(champions);
    }

    public List<Map<String, Object>> championsMapping() {
        List<Map<String, Object>> championList = new ArrayList<>();
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            String championJsonString = getChampions();
            Map<String, Object> championsMap = objectMapper.readValue(championJsonString, new TypeReference<Map<String, Object>>() {});
            Map<String, Map<String, Object>> championMap = (Map<String, Map<String, Object>>) championsMap.get("data");

            championList = championMap.values().stream().map(championData -> Map.of(
                    "id", championData.get("id"),
                    "name", championData.get("name"),
                    "description", championData.get("lore")
            )).toList();
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }

        return championList;
    }

    private String getChampions() {
        return readLines();
    }

    private String readLines() {
        return new BufferedReader(
                new InputStreamReader(
                        ChampionDataParsingService.class.getResourceAsStream(path)
                )
        ).lines().collect(Collectors.joining());
    }

}

서비스 로직은 우선적으로 구현을 위해 json 파일의 경로를 직접 입력하였지만 배포까지 고려했을 때 어떻게 할 지 생각중인 부분이다.

 

배포 시 관리자의 권한에서, json파일을 받아와서 업로드 시키면 서버의 json파일을 교체하고 새로 데이터를 파싱시켜 추가된 챔피언 정보를 업데이트 시킬지, 아니면 Crawler와 같은 기능을 하는 로직을 작성하여 직접 라이엇 개발자 포털에서 파싱하여 사용할 지에 대한 부분이 고민중이며 아직 방향은 잡지 않았다.

 

여튼, 서버 내 json file을 파싱하여 원하는 값만 매핑한 뒤 DB에 insert시키는 로직을 작성했고 이를 테스트하고자 했다.

package com.example.lolchampionsinvestment.api.service.champion;

import com.example.lolchampionsinvestment.domain.champion.Champion;
import com.example.lolchampionsinvestment.domain.champion.ChampionRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@ActiveProfiles("test")
@Transactional
class ChampionDataParsingServiceTest {

    @Autowired
    ChampionDataParsingService championDataParsingService;
    @Autowired
    ChampionRepository championRepository;

    @DisplayName("json파일을 읽어와서 테이블에 챔피언 정보를 등록한다.")
    @Test
    void test(){
        //given
        List<Map<String, Object>> championList = championDataParsingService.championsMapping();
        int listSize = championList.size();
        int beforeTableSize = championRepository.findAll().size();

        //when
        championDataParsingService.championsInsertTable();
        int afterTableSize = championRepository.findAll().size();

        //then
        assertThat(afterTableSize).isEqualTo(listSize);
        assertThat(beforeTableSize).isEqualTo(afterTableSize - championList.size());

    }

}

개선 및 학습 필요

생각하는 부분은, 위에서도 그렇지만 공통적으로 then부분이라고 생각하는데 단순히 전 후 상황을 비교하여 insert됐는지만 파악하기 위해서 위와 같이 작성을 했는데 이게 맞는 테스트 중의 하나인지, 아니면 then에서 검증하는 정형화된 로직이 있는지는 더 공부해나가야 할 부분이라고 생각한다.

 

 

 

2. Controller test

Controller를 마지막으로 테스트 하려했다.

package com.example.lolchampionsinvestment.api.controller.champion;

import com.example.lolchampionsinvestment.api.ApiResponse;
import com.example.lolchampionsinvestment.api.service.champion.ChampionDataParsingService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.HttpClientErrorException;

@RestController
@RequiredArgsConstructor
public class ChampionDataParsingController {

    private final ChampionDataParsingService championDataParsingService;

    @PostMapping("/api/v1/champions/new")
    public ApiResponse<String> addAllJsonChampionsData() {
        try {
            championDataParsingService.championsInsertTable();
            return ApiResponse.ok("파싱 및 테이블 저장 완료");
        } catch (HttpClientErrorException httpClientErrorException) {
            return ApiResponse.of(HttpStatus.BAD_REQUEST, httpClientErrorException.getMessage(), null);
        } catch (Exception e) {
            return ApiResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, "Error occurred while parsing and saving champion data", null);
        }
    }

}

컨트롤러에서는, service의 로직을 동작하게끔 하였으며, 과정에서 Response를 받아오기 위해 작성하였다.

 

Response를 받아오는 과정에서, 인강 수강 당시에 웹 응답 자체를 객체화하여 사용하는 것을 목격해서 사용해보고 싶어서 사용해보았다.

package com.example.lolchampionsinvestment.api;

import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
public class ApiResponse<T> {

    private int code;
    private HttpStatus status;
    private String message;
    private T data;

    public ApiResponse(HttpStatus status, String message, T data) {
        this.code = status.value();
        this.status = status;
        this.message = message;
        this.data = data;
    }

    public static <T> ApiResponse<T> of(HttpStatus httpStatus, String message, T data) {
        return new ApiResponse<>(httpStatus, message, data);
    }

    public static <T> ApiResponse<T> of(HttpStatus httpStatus, T data) {
        return of(httpStatus, httpStatus.name(), data);
    }

    public static <T> ApiResponse<T> ok(T data) {
        return of(HttpStatus.OK, data);
    }

}

 

package com.example.lolchampionsinvestment.api.controller.champion;

import com.example.lolchampionsinvestment.api.service.champion.ChampionDataParsingService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
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.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import java.util.List;
import java.util.Map;

import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
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(controllers = ChampionDataParsingController.class)
class ChampionDataParsingControllerTest {

    @Autowired
    private MockMvc mockMvc;
    @Autowired
    ObjectMapper objectMapper;
    @MockBean
    private ChampionDataParsingService championDataParsingService;

    @DisplayName("챔피언 데이터를 파싱하고 테이블에 등록한다.")
    @Test
    void testChampionDataParsingAndInsertion() throws Exception {
        //when //then
        mockMvc.perform(
                        post("/api/v1/champions/new")
                                .contentType(MediaType.APPLICATION_JSON)
                )
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value("200"))
                .andExpect(jsonPath("$.status").value("OK"))
                .andExpect(jsonPath("$.message").value("OK"));

        verify(championDataParsingService).championsInsertTable();
    }

}

개선 및 학습 필요

red 상황에 대한 test를 하고 싶었는데, 400번대 에러에 대한 테스트를 수행하기 위해서 고민을 했고, 400번대 에러는 관리자가 잘못된 Champion객체를 가져왔을 때라고 생각했고, 이는 json 파싱이 제대로 이루어지지 않았을 때라고 판단했다.

 

그런데 400번대 에러를 내기 위해서 코드를 작성하려는데, 해당 API의 파라미터가 없는 형태기 때문에 어떻게 json 파싱 상황에서 예외를 발생시킬지에 대한 해결책을 구상하지 못했다. 아마 Service에서 경로를 필드선언 하지 않고 파라미터로 넘겨받게끔 해야하는 것 같은데, 내가 구상한 서비스와 맞지 않는 부분이라 헤매고 있다.

 

 

 

마무리

부족부분은 보완해서 재포스팅이 반드시 필요할 것 같다.

테스트를 잘 하는 개발자가 될 수 있도록 앞으로도 정진!

 

 

 

 

 

 

 

관련 포스팅

 

테스트 코드를 작성하는 이유 - TestCode (1)

단위 테스트 - TestCode (2)

JUnit 5를 사용한 Java 단위 테스트 - TestCode (3)

TDD - TestCode (4)

BDD - TestCode(5)

통합 테스트(Integration Test) - TestCode(6)

@AfterEach, @BeforeEach - TestCode(7)

Mock / Test Double - TestCode (8)

더 좋은 테스트 작성하기(1) - TestCode(9)

더 좋은 테스트 코드 작성하기 2 - Test Fixture 구성하기 / TestCode(10)

더 좋은 테스트 코드 작성하기 3 - Dynamic Test / TestCode(11)

더 좋은 테스트 코드 작성하기 4 - 테스트 환경 통합 / TestCode(12)

private method test를 해야하는가? / TestCode(13)

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

private method test를 해야하는가? / TestCode(13)

Tech/Test 2023. 7. 16. 06:10
728x90
728x90

서론

본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다.

 

Practical Testing: 실용적인 테스트 가이드 - 인프런 | 강의

이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강

www.inflearn.com

 


 

 

private method를 테스트해야 하는가

강의에서는, private 메서드를 테스트할 필요가 없으며, private 메서드를 테스트 고려해야 할 때를 객체를 분리해야하는 시점인가? 라는 질문을 던져보라고 하였다.

 

아래의 코드는, 다음 상품번호를 생성해주는 ProductService에서 발췌한 private 메서드 부분이다.

private String createNextProductNumber() {
    String latestProductNumber = productRepository.findLatestProductNumber();
    if (latestProductNumber == null) {
        return "001";
    }

    int latestProductNumberInt = Integer.parseInt(latestProductNumber);
    int nextProductNumberInt = latestProductNumberInt + 1;

    return String.format("%03d", nextProductNumberInt);
}

 

private 메서드 부분을 새로운 Component 객체로 만들어, 새로운 객체의 공개 api(public)으로 만들어 책임 분리를 시켜서

해당 메서드를 테스트할 수 있긴 하다.

@Component
@RequiredArgsConstructor
public class ProductNumberFactory {

    private final ProductRepository productRepository;
    
    public String createNextProductNumber() {
    String latestProductNumber = productRepository.findLatestProductNumber();
    if (latestProductNumber == null) {
        return "001";
    }

    int latestProductNumberInt = Integer.parseInt(latestProductNumber);
    int nextProductNumberInt = latestProductNumberInt + 1;

    return String.format("%03d", nextProductNumberInt);
}

}

 

우선, 해당 private 메서드가 클래스 내부에서 한 번도 사용이 되지 않았을 리가 없다. 해당 private 메서드는 클래스 내부에서 다른 테스트 안에 포함되어 항상 테스트가 가능했고, 가능할 것이다. 그렇게 짜여지지 않았다면 올바른 코드를 작성하지 않았다는 소리다. 그것을 아래 Justin Woo 님은 당신의 코드에 냄새가 난다고 서술하였다.

 

정말 private 메서드를 테스트할 욕구가 강하게 들거나, 해야 하는 상황이라면 무언가 설계가 잘못되었다는 신호로, 객체 분리의 신호로 받아들여 하나의 public 메서드 안에서 하는 일이 많지는 않은지, 객체가 가지는 로직이 많지 않은지 점검하여 private이라는 내부의 작은 기능을 별도의 객체로 위임을 해서 테스트를 단독으로 수행할 수 있도록 한다면 좋은 설계가 될 수 있을 것이다.

 

 

 


 

 

 

References

옛날 글이지만(14년도) 너무 좋은 개인 의견과 자료 제시해주신 Justin Woo님의 글을 정리해보았다.

출처 : https://justinchronicles.net/ko/2014/07/14/dont-do-testing-private-methods/

 

테스트 코드 공부를 진행하면서 접근제어자가 어떤 것이든 테스트를 해야하지 않나?? 라는 생각을 하고 있었다.

찾아보니 private 메서드는 우선 Mocking이 불가능하다. 테스트를 할 경우 보통 인터페이스를 통해 Mocking을 한 후 진행하는데, private 메서드는 인터페이스에 노출되어 있지 않기 때문이다.

 

private 메서드를 테스트해야 한다면, 또는 바꿀 때 마다 테스트 코드를 다시 짜야한다면, 리팩토링에 있어 큰 장애물이 될 것이다. 코드를 개선시키기 위해 해야 하는 일의 양이 private 메서드를 바꾸기 위해 필요한 일의 양이 되어버린다. 결국 이 모든 부정적인 과정들은 코드를 리팩토링하는 것을 주저하게 만든다.

 

 

 

 

 


 

 

 

 

 

관련 포스팅

테스트 코드를 작성하는 이유 - TestCode (1)

단위 테스트 - TestCode (2)

JUnit 5를 사용한 Java 단위 테스트 - TestCode (3)

TDD - TestCode (4)

BDD - TestCode(5)

통합 테스트(Integration Test) - TestCode(6)

@AfterEach, @BeforeEach - TestCode(7)

Mock / Test Double - TestCode (8)

더 좋은 테스트 작성하기(1) - TestCode(9)

더 좋은 테스트 코드 작성하기 2 - Test Fixture 구성하기 / TestCode(10)

더 좋은 테스트 코드 작성하기 3 - Dynamic Test / TestCode(11)

더 좋은 테스트 코드 작성하기 4 - 테스트 환경 통합 / TestCode(12)

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

더 좋은 테스트 코드 작성하기 4 - 테스트 환경 통합 / TestCode(12)

Tech/Test 2023. 7. 15. 06:34
728x90
728x90

서론

본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다.

 

Practical Testing: 실용적인 테스트 가이드 - 인프런 | 강의

이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강

www.inflearn.com

 


 

 

테스트 환경 통합하기

(참고) IntelliJ에서 전체 테스트 수행 시 Gradle - Tasks - verification - test

 

 

강의 내 작성했던 코드를 가지고 전체 테스트를 돌려보았다. 작성한 단위테스트와 통합테스트 모두 돌아갔으며 로그가 남았다. 로그를 확인해보니 Spring Boot가 6번 등장했다. 6번의 서버가 등장했고, 이러한 동작은 테스트 비용을 증가시키고, 테스트 속도도 느리게 동작할 수 있다. 이를 어떻게 개선하면 좋을까?

 

각 테스트 마다, 테스트 환경이 조금이라도 달라지면 새로운 서버를 띄운다. 이를 개선하기 위해 공통적으로 구성할 수 있는 환경은 통합을 하여 서버를 띄우는 수를 줄일 수 있다.

 

강의에서는, 상위 클래스를 생성하여 통합할 수 있는 환경의 테스트에는 상속을 시켜 주는 것으로 해결하였다.

@ActiveProfiles("test")
@SpringBootTest
public abstract class IntergrationTestSupport{
}
class Test1 extends IntegrationTestSupport{

	(...생략...)
    
}

class Test2 extends IntegrationTestSupport{

	(...생략...)
    
}

class Test3 extends IntegrationTestSupport{

	(...생략...)
    
}

상위 클래스를 통해 상속을 진행하여 서버 사용 횟수를 줄였다.

 

같은 클래스를 상속받았더라도 내부 환경이 다를 수 있다. 이런 경우에는 상속을 통해 구현하더라도 서버를 별개로 사용한다. 예를 들어 하위 클래스에서 @MockBean을 사용했다던가 하는 경우이다. 해당 경우에는 상위 클래스에 @MockBean을 올려 사용할 수 있지만 다른 테스트에도 MockBean으로 등록되는 점은 고려하여야 한다.

 

 

 


 

관련 포스팅

테스트 코드를 작성하는 이유 - TestCode (1)

단위 테스트 - TestCode (2)

JUnit 5를 사용한 Java 단위 테스트 - TestCode (3)

TDD - TestCode (4)

BDD - TestCode(5)

통합 테스트(Integration Test) - TestCode(6)

@AfterEach, @BeforeEach - TestCode(7)

Mock / Test Double - TestCode (8)

더 좋은 테스트 작성하기(1) - TestCode(9)

더 좋은 테스트 코드 작성하기 2 - Test Fixture 구성하기 / TestCode(10)

더 좋은 테스트 코드 작성하기 3 - Dynamic Test / TestCode(11)

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

더 좋은 테스트 코드 작성하기 3 - Dynamic Test / TestCode(11)

Tech/Test 2023. 7. 14. 06:57
728x90
728x90

서론

본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다.

 

Practical Testing: 실용적인 테스트 가이드 - 인프런 | 강의

이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강

www.inflearn.com

 


 

 

Dynamic Test

환경을 설정해놓고, 중간중간 변화를 주며 검증을하는 등의 시나리오를 테스트하고 싶을 때 사용한다.

@TestFactory
Stream<DynamicTest> exampleDynamicTest() {
    return Stream.of(
        dynamicTest("First Dynamic Test", () -> {
            // test code
        }),
        dynamicTest("Second Dynamic test", () -> {
            // test code
        })
    );
}

 

사용 방법 및 특징

  • @TestFactory 애너테이션을 사용하여 작성한다. 이 때 메서드는 private / static이 되면 안된다.
  • 또한 Stream, Collection, Iterable 또는 Iterator을 반환해야 한다. 그렇지 않으면 JUnitException에 걸린다.
  • 테스트 수는 동적이며, ArrayList 크기에 따라 달라진다.
  • 테스트 이름과 실행 함수, 두 가지 요소로 이루어져 있으며 테스트 이름을 잘 작성하여 가독성을 높일 수 있어야 한다.
  • 테스트 생명 주기와 관련된 요소들을 사용할 수 없다(Test Fixture Method - @AfterEach 등)
  • 런타임 시점에 테스트 케이스를 생성할 수 있어 유연하다.

 


 

 

잡담

아직 사용해 본 경험이 없어 개념 정도만 정리해두었다.

테스트 관련 공부를 끝내고 토이 프로젝트 하나 진행해보면서 Dynamic Test를 통한 시나리오 테스트를 한번 진행해봐야겠다. 회원가입이라던가 로그인 같은 걸로..

 

 

 

 


 

 

References

https://www.baeldung.com/junit5-dynamic-tests

 

 

 


 

 

 

관련 포스팅

테스트 코드를 작성하는 이유 - TestCode (1)

단위 테스트 - TestCode (2)

JUnit 5를 사용한 Java 단위 테스트 - TestCode (3)

TDD - TestCode (4)

BDD - TestCode(5)

통합 테스트(Integration Test) - TestCode(6)

@AfterEach, @BeforeEach - TestCode(7)

Mock / Test Double - TestCode (8)

더 좋은 테스트 작성하기(1) - TestCode(9)

더 좋은 테스트 코드 작성하기 2 - Test Fixture 구성하기 / TestCode(10)

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

더 좋은 테스트 코드 작성하기 2 - Test Fixture 구성하기 / TestCode(10)

Tech/Test 2023. 7. 13. 22:40
728x90
728x90

서론

본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다.

 

Practical Testing: 실용적인 테스트 가이드 - 인프런 | 강의

이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강

www.inflearn.com

 


 

 

Test Fixture 구성하기

중복 발생이 되는 상황들을 한 곳에 묶어 관리하는 개념으로 JUnit에서는, 동일하거나 유사한 개체 집합에 대해 작동하는 두 개 이상의 테스트가 있는 경우 Test Fixture 메서드의 사용을 권장한다.

 

굳이 메서드를 사용하지 않더라도 픽스처를 구성할 때, 원하는 파라미터만 명확히 명시하여, 테스트 내에서 어떤 파라미터를 통해 확인할 지 명확하게 명시할 수 있어야 한다. 또한 테스트 픽스쳐를 객체의 생성자를 이용해 객체가 생성될 경우를 미리 정의하여 세팅을 따로 해놓았다가 필요한 것만 꺼내서 사용할 수도 있다.

 

아래는 Fixture 메서드이다.

@BeforeAll 
void beforeAll() {
	// 테스트 메서드 시작 전 실행
}

@AfterAll 
void afterAll() {
	// 테스트 메서드 종료 후 실행
}

@BeforeEach
void beforeEach() {
	// 각 테스트 메서드 시작 전 실행
}

@AfterEach
void afterEach() {
	// 각 테스트 메서드 종료 후 실행
}

 


 

@deleteAll, @deleteAllInBatch

강사분을 포함하여 인터넷을 조금만 뒤져봐도, deleteAll 대신 deleteAllInBach를 선호한다고 하는데, 이유를 알아보자

 

deleteAll은 건 별로 delete 쿼리를 수행한다.

deleteAll의 장점은 매핑되어 있는 테이블을 포함하여 delete를 수행한다는 점이다.

하지만 건 별로 진행되기 때문에 100만 건의 element를 삭제해야 한다면, 100만 건의 delete 쿼리를 수행해야 한다.

/*
 * (non-Javadoc)
 * @see org.springframework.data.repository.Repository#deleteAll()
 */
@Override
@Transactional
public void deleteAll() {

	for (T element : findAll()) {
		delete(element);
	}
}

 

deleteAllInBatch는 delete from [table name]의 쿼리를 한 번 수행한다.

/*
 * (non-Javadoc)
 * @see org.springframework.data.jpa.repository.JpaRepository#deleteInBatch(java.lang.Iterable)
 */
@Override
@Transactional
public void deleteAllInBatch(Iterable<T> entities) {

	Assert.notNull(entities, "Entities must not be null!");

	if (!entities.iterator().hasNext()) {
		return;
	}

	applyAndBind(getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName()), entities, em)
			.executeUpdate();
}

 

 

쿼리 성능 측면에서 deleteAllInBatch가 압도적으로 우세할 것 같다. 이렇기 때문에 다들 deleteAllInBatch를 선호하는 것 같다.

 


 

@ParameterizedTest

User 클래스가 있고, User 클래스에 대한 테스트를 작성한다고 하자.

public class User {
    private String name;
    
    public User(String name) {
        validateName(name);
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
    
    private void validateName(String name) {
        validateNotNull(name);
        validateLength(name);
    }
    
    private void validateNotNull(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("이름은 공백일 수 없습니다.");
        }
    }
    
    private void validateLength(String name) {
        if (name.length > 10) {
            throw new IllegalArgumentException("이름은 10자 이하여야 합니다.");
        }
    }
}

 

@Test
@DisplayName("이름으로 null 값 예외처리")
void createUserFromNullName() {
    assertThatThrownBy(() -> new User(null))
            .isInstanceOf(IllegalArgumentException.class);
}

@Test
@DisplayName("공백 이름 예외처리")
void createUserFromBlankName() {
    assertThatThrownBy(() -> new User(""))
            .isInstanceOf(IllegalArgumentException.class);
}

@Test	
@DisplayName("10자를 초과하는 이름 예외처리")
void createUserFromGreaterThan10() {
    assertThatThrownBy(() -> new User("이 이름은 10자를 넘어가는 이름입니다"))
            .isInstanceOf(IllegalArgumentException.class);
}

세 경우 모두 User 객체를 생성하고, 과정에서 IllegalArgumentException을 던지는지 검증하는 로직인데, 같은 로직임에도 테스트가 세 가지 경우나 생겨버렸다. 만약 특수문자 불가능 등 여러 규칙이 더 생성된다면 추가적으로 메서드를 더 작성해주어야 할 것이다.

 

JUnit에서는 하나의 테스트 케이스인데, 값을 여러 개로 바꿔가며 테스트해보고 싶을 때, 여러 개의 테스트를 한 번에 작성하기 위해 @ParameterizedTest 애너테이션을 제공한다. 값이나 환경 등에 대한 데이터를 변경하여 케이스를 확장하며 테스트를 수행하고 싶을 때 사용한다. 이 때, 파라미터로 넘겨줄 값들을 지정하기 위해 아래의 애너테이션들을 사용한다.

 

 

@ValueSource

한 개의 파라미터 입력 시 사용한다.

@ParameterizedTest
@DisplayName("ValueSource를 이용한 User 생성 테스트")
@ValueSource(strings = {"", " "})
void createUserException(String text) {
    assertThatThrownBy(() -> new User(text))
            .isInstanceOf(IllegalArgumentException.class);
}

 

@CsvSource

한 파라미터와, 해당 파라미터를 입력했을 때의 출력 값을 입력할 때 사용한다.

int multiplyBy2(int number) {
    return number * 2;
}

@ParameterizedTest
@CsvSource(value = {"1,2", "2,4", "3,6"})
void multiplyBy2Test(int input, int expected) {
    assertThat(multiplyBy2(input)).isEqualTo(expected);
}

CSV라는 이름에서 알 수 있듯이, input과 expected 값은 콤마로 구분할 수 있다. delimiter값을 정의하여 커스텀 구분자를 사용할 수 있으며, 구분자는 char 값이기 때문에 단일 문자를 넣어주어야 한다. String값을 사용하고 싶다면 delimiterString을 사용한다.

@CsvSource(value = {"1:2", "2:4", "3:6"}, delimiter = ':')
@CsvSource(value = {"1//2", "2//4", "3//6"}, delimiterString = "//")

 

 

@NullSource @EmptySource @NullOrEmptySource

Null / 공백 / Null or 공백 상황에 대한 테스트 시 사용한다.

당연한 소리지만, 원시 타입에는 Null이 들어갈 수 없기 때문에 원시 타입을 파라미터로 사용할 경우 @NullSource와 @NullOrEmptySource는 사용이 불가능하다.

@NullSource와 @EmptySource 모두를 사용한 것과 @NullOrEmptySource은 같으며, @ValueSource를 같이 사용할 수 있다.

@ParameterizedTest
@DisplayName("null 값 또는 empty 값으로 User 생성 테스트")
@NullAndEmptySource
@ValueSource(strings = {""," "})
void createUserExceptionFromNullOrEmpty(String text) {
    assertThatThrownBy(() -> new User(text))
            .isInstanceOf(IllegalArgumentException.class);
}

 

@EnumSource

Enum값에 대한 테스트 시 사용한다.

value에 Enum 클래스를 넣고, names에 조건으로 지정할 값을 넣어준다.

@ParameterizedTest
@DisplayName("6, 7월이 31일까지 있는지 테스트")
@EnumSource(value = Month.class, names = {"JUNE", "JULY"})
void isJuneAndJuly31(Month month) {
    assertThat(month.minLength()).isEqualTo(31);
}

names를 입력했을 때 mode값을 설정할 수 있으며 지원하는 mode값은 다음과 같다.

// name과 일치하는 모든 Enum 값
mode = INCLUDE

// name을 제외한 모든 Enum 값
mode = EXCLUDE

// 조건을 하나라도 만족하는 Enum값
mode = MATCH_ANY

// 조건을 모두 만족하는 Enum값
mode = MATCH_ALL

 

 

 

@MethodSource

파라미터에 메서드를 사용한다.

@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void isBlank_ShouldReturnTrueForBlankStrings(String input, boolean expected) {
    assertThat(input.isBlank()).isEqualTo(expected);
}

private static Stream<Arguments> provideStringsForIsBlank() {
    return Stream.of(
            Arguments.of("", true),
            Arguments.of("  ", true),
            Arguments.of("not blank", false)
    );
}

위의 예시는 메서드를 정의하고, 메서드를 @MethodSource를 이용하여 테스트 메서드에 넘겨준다.

이 때, 메서드 이름은 서로 같아야 하며, 메서드 정의는 static으로 해야 한다. 만약 애너테이션에 메서드 이름을 작성하지 않았을 경우 JUnit이 테스트 메서드와 같은 이름의 메서드를 찾아서 파라미터로 제공한다.

 

 


 

 

 

 

 

 

References

https://www.baeldung.com/parameterized-tests-junit-5

https://velog.io/@ohzzi/junit5-parameterizedtest

https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests

 

 

 


 

 

 

관련 포스팅

테스트 코드를 작성하는 이유 - TestCode (1)

단위 테스트 - TestCode (2)

JUnit 5를 사용한 Java 단위 테스트 - TestCode (3)

TDD - TestCode (4)

BDD - TestCode(5)

통합 테스트(Integration Test) - TestCode(6)

@AfterEach, @BeforeEach - TestCode(7)

Mock / Test Double - TestCode (8)

더 좋은 테스트 작성하기(1) - TestCode(9)

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

더 좋은 테스트 코드 작성하기 1 - TestCode(9)

Tech/Test 2023. 7. 11. 22:52
728x90
728x90

서론

본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다.

 

Practical Testing: 실용적인 테스트 가이드 - 인프런 | 강의

이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강

www.inflearn.com

 


 

 

 

하나의 주제에 대한 테스트 수행

한 가지의 테스트에서는 한 가지 목적의 검증만을 수행하여야 한다. DisplayName을 한 문장으로 치환할 수 있는지에 대한 고민을 해보는 것이 좋다.

 

예를 들어, 조건문, 반복문이 들어간 테스트는 포괄적인 테스트가 될 수 있다.

@DisplayName("동물이 동물원에 있는지 확인한다.")
@Test
void checkAnimalType() {
	//given
    AnimalType[] animalTypes = AnimalType.values();
    
    for (AnimalType animalType : animalTypes) {
    	if (animalType == AnimalType.TIGER) {
        	boolean result = NationalPark.containsAnimalType(animalType);
            
            assertThat(result).isTrue();
    }
    
    	if (animalType == AnimalType.LION) {
        	boolean result = NationalPark.containsAnimalType(animalType);
            
            assertThat(result).isTrue();
        }    
}

위 예시코드는, 강의 내용을 조금 변형하여 작성한 예시이다. 위의 경우 하나의 타입과 메서드에 대한 테스트를 진행하는 것을 하나의 메서드로 작성하려고 했다. 그러다보니 여러 상황을 animalTypes에 여러 type들이 들어오는 상황을 고려해야 한다고 생각하다보니 조건 반복문을 통해 여러 경우로 쪼개줄 수 밖에 없었다.

 

조건문의 경우 두가지 이상의 내용을 포함하고 있을 수 밖에 없고, 반복문의 경우 테스트 작성자가 아닌 다른 사람이 볼 경우 어떤 의도를 갖고 테스트 코드를 작성하였는지 생각해볼 수 밖에 없다.

이러한 논리 구조(조건, 반복)는 테스트 코드가 지향하는 테스트 대상과, 환경 구성에 대한 방해 요소로 작용될 수 있다.

케이스 확장이 필요하다면 @ParameterizedTest와 같은 애너테이션을 활용하는 것이 바람직하다.

 

 


 

완벽하게 제어하기

테스트 환경 조성 시 모든 조건을 완벽하게 제어할 수 있어야 한다.

현재시간, 랜덤값 등 제어할 수 없는 값들은 상위 계층으로 분리하여 테스트 가능한 값으로 만드는 것을 고려하여야 한다.

public Enter userEnter() {
    LocalDateTime curDateTime = LocalDateTime.now();
    LocalTime curTime = curDateTime.toLocalTime();
    if (curTime.isBefore(OPEN_TIME) || curTime.isAfter(CLOSE_TIME) {
        throws new IllegalArgumentException("영업 시간이 아닙니다.");
    }
    
    return new Enter(curDateTime, user);
}

public Enter userEnter(LocalDateTime curDateTime) {
    LocalTime curTime = curDateTime.toLocalTime();
    if (curTime.isBefore(OPEN_TIME) || curTime.isAfter(CLOSE_TIME) {
    	throws new IllegalArgumentException("영업 시간이 아닙니다.");
    }
    
    return new Enter(curDateTime, user);
}

 

위의 예시는 현재 시간을 분리하여 상위 레벨인 파라미터로 분리하여 주입시켜 고정된 상황을 테스트할 수 있는 좋은 예시이다.

 

테스트 시 아래처럼 시간에 대한 값을 고정으로 지정하여 RED, GREEN상황에 대한 테스트를 작성하여 완벽하게 제어할 수 있어야 한다.

@Test
void userEnterWithCurrentTime(){
    Zoo zoo = new Zoo();
    User user1 = new User();

    Zoo.add(user1);

    Enter enter = zoo.userEnter(LocalDateTime.of(2023, 7, 11, 22, 45));

    assertThat(enter.getUser()).isNotEmpty();
}

 

이처럼, 테스트 시 완벽하게 제어할 수 있는가 에 대한 질문을 계속하면서 테스트를 구성하는 것이 좋다. 만약 외부 시스템과 연동할 경우, 외부 시스템에서의 응답을 예상하여 Mocking 처리를 한 후 테스트를 구성할 수 있다.

 

 


 

테스트 환경의 독립성 보장

테스트의 주제를 제외한 곳에서, 외부의 메서드를 끌어다 쓸 때 발생하는 문제를 알아보자.

@DisplayName("재고가 없는 상품으로 주문을 생성하려는 경우 예외가 발생한다.")
@Test
void createOrderWithNoStock(){
    //given
    LocalDateTime registeredDateTime = LocalDateTime.now();

    Product product1 = createProduct(BOTTLE, "001", 1000);
    Product product2 = createProduct(BAKERY, "002", 3000);
    Product product3 = createProduct(HANDMADE, "003", 5000);
    productRepository.saveAll(List.of(product1, product2, product3));

    Stock stock1 = Stock.create("001", 2);
    Stock stock2 = Stock.create("002", 2);
    stock1.deduckQuantity(1);
    stockRepository.saveAll(List.of(stock1, stock2));

    OrderCreateServiceRequest  request = OrderCreateServiceRequest.builder()
            .productNumbers(List.of("001", "001", "002", "003"))
            .build();

    //when // then
    assertThatThrownBy(() -> orderService.createOrder(request, registeredDateTime))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessage("재고가 부족한 상품이 있습니다.");
}

위의 코드는 따로 예시를 만들기가 애매해서, 강의의 일부를 가져왔다.

 

문제가 되는 부분은 deduckQuantity(1) 메서드를 호출하는 부분이다.

 

테스트가 실패하는 구간은 then인데, given절에서 테스트를 생성하기 위해 조건을 줄 때, 해당 메서드에서 테스트가 깨져버려서 테스트를 수행하는 의미를 퇴색시킨다.

 

또한 given에서, 수행하고자 하는 테스트의 조건을 파악하는 데 쉽지 않다. 테스트 코드만을 보고 파악할 수 없고, 해당 메서드를 직접 확인하여 어떤 동작을 거치는지 또 파악해야하기 때문이다.

 


 

테스트 간 독립성 보장

두 가지 이상의 테스트가 하나의 자원을 공유해서는 안된다.

class NewTest {

    private static final TestSomthing ts = TestSomthing("001", 100);
    
    @DisplayName("테스트 객체의 수량이 제공된 수량보다 작은지 확인한다")
    @Test
    void isCntLessThan() {
    
    	// given
        int quantity = 50;
        
        // when // then
        assertThat(ts.getCnt() < quantity).isTrue();
    
    }
    
    @DisplayName("테스트 객체를 주어진 수만큼 차감할 수 있다.")
    @Test
    void minusCnt() {
    
    	// given
        int quantity = 60;
        
        // when
        int now = ts.getCnt();
        int minus = ts.minus();
        
        // then
        assertEquals(now, minus);
    
    }

}

위의 코드에서, 순서가 변하면, 테스트의 결과가 변할 수 있다.

 

이처럼 테스트 내부에서 자원의 상태를 변경한다면 다른 테스트에서 어떤 영향을 미칠지 몰라 테스트의 신뢰성을 보장할 수 없다. 테스트는 수행 순서와 관계없이 절대적인 결과를 반환해야 한다.

 

 


 

 

 

관련 포스팅

테스트 코드를 작성하는 이유 - TestCode (1)

단위 테스트 - TestCode (2)

JUnit 5를 사용한 Java 단위 테스트 - TestCode (3)

TDD - TestCode (4)

BDD - TestCode(5)

통합 테스트(Integration Test) - TestCode(6)

@AfterEach, @BeforeEach - TestCode(7)

Mock / Test Double - TestCode (8)

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

Mock / Test Double - TestCode (8)

Tech/Test 2023. 7. 9. 18:34
728x90
728x90

서론

본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다.

 

Practical Testing: 실용적인 테스트 가이드 - 인프런 | 강의

이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강

www.inflearn.com

 


 

Mock

사전적인 뜻은 모조품, 가짜 라는 뜻을 가지고 있으며, 테스트 진행 시 진행하고자 하는 실제 객체와 동일한 가짜 객체(Mock)를 만들어서 사용한다.

 

언제 사용해야 할까?

  • 테스트 작성 환경 구축이 어려울때
  • 특정 경우에 의존적일때
  • 테스트 시간이 오래걸릴 때

 

 

Mock과 Stub

자주 사용되는 Mock과 Stub은 가짜 객체이고, 요청한 것에 대한 어떤 반환 값을 받아볼 수 있다는 점에서는 비슷하여 혼동하기 쉽다. 두 개념에 대해 마틴 파울러의 의견을 담은 게시물을 가져왔다. 참고 해보면 좋을 것 같다.

 

Mocks Aren't Stubs

Explaining the difference between Mock Objects and Stubs (together with other forms of Test Double). Also the difference between classical and mockist styles of unit testing.

martinfowler.com

Stub은 상태 검증에 초점이 맞춰져있다. 테스트에서 요청한 것에 대한 미리 준비된 결과를 제공하는 객체로써, 그 외에는 응답하지 않는다. 외부 의존성을 가진 코드의 테스트에 주로 사용된다.

Mock은 행위 검증이 중심이다. 행위에 대한 기대값을 작성하고, 그에 따라 동작하도록 만들어진 객체이다. 특정 메서드의 호출 여부와 매개변수 등을 확인하는 등 더 많은 기능을 제공한다.

 

 

 

 

Framework 종류

1. EasyMock

  • https://easymock.org/
  • 가장 오래된 Mock Framework
  • EasyMook Class Extention이라는 기능을 통해 구현 클래스를 통해 객체를 생성한다.
진행 순서
CreateMock : 인터페이스에 해당하는 Mock 객체 생성
Record : Mock 객체 메서드의 예상 동작을 녹화
Replay : 예정된 상태로 재생한다.
Verify : 행위가 발생했는지 검증한다.

 

 

2. jMock

  • http://jmock.org/
  • 가독성이 좋은 프레임워크로 연쇄호출이 특징이다.
  • Hamcrest Macher 라이브러리를 사용한다.
진행 순서
CreateMock : 인터페이스에 해당하는 Mock 객체 생성
Expect : 예상 동작을 미리 지정
Excercise : 테스트 메서드 내에서 Mock 객체 사용
Verify : 검증 (프레임워크가 직접 판단)

 

 

3. Mockito

  • https://site.mockito.org/
  • 사용법이 단순하여 테스트 자체에 집중할 수 있다.
  • 리팩토링이 쉽고 API가 단순하다.
  • 테스트 스텁을 만들기 쉽고, 스텁을 만드는 것과 검증을 분리한다.
  • 실패 시 발생하는 스택 트레이스가 깔끔하다.
진행 순서
CreateMock : 인터페이스에 해당하는 Mock 객체 생성
Stub : 예상 동작 지정
Excercise : 테스트 메서드 내에서 Mock 객체 사용
Verify : 검증 (프레임워크가 직접 판단)

 

 


 

 

테스트 더블(Test Double)

Mock의 상위 개념으로, 테스트하려는 객체와 연관된 객체를 사용하기가 어려울 때, 실제 객체의 일부 기능을 대신하는 대체 구현된 객체를 말한다. 용어의 출처는 스턴트맨에서 왔다고 한다. 스턴트맨을 영어로는 Stunt Double이라고 하며 여기서 차용된 용어라고 한다.

  • 테스트를 진행하기 어려운 경우 이를 대신해 테스트를 진행할 수 있도록 만들어주는 객체
  • Mock 객체와 유사하지만 테스트 더블이 상위의 의미로 사용됨

테스트 더블은 목적에 따라 아래의 유형별로 분류될 수 있다.

 

 

더미 객체(Dummy Object)

  • 인스턴스화 될 수 있는 수준으로 객체를 구현하며 객체의 기능은 필요하지 않은 경우 사용
  • 아무것도 하지 않는 허수아비

 

 

페이크 객체(Fake Object)

  • 실제 로직이 구현된 것처럼 보이는 객체
  • 다른 객체들과의 의존성을 제거하기 위해 사용
  • 적절한 수준에서 구현하거나, Mock 프레임워크를 사용한다.
  • 페이크 객체 생성을 위한 시간 등의 소요가 클 경우, 실제 객체를 가져와 테스트한다.
  • 단순 형태로 동일한 기능은 수행하지만, 프로덕션에서 쓰기에는 부족한 객체(ex : FakeRepository)
map으로 단순 구현이 가능하다.
Repository.save를 실행했을 경우 해당 객체를 put하고
Repository.findById를 실행했을 경우 Key 기반으로 Value를 return해주는 등의 형태를 취할 수 있음.

 

 

 

테스트 스텁(Test Stub)

  • 실제로 동작하는 것처럼 보이게 만들어 놓은 객체로 더미 객체보다 조금 더 구현된 객체
  • 특정 상태를 가정해 특정 값이나 메세지를 리턴해주는 작업을 한다.
  • 특정 상태를 가정해서 하드코딩된 형태로 로직에 따른 값의 변경을 테스트하는 것이 불가능하다.

 

 

테스트 스파이(Test Spy)

  • 테스트에 사용되는 객체, 메서드의 사용 여부 및 정상 호출 여부를 기록하고 보여준다.
  • 테스트 더블로 구현된 객체에 자기 자신이 호출되었을 때 확인이 필요한 부분을 기록하도록 구현한다.
  • 호출 횟수가 필요한 경우 전역 변수로 설정하여 로직을 메서드화하여 특정 테스트에서 호출한다.
  • 행위 기반 테스트가 필요한 경우 사용

 

 

Mock 객체(Mock Object)

  • 행위를 검증하기 위해 사용되는 객체로 프레임워크로 만들 수도 있다.
  • 테스트 더블의 하위 객체로의 좁은 의미와, 테스트 더블을 포함한 넓은 의미 두 가지로 사용될 수 있다.
  • 행위 검증은 복잡도나 정확성 등 작성하기 어려운 부분이 많기 때문에 상태 검증은 가능하다면 만들지 않는다.

 

 


 

 

 

References

 

Mock 객체란 무엇일까? 왜 써야될까?

아래 내용은 위 책에서 말하는 4장 TDD with Mock 에서 내용을 발췌했습니다. TDD를 공부하면서 Mock 이라는 용어는 너무나도 많이 나오고, 실제로 테스트 프레임워크를 사용하면 Mock 객체를 많이 사용

happy-coding-day.tistory.com

 

생물정보 전문위키, 인코덤

Wikipedia for Bioinformatics

www.incodom.kr

 

 


 

 

관련 포스팅

테스트 코드를 작성하는 이유 - TestCode (1)

단위 테스트 - TestCode (2)

JUnit 5를 사용한 Java 단위 테스트 - TestCode (3)

TDD - TestCode (4)

BDD - TestCode(5)

통합 테스트(Integration Test) - TestCode(6)

@AfterEach, @BeforeEach - TestCode(7)

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

@AfterEach, @BeforeEach - TestCode(7)

Tech/Test 2023. 7. 3. 06:09
728x90
728x90

서론

본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다.

 

Practical Testing: 실용적인 테스트 가이드 - 인프런 | 강의

이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강

www.inflearn.com


 

인강을 쭉 따라가다보니 어느새 Business Layer Test를 진행중이다.

해당 테스트에는, 주문을 생성하는 두 개의 테스트 메서드가 있고. 각각 red, green, refactor 과정을 거쳐 테스트를 수행했다.

 

하지만 위 사진처럼, 두 메서드를 통합하여 테스트 할 경우 테스트 에러가 발생하게 되었고 강의에서는 아래의 코드를 작성하여 해결하였다.

@AfterEach
void tearDown() {
    //데이터 클랜징 작업을 통해
    //tearDown절에서 clean작업
}

@AfterEach 애너테이션이 있는 tearDown 메서드를 활용하여 map의 key가 중복되는 것을 repository의 deleteAll(), deleteAllInBatch()를 사용하여 제거하였는데, 이에 대한 공부를 좀 해볼까 한다.

 

 


 

 

@AfterEach

한 번에 여러 테스트를 실행하면, 메모리 DB에 이전 테스트 결과가 남을 수 있다. 서론에서 든 예시에서도, 남아있는 map의 key 때문에 에러가 발생하였다.

 

@AfterEach를 사용하면 각 테스트가 종료될 때 마다 명시된 메서드의 코드들을 실행하기 때문에, 애너테이션의 기능을 활용하여 DB에 저장된 데이터를 삭제하여 테스트를 독립성을 보장해주어야 한다.

class OrderServiceTest {

    ProductRepository productRepository = new ProductRepository;

    @AfterEach
    void tearDown() {
        productRepository.clearProduct();
    }

(...생략...)

위 코드에서는, AfterEach를 통해, 테스트를 수행한 후 DB에 저장되어 있는 product들을 지워주었다.

당연하지만, 해당 repository에서 clearProduct 메서드에 대한 명시를 해주어야 한다.

 

 


 

 

@BeforeEach

반대로 @BeforeEach는 테스트 실행 전 무조건 실행된다.

공통 설정이나 초기화 작업을 수행하여 중복 코드의 제거를 통한 코드의 가독성 향상을 기대할 수 있다.

또한 일관된 테스트 환경을 설정할 수 있다.

class OrderServiceTest {
    ProductRepository productRepository;
    
    @BeforeEach
    public void setUp() {
        productRepository = new ProductRepository;
    }
}

 

 

 


 

 

번외

@DataJpaTest는 @Transactional이 붙어있어 @AfterEach를 사용하여 초기화를 하지 않아도 자동 롤백되어 테스트가 통과되었다.

또한 후처리를 하기 위한 @AfterEach를 사용하지 않고, @Transactional을 사용해도 롤백이 되기 때문에 테스트가 통과되었다. 롤백 기능에 대한 편리함 때문에 사용하긴 하지만 실제 배포 시 상황을 고려하며 작성해야 할 것이다.

 

 

 

 

관련 포스팅

테스트 코드를 작성하는 이유 - TestCode (1)

단위 테스트 - TestCode (2)

JUnit 5를 사용한 Java 단위 테스트 - TestCode (3)

TDD - TestCode (4)

BDD - TestCode(5)

통합 테스트(Integration Test) - TestCode(6)

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

Layered Architecture란?

Tech/Test 2023. 7. 2. 13:28
728x90
728x90

 

 

 

 

Architecture - 아키텍쳐

아키텍쳐란 목표하는 대상에 대하여 그 구성과 동작원리, 구성 요소 간의 관계 및 시스템 외부 환경과의 관계 등을 설명하는 설계도 혹은 청사진을 말한다.

 

시스템에서의 아키텍쳐는 시스템의 구조, 행위, 더 많은 뷰를 정의하는 개념적 모형으로 시스템 목적을 달성하기 위해 시스템의 각 컴포넌트가 무엇이며 어떻게 상호작용하는지, 정보가 어떻게 교환되는지를 설명한다.

 

 

 

 

 


 

 

 

 

Layerd Architecture

소프트웨어 시스템의 구성요소 간의 관심사 분리(관심사 별로 계층화)한 아키텍처로, 특정 계층 내의 구성 요소는 해당 계층과 관련된 논리만 처리한다.

 

관심사 분리를 통해 계층의 응집도를 높이고 결합도를 낮출 수 있으며 이를 통한 재사용성과 유지보수의 향상을 기대할 수 있다.

 

서비스의 크기에 따라 3개의 계층이 될 수도 있고, 5개 이상의 계층이 될 수도 있지만, 표준 계층을 예시로 공부했다.

 

Presentation Layer

사용자의 요청, 응답을 처리하는 계층 - Controller

Business Layer

비즈니스 로직을 수행하는 계층 - Service

트랜잭션을 보장해야함

Persistence Layer

DB와의 상호작용을 하는 계층 - Dao / Repository

Database Layer(Infrastructure Layer)

DB 계층 - ORM, JPA 등

 

 

 

각 계층은 상호작용하지만, 특정 비즈니스 로직을 수행하기 위한 작업에 대한 추상화를 형성하여 다른 계층에서 발생하는 로직을 신경쓰지 않아도 된다.

 

예를 들어 Presentation Layer에서는, 사용자에게 특정 형식으로 화면에 해당 정보를 View하기만 하면 된다.

또한 Business Layer는 Persistence Layer에서 데이터를 가져오고, 데이터에 대해 연산을 수행하여 전달만 하면 된다. 데이터의 형식을 지정하는 방법이나, 출처에 대해 신경 쓸 필요가 없다.

 

 

 

 

 

Point

중요 포인트는, 단방향 의존성이다. 각 계층은 하위 레이어에만 의존해야한다.

Presentation Layer에서 직접 Persistence Layer에 엑세스하도록 허용한다면 Business, Presentation Layer 모두에 영향을 미치기 때문에, 결합도가 높아진다. 이는 유지보수 및 확장성 측면에서 굉장히 불리하다.

 

 

 

 

예시

아래의 예시는, Layered Architecture를 사용하여 특정 개인에 대한 정보를 검색하는 Application이다.

 

 

 

 


 

 

 

참조

https://namu.wiki/w/%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98

 

아키텍처 - 나무위키

  자세한 내용은 건축학 문서 를 참고하십시오. 한국에서 건축학은 건축학이라 부르지 굳이 영어로 부르지 않는 경우가 대부분이라, 이보다는 아래의 뜻이 더 많이 쓰인다.

namu.wiki

https://ko.wikipedia.org/wiki/%EC%8B%9C%EC%8A%A4%ED%85%9C_%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98

 

시스템 아키텍처 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 시스템 아키텍처(system Architecture)는 시스템의 구조, 행위, 더 많은 뷰를 정의하는 개념적 모형이다.[1] 시스템 목적을 달성하기 위해 시스템의 각 컴포넌트가 무

ko.wikipedia.org

https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html

 

Software Architecture Patterns

Chapter 1. Layered Architecture The most common architecture pattern is the layered architecture pattern, otherwise known as the n-tier architecture pattern. This pattern is the de facto standard for most … - Selection from Software Architecture Pattern

www.oreilly.com

 

 

 

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

방명록