이 튜토리얼에서는 비추상 메소드를 사용하여 추상 클래스의 단위 테스트에 대한 다양한 사용 사례와 가능한 대체 솔루션을 분석합니다.
추상 클래스 테스트는 거의 항상 구체적인 구현의 공개 API를 거쳐야 하므로 수행 중인 작업이 확실하지 않은 경우 아래 기술을 적용하지 마십시오.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.8.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.7.4</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
public non-abstract 메소드가 있는 추상 클래스가 있는 경우를 고려해 보겠습니다.
public abstract class AbstractIndependent {
public abstract int abstractFunc();
public String defaultImpl() {
return "DEFAULT-1";
}
}
우리는 defaultImpl() 메소드를 테스트하고 싶고 구체적인 클래스를 사용하거나 Mockito를 사용하는 두 가지 가능한 솔루션이 있습니다.
AbstractIndependent 클래스를 확장하는 구체적인 클래스를 만들고 이를 사용하여 메서드를 테스트합니다.
public class ConcreteImpl extends AbstractIndependent {
@Override
public int abstractFunc() {
return 4;
}
}
@Test
public void givenNonAbstractMethod_whenConcreteImpl_testCorrectBehaviour() {
ConcreteImpl conClass = new ConcreteImpl();
String actual = conClass.defaultImpl();
assertEquals("DEFAULT-1", actual);
}
이 솔루션의 단점은 모든 추상 메서드의 더미 구현을 사용하여 구체적인 클래스를 만들어야 한다는 것입니다.
Mockito를 사용하여 모의 모형을 만들 수 있습니다.
@Test
public void givenNonAbstractMethod_whenMockitoMock_testCorrectBehaviour() {
AbstractIndependent absCls = Mockito.mock(
AbstractIndependent.class,
Mockito.CALLS_REAL_METHODS);
assertEquals("DEFAULT-1", absCls.defaultImpl());
}
여기서 가장 중요한 부분은 Mockito.CALLS_REAL_METHODS를 사용하여 메서드가 호출될 때 실제 코드를 사용하기 위한 모의 코드를 준비하는 것입니다.
이 경우 비추상 메서드는 전역 실행 흐름을 정의하는 반면 추상 메서드는 사용 사례에 따라 다른 방식으로 작성될 수 있습니다.
public abstract class AbstractMethodCalling {
public abstract String abstractFunc();
public String defaultImpl() {
String res = abstractFunc();
return (res == null) ? "Default" : (res + " Default");
}
}
이 코드를 테스트하기 위해 이전과 동일한 두 가지 접근 방식을 사용할 수 있습니다. 즉, 구체적인 클래스를 생성하거나 Mockito를 사용하여 모의 클래스를 생성하는 것입니다.
@Test
public void givenDefaultImpl_whenMockAbstractFunc_thenExpectedBehaviour() {
AbstractMethodCalling cls = Mockito.mock(AbstractMethodCalling.class);
Mockito.when(cls.abstractFunc())
.thenReturn("Abstract");
Mockito.doCallRealMethod()
.when(cls)
.defaultImpl();
assertEquals("Abstract Default", cls.defaultImpl());
}
여기서 abstractFunc()는 테스트에서 선호하는 반환 값으로 스텁됩니다. 이는 비추상 메소드인 defaultImpl()을 호출할 때 이 스텁을 사용한다는 것을 의미합니다.
일부 시나리오에서는 테스트하려는 메서드가 테스트 방해물이 포함된 전용 메서드를 호출합니다. 타겟 메서드를 테스트하기 전에 방해하는 테스트 메서드를 우회해야 합니다.
public abstract class AbstractPrivateMethods {
public abstract int abstractFunc();
public String defaultImpl() {
return getCurrentDateTime() + "DEFAULT-1";
}
private String getCurrentDateTime() {
return LocalDateTime.now().toString();
}
}
이 예에서 defaultImpl() 메소드는 개인 메소드 getCurrentDateTime()을 호출합니다. 이 비공개 메서드는 런타임 시 현재 시간을 가져오며, 이는 단위 테스트에서 피해야 합니다.
이제 이 프라이빗 메서드의 표준 동작을 조롱하기 위해 Mockito를 사용할 수도 없습니다. 프라이빗 메서드를 제어할 수 없기 때문입니다.
대신 PowerMock을 사용해야 합니다(이 종속성에 대한 지원은 JUnit 5에서는 지원되지 않으므로 이 예제는 JUnit 4에서만 작동합니다).
@RunWith(PowerMockRunner.class)
@PrepareForTest(AbstractPrivateMethods.class)
public class AbstractPrivateMethodsUnitTest {
@Test
public void whenMockPrivateMethod_thenVerifyBehaviour() {
AbstractPrivateMethods mockClass = PowerMockito.mock(AbstractPrivateMethods.class);
PowerMockito.doCallRealMethod()
.when(mockClass)
.defaultImpl();
String dateTime = LocalDateTime.now().toString();
PowerMockito.doReturn(dateTime).when(mockClass, "getCurrentDateTime");
String actual = mockClass.defaultImpl();
assertEquals(dateTime + "DEFAULT-1", actual);
}
}
이 예에서 중요한 부분은 다음과 같습니다.
@RunWith는 PowerMock을 테스트 실행자로 정의합니다.@PrepareForTest(class)는 PowerMock에게 나중에 처리할 클래스를 준비하라고 지시합니다.흥미롭게도 우리는 PowerMock에게 private 메소드 getCurrentDateTime()을 스텁하도록 요청하고 있습니다. PowerMock은 외부에서 접근할 수 없기 때문에 리플렉션을 사용하여 이를 찾습니다.
따라서 defaultImpl()을 호출하면 실제 메소드 대신 private 메소드용으로 생성된 스텁이 호출됩니다.
추상 클래스는 클래스 필드로 구현된 내부 상태를 가질 수 있습니다. 필드의 값은 테스트되는 메서드에 중요한 영향을 미칠 수 있습니다. 필드가 public이거나 protected인 경우 테스트 메서드에서 쉽게 액세스할 수 있습니다. 하지만 private라면 PowerMockito를 사용해야 합니다.
public abstract class AbstractInstanceFields {
protected int count;
private boolean active = false;
public abstract int abstractFunc();
public String testFunc() {
if (count > 5) {
return "Overflow";
}
return active ? "Added" : "Blocked";
}
}
여기서 testFunc() 메서드는 반환되기 전에 인스턴스 수준 필드 count 및 active를 사용합니다.
testFunc()를 테스트할 때 Mockito를 사용하여 생성된 인스턴스에 액세스하여 count 필드의 값을 변경할 수 있습니다.
반면에 private active 필드의 동작을 테스트하려면 PowerMockito와 해당 Whitebox 클래스를 다시 사용해야 합니다.
@Test
public void whenPowerMockitoAndActiveFieldTrue_thenCorrectBehaviour() {
AbstractInstanceFields instClass = PowerMockito.mock(AbstractInstanceFields.class);
PowerMockito.doCallRealMethod()
.when(instClass)
.testFunc();
Whitebox.setInternalState(instClass, "active", true);
assertEquals("Added", instClass.testFunc());
}
PowerMockito.mock()을 사용하여 스텁 클래스를 생성하고 있으며 Whitebox 클래스를 사용하여 객체의 내부 상태를 제어하고 있습니다. 활성 필드의 값이 true로 변경됩니다.