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() 方法依赖了 countactive 两个字段。

对于 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 示例代码



原始标题:Testing an Abstract Class With JUnit