3

I have a java application (no Spring inside) that I want to test with an integration test.

My main use case is the main function that with a specified input do some things on the database and send some request to two different services, one SOAP and one REST.

Now I have a working JUnit configuration (splitted in unit and integration tests) + io.fabric8:docker-maven-plugin that use a docker image for the database during integration tests.

What I'm trying to do is to add a mock for these 2 services, in particular, the method that is used to call directly the external service.

The big problem is that I have this structure:

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

where I have:

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

What I need: call A.mainFunction(r) having, in the test environment, replaced the NotifierHandler with a FakeNotifierHandler and/or change the behaviour of the method sendNotification().

Actual problems: Using Mockito and PowerMock now I have the problem that I'm not able to change globally and directly the class NotifierHandler with FakeNotifierHandler. The same trying to changing the behaviour of the method.

In particular, what I need is to create a

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

Reading all example that I tryed I saw only simple examples that change the return value of a method and not the behaviour of one method of one class used by another and another that I'm using as the start point of the integration test.

NOTE: probably there is a fast way to do this but I'm very new on this type of tests (Mockito, PowerMock,...) and I have found no example for this particular strange case.

EDIT: not similar to How to mock constructor with PowerMockito because I need to change the behaviour of the method, not only the return value.

Thanks a lot in advance

matfur92
  • 330
  • 2
  • 12
  • Mocking is not intended to change the *behavior*. You mock external things, in such way that it responds with a specific value when called with a specific input - precisely what you call "change the return value of a method". It's not clear what you actually want to test there, can you explan that in more detail? – M. Prokhorov Oct 15 '19 at 11:56
  • Hi, like the subject of the post, what I want is to change, during tests, one class with another (NotifierHandler-->FakeNotifierHandler) in order to do different things from the original 'sendNotification()' method like bypass networks calls and store them into a text file for example. Sorry if I'm not totally clear. Thanks a lot – matfur92 Oct 15 '19 at 12:34
  • If you're using Mockito, then conceptually, such change requires you to change your main code logic: https://github.com/mockito/mockito/wiki/Mocking-Object-Creation -- You can use powermock instead: https://github.com/powermock/powermock/wiki/MockConstructor. Also, technically, your question seem to be a duplicate, so I'm going to mark it as such for now. – M. Prokhorov Oct 15 '19 at 12:49
  • Possible duplicate of [How to mock constructor with PowerMockito](https://stackoverflow.com/questions/41110804/how-to-mock-constructor-with-powermockito) – M. Prokhorov Oct 15 '19 at 12:49
  • For me no, I need to change not only the result but also the behaviour of the method – matfur92 Oct 15 '19 at 14:53
  • That can't be achieved by mocking something, because mocks completely replace method body, not modify something in it. You can replace class constructor like in the linked articles and question, that would kind of amount to changing some parts of behavior. If that's not a fitting route, then try refactoring your code so that object creation is abstracted (like Mockito authors suggest). – M. Prokhorov Oct 15 '19 at 15:09
  • I written something related to Mockito and PowerMock/ito but, it's the only way? I'm open to everything. The way that follow a refactoring for now is not a chance. – matfur92 Oct 15 '19 at 15:35
  • Still don't get what specifically you're trying to do, but just remember: mocking fully replaces a method body, without exception. If you only need to replace certain parts of a big method, those parts must themselves become methods. – M. Prokhorov Oct 15 '19 at 15:38
  • As you tell "mock" it is not the correct word, ok! Please ignore "mock" from now. What I need is pretty clear, it is replace NotifierHandler.sendNotification() with FakeNotifierHandler.sendNotification() during JUnit integration tests. What I need is it and I'm asking help for this. How can I reach my goal? Thanks a lot – matfur92 Oct 15 '19 at 15:45
  • What I'm then reading is during test you want to create `FakeNotifierHandler` when code under test calls `new NotifierHandler`. Second and third links from my second comment should do the trick. In the end though, I *strongly* advise you to refactor this thing. – M. Prokhorov Oct 15 '19 at 15:49
  • Thanks a lot! I understand this, but now I can't! So I hope for other ideas and new point of view – matfur92 Oct 15 '19 at 15:54

2 Answers2

2

I found a solution that works very well and it is very simple!

The solution is 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

There is only one problem in my project and it is JUnit 5. PowerMock support JUnit 4 and for this reason, only for some tests of the solution are using it. In order to do this there is the needed to replace

import org.junit.jupiter.api.Test;

with

import org.junit.Test;

In order to use teh "whenNew()" methods I had extented the class that in tests must be replaced and I have overwritten only methods that are necessary for the integration test. The big benefit of this solution is that my code is untouched and I can use this approach also on old code without the risk of introducing regressions during the refactor of the code.

Regarding the code of a integration test, here an example:

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

Here the configuration of the 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>
matfur92
  • 330
  • 2
  • 12
-1

I think the main headache in your case is that you have tightly coupled dependencies between class A, B and NotifierHandler. I would start with:

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

Make NotifierHanlder an interface:

interface NotifierHandler {
  String sendNotification(String json);
}

and make two implementations: one for a real use case, and one fake that you can stub whatever you want:

class FakeNotifierHandler implements NotifierHandler {

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

Inject FakeNotifierHandler in your test.

I hope this helps you.

mate00
  • 2,727
  • 5
  • 26
  • 34
  • Tight coupling is definitely a thing in there, but OP's task is probably just test the code, not refactor it. It'd be way out of scope to refactor and decouple implementation at the time of writing tests. Especially considering that if there's no tests to begin with, how do you know you refactored it correctly and it still works? – M. Prokhorov Oct 15 '19 at 12:58
  • You have reason M. Prokhorov! I cannot do a refactor now, I need to start from tests now. – matfur92 Oct 15 '19 at 13:48