1. 概述
在本教程中,我们将深入探讨如何对包含非抽象方法的抽象类进行单元测试。抽象类虽然不能直接实例化,但其内部实现的逻辑往往也需要被验证。
⚠️ 注意:通常情况下,测试抽象类的最佳实践是通过其具体子类的公共 API 来完成。除非你非常清楚自己在做什么,否则不建议直接测试抽象类本身。
2. Maven 依赖
我们先从项目依赖开始:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</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>
📌 你可以在 Maven Central 上找到这些库的最新版本。
⚠️ 注意:PowerMock 并不完全支持 JUnit 5。示例中使用的 powermock-module-junit4
仅用于第 5 节中的一个示例。此外,与 PowerMock 兼容的 mockito-core
最高版本为 3.3.0
。
3. 独立的非抽象方法
考虑这样一个抽象类,它包含一个非抽象的公共方法:
public abstract class AbstractIndependent {
public abstract int abstractFunc();
public String defaultImpl() {
return "DEFAULT-1";
}
}
我们希望测试 defaultImpl()
方法,有两种常见做法:
3.1. 使用具体子类
最简单的方式是创建一个子类来继承该抽象类,并实现所有抽象方法:
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);
}
✅ 优点:逻辑清晰,易于维护。
❌ 缺点:需要为每个抽象方法提供“占位”实现,代码冗余。
3.2. 使用 Mockito
另一种方式是使用 Mockito 创建 mock 对象,并让其调用真实方法:
@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
,这会让 mock 对象在调用非抽象方法时执行真实逻辑。
4. 非抽象方法调用抽象方法
有时候,非抽象方法中会调用抽象方法,抽象方法的实现由子类决定:
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());
}
📌 在这个例子中,我们通过 when(...).thenReturn(...)
模拟了 abstractFunc()
的返回值,从而控制 defaultImpl()
的行为。
5. 包含私有方法调用的非抽象方法
有时候,我们要测试的方法会调用一个私有方法,而这个私有方法又包含一些测试障碍(比如时间获取):
public abstract class AbstractPrivateMethods {
public abstract int abstractFunc();
public String defaultImpl() {
return getCurrentDateTime() + "DEFAULT-1";
}
private String getCurrentDateTime() {
return LocalDateTime.now().toString();
}
}
📌 这里的 getCurrentDateTime()
是私有方法,且返回值会变化,影响测试结果。
Mockito 无法处理私有方法,因此我们需要借助 PowerMock(⚠️ 仅支持 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(PowerMockRunner.class)
:启用 PowerMock 测试运行器@PrepareForTest
:告诉 PowerMock 要处理的类doReturn(...).when(..., "method")
:通过反射调用私有方法并模拟返回值
6. 访问实例字段的非抽象方法
抽象类也可以拥有实例字段,这些字段可能会影响方法的行为:
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
两个字段。
对于 protected
字段,可以通过子类或 mock 直接访问;但对于 private
字段,需要使用 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());
}
📌 Whitebox.setInternalState()
可以直接修改对象的私有字段值,非常实用。
7. 总结
在这篇文章中,我们讨论了以下几种测试抽象类的典型场景:
✅ 独立非抽象方法
✅ 非抽象方法调用抽象方法
✅ 私有方法干扰测试
✅ 实例字段影响方法行为
虽然抽象类的测试比普通类复杂一些,但借助 Mockito 和 PowerMock 等工具,我们依然可以写出高质量的单元测试。
📌 完整代码可参考:GitHub 示例代码