개발/Spring

ReflectionTestUtils를 이용한 private 필드와 메서드 단위테스트

nova_dev 2024. 12. 2. 23:22
반응형

ReflectionTestUtils를 이용한 private 필드와 메서드 단위테스트

개요

ReflectionTestUtils는 SpringFramework의 Test Context 에서 제공하는 유틸로, 프라이빗 필드나 메서드를 테스트할 수 있다.

  • 비공개 필드 값 설정
  • 비공개 필드 값 접근
  • 비공개 메서드 호출

테스트를 작성하다보면 종종 단위테스트에서 위와 같은 접근이 필요할 때가 생기는데, 이 때 해당 클래스의 Private 메서드를 열지 않고 ReflectionTestUtils를 사용하면 접근할 수 있다.

예시 코드

예시 코드를 위해 직원 클래스를 만들어보자. 이 직원을 고용할 때 계약서에 이름과 계약 연봉을 작성한다.
이름은 변경 불가능하니 private final을 붙이고, 연봉(annualSalary)는 매년 변경 가능하니 private int 필드로 만들었다.

import lombok.Builder;
import lombok.Getter;

@Builder(access = lombok.AccessLevel.PRIVATE)
public class Employee {
    @Getter
    private final String name;

    private int annualSalary;

    public static Employee hire(String name, int salary) {
        return Employee.builder()
                .name(name)
                .annualSalary(salary)
                .build();
    }
}

직원의 연봉협상을 하는 소소한 망상을 하며 메서드를 작성한다.
내 연봉과 월급은 아무나 볼 수 없으니 getter도 막아둔다.

    private void setAnnualSalary(int annualSalary) {
        if (annualSalary < 0) {
            throw new IllegalArgumentException("연봉은 0보다 작을 수 없습니다.");
        }
        this.annualSalary = annualSalary;
    }

    private int getAnnualSalary() {
        return annualSalary;
    }

    private int calculateMonthlySalary() {
        return annualSalary / 12;
    }

위와 같은 Employee 클래스가 있을 때, 테스트 코드를 작성한다면 private 필드에 접근해야 하므로 테스트코드에서 접근이 안된다. 이럴 경우 사용할 수 있는게 'ReflectionTestUtils'이다.

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.util.ReflectionTestUtils;

@ExtendWith(SpringExtension.class)
class EmployeeReflectionTest {

    private Employee employee;

    @BeforeEach
    void setUp() {
        employee = Employee.hire("김철수", 5000);
    }

    @BeforeEach
    void setUp() {
        employee = Employee.hire("김철수", 5000);
    }

    @Test
    void 연봉을_변경한다() {
        Assertions.assertEquals(5000, ReflectionTestUtils.getField(employee, "annualSalary"));

        ReflectionTestUtils.setField(employee, "annualSalary", 6000);
        Assertions.assertEquals(6000, ReflectionTestUtils.getField(employee, "annualSalary"));
    }

    @Test
    void 월급을_확인한다() {
        int monthlySalary = ReflectionTestUtils.invokeMethod(employee, "calculateMonthlySalary");
        Assertions.assertEquals(391, monthlySalary);
    }
}

ReflectionTestUtils 클래스로는 아래와 같은 작업들도 할 수 있다.

  1. private 필드 접근하기

ReflectionTestUtils.getField(employee, "annualSalary")와 같이 사용하면 employee 클래스에 private으로 설정된 'annualSalary'에 접근이 가능하다.

  1. private 필드 setter 접근하기

ReflectionTestUtils.setField(employee, "annualSalary", 6000) 와 같이 사용하면 특정 private 필드의 값을 변경할 수 있다.

  1. private 메서드 접근하기
    ReflectionTestUtils.invokeMethod(employee, "calculateMonthlySalary")와 같이 사용하면 특정 private 메서드를 사용할 수 있다.

만약 static 클래스나 메서드에 접근하고 싶다면 어떻게 해야할까?

지금 연봉은 세금이 없으므로 소득세 계산을 해주는 enum class를 추가해보자.

public enum TaxRate {
    DEFAULT(0, 1200, 0.06, 0),
    OVER_1400(1400, 5000, 0.15, 84),
    OVER_5000(5000, 8800, 0.24, 624),
    OVER_8800(8800, 15000, 0.35, 1536),
    OVER_15000(15000, 30000, 0.38, 3706);

    private final int minAnnualSalary;

    private final int maxAnnualSalary;
    private final double rate;
    private final int defaultTax;

    TaxRate(int minAnnualSalary, int maxAnnualSalary, double rate, int defaultTax) {
        this.minAnnualSalary = minAnnualSalary;
        this.maxAnnualSalary = maxAnnualSalary;
        this.rate = rate;
        this.defaultTax = defaultTax;
    }

    private static int calculateAnnualTax(int annualSalary) {
        for (TaxRate taxRate : TaxRate.values()) {
            if (annualSalary > taxRate.minAnnualSalary && annualSalary <= taxRate.maxAnnualSalary) {
                return taxRate.defaultTax + (int) ((annualSalary - taxRate.minAnnualSalary) * taxRate.rate);
            }
        }
        return 0;
    }

    public static int calculateMonthlySalary(int annualSalary) {
        return (annualSalary - calculateAnnualTax(annualSalary)) / 12;
    }
}

2024년을 기준으로 소득세 구간별 세금을 가져왔다.
만약 철수의 연봉이 5000만원이라면, 철수의 구간은 1400초과 5000이하 소득세 구간이라 0.15%의 소득세를 내야 한다.
이 때 계산은 (5000-1400)*0.15 + 84 와 같은 연 소득세를 내야 한다.

이런 계산을 해주는 calculateAnnualTax 메서드를 private으로 구현하고, 월 소득을 구해주는 것은 외부 public으로 노출해둔다.
이제 해당 클래스를 사용해서 소득세를 제외한 월급을 계산하는 코드를 추가해본다.

public class Employee {
...
    private int calculateMonthlySalary() {
        return annualSalary / 12;
    }

    private int calculateMonthlySalaryWithTax() {
        return TaxRate.calculateMonthlySalary(annualSalary);
    }
}

이에 대한 테스트 코드도 작성한다.

    @Test
    void 연봉의_연소득세를_확인한다() {
        int tax = ReflectionTestUtils.invokeMethod(TaxRate.class, "calculateAnnualTax", 5000);
        Assertions.assertEquals(624, tax);
    }

    @Test
    void 소득세를뺀_월급을_확인한다() {
        int monthlySalaryWithTax = ReflectionTestUtils.invokeMethod(employee, "calculateMonthlySalaryWithTax");
        Assertions.assertEquals(364, monthlySalaryWithTax);
    }

위와 같이, static 클래스나 static 메서드의 테스트도 할 수 있고, 메서드에 특정 변수 값을 넣어주는 것도 가능하다.

ReflectionTestUtils.invokeMethod(TaxRate.class, "calculateAnnualTax", 5000)

참고로 invoke해서 넣을 수 있는 것은 변수에는 값 뿐만 아니라 클래스도 가능하다. (ex. mock으로 만들어둔 repository 등)

참고

https://www.baeldung.com/spring-reflection-test-utils

반응형

'개발 > Spring' 카테고리의 다른 글

[Spring] Spring Boot 버전별 차이점  (0) 2022.10.22
[Spring] Spring Boot v2.7  (0) 2022.10.18
[Spring] Spring Boot v2.6  (0) 2022.10.17
[Spring] Spring Boot v2.4  (0) 2022.10.15
[Spring] Spring Boot v2.3  (0) 2022.10.14