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 클래스로는 아래와 같은 작업들도 할 수 있다.
- private 필드 접근하기
ReflectionTestUtils.getField(employee, "annualSalary")
와 같이 사용하면 employee 클래스에 private으로 설정된 'annualSalary'에 접근이 가능하다.
- private 필드 setter 접근하기
ReflectionTestUtils.setField(employee, "annualSalary", 6000)
와 같이 사용하면 특정 private 필드의 값을 변경할 수 있다.
- 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 등)
참고
'개발 > 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 |