Programming/Refactoring

단위 테스트 대상 분리하기!

JaeHoney 2023. 10. 23. 08:48

아래는 최범균님의 유튜브를 보고 느낀 점을 나름대로 정리한 것이다.

테스트 불가능한 문제

아래의 코드가 테스트가 불가능한 문제가 있었다고 한다.

ResultBuilder builder = ...;
InputPeriod realPeriod = mapper.selectPeriod(param); // 1. DB에서 읽음
if (realPeriod == null) { // 2. 없으면 다른 값 읽음
    InputPeriod expectedPeriod = mapper.selectExpectedPeriod(otherParam);
    builder.period(expectedPeriod).type(EXPECTED);
} else if (realPeriod.getStart() == null || realPeriod.getEnd() == null) { // 3. 데이터는 있는데 실제 값이 없으면
    builder.period(null).type(EXPECTED);
} else {
    // 4. 데이터의 값이 있고 아래 조건을 충족하면
    if((today.isEqual(real.getStart()) || today.isEqual(real.getEnd())) ||
        (today.isAfter(real.getStart()) && today.isBefore(real.getEnd()))
    ) {
        builder.period(realPeriod).type(SCHEDULED);
    } else { // 5. (4) 조건이 아니면
        builder.period(realPeriod).type(EXPECTED);
    }
}

realPeriod, expectedPeriod, today 값에 따라 사용할 period와 type이 달라지기 때문

단위 테스트

단위 테스트를 방해하는 요소는 아래와 같다.

  • 중간에 섞여 있는 DB 연동
  • ResultBuilder를 만드는 과정

그래서 아래의 로직으로 분리

  • DB에서 InputPeriod를 읽어오는 코드
  • realPeriod, expectedPeriod, today로 사용할 period와 type 구하기
  • 코드 분리를 위한 타입 추가

데이터 타입 추가

public class InputPeriods {
    private InputPeriod real;
    private InputPeriod expected;
}

DB에서 읽어오는 코드 분리

DB에서 읽어오는 부분을 아래와 같이 분리했다.

private InputPeriods selectPeriod(Param param, OtherParam otherParam) {
    InputPeriod realPeriod = mapper.selectPeriod(param);
    if(realPeriod != null) return new InputPeriods(realPeriod, null);
    InputPeriod expectedPeriod = mapper.selectExpectedPeriod(otherParam);
    return new InputPeriods(null, expectedPeriod);
}

결과적으로 코드가 아래와 같이 변경되었다.

ResultBuilder builder = ...;
// InputPeriod realPeriod = mapper.selectPeriod(param);
InputPeriods periods = selectPeriod(param, otherParam);
if (realPeriod == null) {
    // InputPeriod expectedPeriod = mapper.selectExpectedPeriod(otherParam);
    // builder.period(expectedPeriod).type(EXPECTED);
    builder.period(periods.getExpected()).type(EXPECTED);
} else if (realPeriod.getStart() == null || realPeriod.getEnd() == null) {
    builder.period(null).type(EXPECTED);
} else {
    if((today.isEqual(real.getStart()) || today.isEqual(real.getEnd())) ||
        (today.isAfter(real.getStart()) && today.isBefore(real.getEnd()))
    ) {
        builder.period(realPeriod).type(SCHEDULED);
    } else {
        builder.period(realPeriod).type(EXPECTED);
    }
}

period, type 구하는 로직을 InputPeriods 클래스로 이동

period, type을 build하는 과정을 삭제하기 위해 로직을 Data 객체에 위임했다.

public class InputPeriods {
    private InputPeriod real;
    private InputPeriod expected;

    public InputPeriod getPeriod() {
        if(real == null) return expected;
        if(real.getStart() == null || real.getEnd() == null) return null;
        return expected;
    }

    public PeriodType getType(LocalDate today) {
        if(real == null) return EXPECTED;
        if(realPeriod.getStart() == null || realPeriod.getEnd() == null) return EXPECTED;
        if((today.isEqual(real.getStart()) || today.isEqual(real.getEnd())) || 
            (today.isAfter(real.getStart()) && today.isBefore(real.getEnd()))
        ) return SCHEDULED;
        return EXPECTED;
    }
}

여기서 LocalDate로 today라는 파라미터를 내부에서 수행하지 않는 이유는 제어가 가능해야 하기 때문이다.

변경 후

결과적으로 아래와 같이 깔끔한 코드가 되었다.

ResultBuilder builder = ...;
InputPeriods periods = selectPeriod(param, otherParam);
builder.period(periods.getPeriod()).type(periods.getType(today));

결과

  • 코드가 단순해졌음
  • 로직에 대한 단위 테스트가 쉬워짐
    • DB 연동 필요 없음 (Mocking 필요 X)
    • 실제 테스트하고 싶은 대상에만 초점
  • 로직 변경/기능/추가가 쉬워짐
    • 관련 로직이 한 곳에 모임 -> 변경할 곳도 한 곳으로 모임
    • 데이터가 한 클래스에 있음 -> 데이터(expectedPeriod, realPeriod)와 관련된 기능 추가는 해당 클래스
    • 코드가 흩어지는 문제를 방지

마무리

나도 개발하면서 영속성 계층에서 (저 만큼 복잡하지는 않았지만) 로직을 수행해서 가져와야 하는 경우가 일부 있었다.

  • ALTER(Utf8mb4 적용)를 할 수 없어서 임시 테이블에 데이터가 존재 시 해당 테이블에서 데이터를 꺼내와야 했던 문제

나도 JPA Entity에서 로직을 구현해서 테스트를 했었다. 내가 한건 처음 코드를 작성한 것이었고 기존의 코드를 리팩토링하는 것은 조금 더 어려운 것 같다.

로직이 수행이 많아서 단위 테스트를 작성하기가 어렵다면 수행할 로직에서 테스트할 대상이 정말 1가지가 맞는 지 확인하고, 2가지 이상이라면 테스트할 대상을 분리해보자.

  • SQL을 실행하는 부분에서는 가능한 로직을 담지 말고 저장소의 역할만 해야 한다.

참고