1. 概述

在这个教程中,我们将分析抽象类中带有非抽象方法的各种用例及其可能的替代解决方案。请注意,通常情况下,测试抽象类应通过具体实现的公共API进行,除非你确定自己的做法,否则不要使用下面介绍的方法。

2. Maven 依赖

让我们从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中央仓库

注意:Powermock并不完全支持JUnit5。实际上,仅在第5部分提供的一个示例中使用了powermock-module-junit4。此外,mockito-core的最新兼容版本是3.3.0

3. 独立的非抽象方法

考虑一个抽象类具有公开非抽象方法的情况:

public abstract class AbstractIndependent {
    public abstract int abstractFunc();

    public String defaultImpl() {
        return "DEFAULT-1";
    }
}

我们想要测试defaultImpl()方法,有以下两种可能的解决方案:使用具体类或使用Mockito。

3.1. 使用具体类

创建一个扩展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);
}

这种方案的缺点是需要为所有抽象方法创建具有模拟实现的具体类。

3.2. 使用Mockito

另一种选择是使用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确保在方法被调用时使用实际代码。

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());
}

在这里,我们为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();
    }
}

在这个例子中,defaultImpl()方法调用了私有方法getCurrentDateTime()。这个私有方法会在运行时获取当前时间,我们在单元测试中应避免这样做。

为了模拟这个私有方法的标准行为,由于Mockito无法控制私有方法,我们不能直接使用它。

相反,我们需要使用PowerMock请注意,这个示例只适用于JUnit 4,因为对这个依赖的支持在JUnit 5中不可用):

@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模拟私有方法getCurrentDateTime()。PowerMock会使用反射找到它,因为它对外部不可访问。

因此,当我们调用defaultImpl()时,将调用为私有方法创建的模拟,而不是实际方法。

6. 访问实例字段的非抽象方法

抽象类可以使用类字段实现内部状态,字段的值可能对正在测试的方法产生重大影响。

如果字段是公共或受保护的,我们可以在测试方法中轻松访问它。

但如果它是私有的,我们需要使用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()方法在返回前使用了实例级字段countactive

在测试testFunc()时,我们可以通过使用Mockito创建的实例改变count字段的值。

另一方面,要测试私有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类控制对象的内部状态。

active字段的值设置为true

7. 总结

在这篇教程中,我们探讨了多个例子,涵盖了大量用例。根据所遵循的设计,我们可以在更多场景中使用抽象类。

同样,为抽象类方法编写单元测试与为普通类和方法一样重要。我们可以使用不同的技术和可用的测试支持库来测试它们。

完整的源代码可在GitHub上找到。