JUnit 更改 class 或集成测试中的方法行为

JUnit change class or method behaviour in integration tests

我有一个 java 应用程序(内部没有 Spring),我想通过集成测试对其进行测试。

我的主要用例是使用指定输入在数据库上执行某些操作并向两种不同的服务(一种 SOAP 和一种 REST)发送一些请求的主要功能。

现在我有一个工作的 JUnit 配置(在单元测试和集成测试中分开)+ io.fabric8:docker-maven-plugin 在集成测试期间使用数据库的 docker 图像。

我想做的是为这两个服务添加一个模拟,特别是用于直接调用外部服务的方法。

最大的问题是我有这个结构:

class A{
    Result mainFunction(Request r){
        ....
        B b = new B(params);
        b.logEvent(someParameters)
        ....
    }
}
class B{
    int logEvent(Object someParameters){
        ....
        NotifierHandler nh = new NotifierHandler(param1);
        nh.sendNotification(json);
        ....
    }
}

我有:

class NotifierHandler{
    String sendNotification(Json j){
        ...
        [call to REST service with some parameters]
        ...
        ...
        [call to SOAP service with some parameters]
        ...
    }
}

我需要什么:调用A.mainFunction(r),在测试环境中,用 FakeNotifierHandler 替换了 NotifierHandler and/or 改变方法的行为sendNotification().

实际问题:现在使用 Mockito 和 PowerMock 我遇到的问题是我无法全局更改并直接更改 class NotifierHandler 和 FakeNotifierHandler。同样尝试更改方法的行为。

特别是,我需要创建一个

class FakeNotifierHandler{
    String sendNotification(Json j){
        ...
        [save on an HashMap what I should send to the REST service]
        ...
        ...
        [save on another HashMap what I should send to the SOAP service]
        ...
    }
}

阅读我尝试过的所有示例,我只看到了简单的示例,这些示例更改了一种方法的 return 值,而不是另一种方法和另一种方法使用的一种方法的行为 class m 用作集成测试的起点。

注意:可能有一种快速的方法可以做到这一点,但我对这种类型的测试(Mockito、PowerMock 等)非常陌生,而且我没有找到这种特殊情况的例子。

编辑:与 不同,因为我需要更改方法的行为,而不仅仅是 return 值。

非常感谢

我认为你的情况主要令人头疼的是 class ABNotifierHandler 之间存在紧密耦合的依赖关系。我将从以下内容开始:

class A {
  private B b;

  public A(B b) {
    this.b = b;
  }

  Result mainFunction(Request r){
      ....
      b.logEvent(someParameters)
      ....
  }
}
class B {
  private NotifierHandler nh;

  public B(NotifierHandler nh) {
    this.nh = nh;
  }

  int logEvent(Object someParameters){
      ....
      nh.sendNotification(json);
      ....
  }
}

将 NotifierHanlder 设为接口:

interface NotifierHandler {
  String sendNotification(String json);
}

并进行两种实现:一种用于真实的用例,另一种是你可以存根的假的:

class FakeNotifierHandler implements NotifierHandler {

  @Override
  public String sendNotification(String json) {
    // whatever is needed for you
  }
}

在你的测试中注入 FakeNotifierHandler

希望对你有所帮助。

我找到了一个非常有效且非常简单的解决方案!

解决方案是PowerMock (https://github.com/powermock/powermock) and in particular replace the creation of an instance of a class with another: https://github.com/powermock/powermock/wiki/mockito#how-to-mock-construction-of-new-objects

我的项目中只有一个问题,就是 JUnit 5。PowerMock 支持 JUnit 4,因此,只有解决方案的一些测试才使用它。 为此,需要替换

import org.junit.jupiter.api.Test;

import org.junit.Test;

为了使用“whenNew()”方法,我扩展了测试中必须替换的 class 并且我只覆盖了必要的方法用于集成测试。 这个解决方案的最大好处是我的代码没有受到影响,我也可以在旧代码上使用这种方法,而不会有在代码重构期间引入回归的风险。

关于集成测试的代码,举个例子:

import org.junit.jupiter.api.DisplayName;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PowerMockIgnore({"javax.crypto.*" }) // https://github.com/powermock/powermock/issues/294
@PrepareForTest(LegacyCoreNetworkClassPlg.class) // it is the class that contains the "new SOAPCallHelper(..)" code that I want to intercept and replace with a stub
public class ITestExample extends InitTestSuite {
    @Test
    @DisplayName("Test the update of a document status")
    public void iTestStubLegacyNetworkCall() throws Exception {

        // I'm using JUnit 4
        // I need to call @BeforeAll defined in InitTestSuite.init();
        // that works only with JUnit 5
        init();

        LOG.debug("IN stubbing...");
        SOAPCallHelperStub stub = new SOAPCallHelperStub("empty");
        PowerMockito.whenNew(SOAPCallHelper.class).withAnyArguments().thenReturn(stub);
        LOG.debug("OUT stubbing!!!");

        LOG.debug("IN iTestStubLegacyNetworkCall");
        ...
        // Here I can create any instance of every class, but when an instance of 
        // LegacyCoreNetworkClassPlg.class is created directly or indirectly, PowerMock
        // is checking it and when LegacyCoreNetworkClassPlg.class will create a new
        // instance of SOAPCallHelper it will change it with the 
        // SOAPCallHelperStub instance.
        ...
        LOG.debug("OUT iTestStubLegacyNetworkCall");
    }
}

这里配置pom.xml

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <junit.jupiter.version>5.5.2</junit.jupiter.version>
    <junit.vintage.version>5.5.2</junit.vintage.version>
    <junit.platform.version>1.3.2</junit.platform.version>
    <junit.platform.engine.version>1.5.2</junit.platform.engine.version>
    <powermock.version>2.0.2</powermock.version>

    <!-- FOR TEST -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>${junit.jupiter.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <scope>test</scope>
    </dependency>
    <!-- Only required to run tests in an IDE that bundles an older version -->
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-launcher</artifactId>
        <version>${junit.platform.version}</version>
        <scope>test</scope>
    </dependency>
    <!-- Only required to run tests in an IDE that bundles an older version -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>${junit.jupiter.version}</version>
        <scope>test</scope>
    </dependency>
    <!-- Only required to run tests in an IDE that bundles an older version -->
    <dependency>
        <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
        <version>${junit.vintage.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-engine</artifactId>
        <version>${junit.platform.engine.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-params</artifactId>
        <version>${junit.vintage.version}</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-module-junit4</artifactId>
        <version>${powermock.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-api-mockito2</artifactId>
        <version>${powermock.version}</version>
        <scope>test</scope>
    </dependency>