Testing an Abstract Class With JUnit – 用JUnit测试一个抽象类

最后修改: 2018年 8月 12日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll analyze various use cases and possible alternative solutions to unit-testing of abstract classes with non-abstract methods.

在本教程中,我们将分析各种用例以及对具有非抽象方法的抽象类进行单元测试的可能替代解决方案。

Note that testing abstract classes should almost always go through the public API of the concrete implementations, so don’t apply the below techniques unless you’re sure what you’re doing.

请注意,测试抽象类几乎总是要通过具体实现的公共API,所以除非你确信自己在做什么,否则不要应用下面的技术。

2. Maven Dependencies

2.Maven的依赖性

Let’s start with Maven dependencies:

让我们从Maven的依赖性开始。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.8.1</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>1.7.4</version>
    <scope>test</scope>
</dependency>

You can find the latest versions of these libraries on Maven Central.

你可以在Maven Central.

Powermock isn’t fully supported for Junit5. Also, powermock-module-junit4 is used only for one example presented in section 5.

Powermock并不完全支持Junit5。另外,powermock-module-junit4只用于第5节中的一个例子。

3. Independent Non-Abstract Method

3.独立的非抽象方法

Let’s consider a case when we have an abstract class with a public non-abstract method:

让我们考虑这样一种情况:我们有一个抽象类,有一个公共的非抽象方法。

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

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

We want to test the method defaultImpl(), and we have two possible solutions – using a concrete class, or using Mockito.

我们想测试方法defaultImpl(),我们有两种可能的解决方案–使用一个具体的类,或者使用Mockito。

3.1. Using a Concrete Class

3.1.使用一个具体的类

Create a concrete class which extends AbstractIndependent class, and use it to test the method:

创建一个扩展AbstractIndependentclass的具体类,并使用它来测试方法。

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

The drawback of this solution is the need to create the concrete class with dummy implementations of all abstract methods.

这种解决方案的缺点是需要用所有抽象方法的假实现来创建具体类。

3.2. Using Mockito

3.2.使用Mockito

Alternatively, we can use Mockito to create a mock:

另外,我们可以使用Mockito来创建一个模拟。

@Test
public void givenNonAbstractMethod_whenMockitoMock_testCorrectBehaviour() {
    AbstractIndependent absCls = Mockito.mock(
      AbstractIndependent.class, 
      Mockito.CALLS_REAL_METHODS);
 
    assertEquals("DEFAULT-1", absCls.defaultImpl());
}

The most important part here is the preparation of the mock to use the real code when a method is invoked using Mockito.CALLS_REAL_METHODS.

这里最重要的部分是使用Mockito.CALLS_REAL_METHODS为mock做准备,以便在方法被调用时使用真实代码。

4. Abstract Method Called From Non-Abstract Method

4.从非抽象方法中调用的抽象方法

In this case, the non-abstract method defines the global execution flow, while the abstract method can be written in different ways depending upon the use case:

在这种情况下,非抽象方法定义了全局执行流程,而抽象方法可以根据用例以不同方式编写。

public abstract class AbstractMethodCalling {

    public abstract String abstractFunc();

    public String defaultImpl() {
        String res = abstractFunc();
        return (res == null) ? "Default" : (res + " Default");
    }
}

To test this code, we can use the same two approaches as before – either create a concrete class or use Mockito to create a mock:

为了测试这段代码,我们可以使用与之前相同的两种方法–要么创建一个具体的类,要么使用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());
}

Here, the abstractFunc() is stubbed with the return value we prefer for the test. This means that when we call the non-abstract method defaultImpl(), it will use this stub.

在这里,abstractFunc()被存根为我们喜欢的测试的返回值。这意味着,当我们调用非抽象方法defaultImpl()时,它将使用这个存根。

5. Non-Abstract Method With Test Obstruction

5.有测试障碍的非抽象方法

In some scenarios, the method we want to test calls a private method which contains a test obstruction.

在某些情况下,我们要测试的方法会调用一个包含测试障碍的私有方法。

We need to bypass the obstructing test method before testing the target method:

在测试目标方法之前,我们需要绕过阻碍性的测试方法。

public abstract class AbstractPrivateMethods {

    public abstract int abstractFunc();

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

    private String getCurrentDateTime() {
        return LocalDateTime.now().toString();
    }
}

In this example, the defaultImpl() method calls the private method getCurrentDateTime(). This private method gets the current time at runtime, which should be avoided in our unit tests.

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

Now, to mock the standard behavior of this private method, we cannot even use Mockito because it cannot control private methods.

现在,为了模拟这个私有方法的标准行为,我们甚至不能使用Mockito,因为它不能控制私有方法。

Instead, we need to use PowerMock (note that this example works only with JUnit 4 because support for this dependency isn’t available for JUnit 5):

相反,我们需要使用PowerMockn注意,本示例仅适用于 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);
    }
}

Important bits in this example:

这个例子中的重要位。

  • @RunWith defines PowerMock as the runner for the test
  • @PrepareForTest(class) tells PowerMock to prepare the class for later processing

Interestingly, we’re asking PowerMock to stub the private method getCurrentDateTime(). PowerMock will use reflection to find it because it’s not accessible from outside.

有趣的是,我们要求PowerMock为私有方法getCurrentDateTime()提供存根。PowerMock将使用反射来找到它,因为它不能从外部访问。

Sowhen we call defaultImpl(), the stub created for a private method will be invoked instead of the actual method.

所以当我们调用defaultImpl()时,为一个私有方法创建的存根将被调用,而不是实际的方法。

6. Non-Abstract Method Which Accesses Instance Fields

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

Abstract classes can have an internal state implemented with class fields. The value of the fields could have a significant effect on the method getting tested.

抽象类可以有一个用类字段实现的内部状态。字段的值可能会对被测试的方法产生重大影响。

If a field is public or protected, we can easily access it from the test method.

如果一个字段是公开的或受保护的,我们可以很容易地从测试方法中访问它。

But if it’s private, we have to use PowerMockito:

但如果它是私有的,我们必须使用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";
    }
}

Here, the testFunc() method is using instance-level fields count and active before it returns.

这里,testFunc()方法在返回之前使用了实例级字段countactive

When testing testFunc(), we can change the value of the count field by accessing instance created using Mockito. 

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

On the other hand, to test the behavior with the private active field, we’ll again have to use PowerMockito, and its Whitebox class:

另一方面,为了测试私有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());
}

We’re creating a stub class using PowerMockito.mock(), and we’re using Whitebox class to control object’s internal state.

我们使用PowerMockito.mock()创建一个存根类,我们使用Whitebox类来控制对象的内部状态。

The value of the active field is changed to true.

active字段的值被改变为true

7. Conclusion

7.结论

In this tutorial, we’ve seen multiple examples which cover a lot of use cases. We can use abstract classes in many more scenarios depending upon the design followed.

在本教程中,我们已经看到了多个例子,涵盖了很多用例。我们可以在更多的场景中使用抽象类,这取决于所采用的设计。

Also, writing unit tests for abstract class methods is as important as for normal classes and methods. We can test each of them using different techniques or different test support libraries available.

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

The full source code is available over on GitHub.

完整的源代码可在GitHub上获得