【问题标题】:JUnit change class or method behaviour in integration testsJUnit 在集成测试中更改类或方法行为
【发布时间】:2019-10-15 11:48:31
【问题描述】:

我有一个 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),在测试环境中将 NotifierHandler 替换为 FakeNotifierHandler 和/或更改方法 sendNotification() 的行为。

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

特别是,我需要创建一个

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]
        ...
    }
}

阅读我尝试过的所有示例,我只看到了更改方法返回值的简单示例,而不是更改一个类的一个方法的行为,另一个类和另一个我用作集成测试起点的方法.

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

编辑:与How to mock constructor with PowerMockito 不相似,因为我需要更改方法的行为,而不仅仅是返回值。

提前非常感谢

【问题讨论】:

  • Mocking 并不是为了改变行为。您模拟外部事物,以这样的方式在使用特定输入调用时以特定值响应 - 正是您所说的“更改方法的返回值”。目前还不清楚你真正想在那里测试什么,你能更详细地解释一下吗?
  • 嗨,就像帖子的主题一样,我想要在测试期间更改一个类与另一个类(NotifierHandler-->FakeNotifierHandler),以便与原始的'sendNotification( )' 方法,例如绕过网络调用并将它们存储到文本文件中。对不起,如果我不完全清楚。非常感谢
  • 如果您使用的是 Mockito,那么从概念上讲,这种更改需要您更改主代码逻辑:github.com/mockito/mockito/wiki/Mocking-Object-Creation -- 您可以使用 powermock 代替:github.com/powermock/powermock/wiki/MockConstructor。另外,从技术上讲,您的问题似乎是重复的,所以我现在将其标记为这样。
  • 对我来说不,我不仅需要改变结果,还需要改变方法的行为

标签: java junit mockito integration-testing powermock


【解决方案1】:

我找到了一个效果很好而且很简单的解决方案!

解决方案是 PowerMock (https://github.com/powermock/powermock),特别是用另一个类的实例替换创建: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()”方法,我扩展了测试中必须替换的类,并且只覆盖了集成测试所需的方法。 这个解决方案的最大好处是我的代码没有受到影响,我也可以在旧代码上使用这种方法,而不会在代码重构期间引入回归的风险。

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

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>

【讨论】:

    【解决方案2】:

    我认为在您的案例中,主要令人头疼的是您在类 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

    希望对你有帮助。

    【讨论】:

    • 紧耦合绝对是其中的一件事,但 OP 的任务可能只是测试代码,而不是重构它。在编写测试时重构和解耦实现超出了范围。特别是考虑到如果没有测试开始,你怎么知道你正确地重构了它并且它仍然有效?
    • 普罗霍罗夫先生你有理由!我现在无法进行重构,我需要从测试开始。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-25
    • 1970-01-01
    • 2013-04-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多