0

So, I have a class I need to mock, since I don't want the tests to actually do this connection, I want to provide fake results for the privateMethodCall():

public final Connection {
  private Connection() {
    //does some stuff  
  }

  public static void get(/*some params*/) {
    Connection conn = new Connection();
    con.privateMethodCall();
  }
}        

The issue is most singletons I see mocked are returning the instance in the public static method, this one is not.

In the class I'm testing, the Connection class is just called in a private method from the method I'm trying to test (prepareForThing()).

import Connection;

public class ManagerClass {
  ManagerClass(/* some params*/){
    //Does some stuff
  }

  void prepareForThing(/* params */) {
    // Does stuff
    doThing();
  }

  private void doThing(/* more params */) {
    // Does some stuff
    Connection.get();
    // Continues doing stuff
  }
}

Even if I create another constructor and pass the mock class like is suggested Using Singleton with Interfaces Java, since the instance is just created and used within the static method it wouldn't work.

My workaround so far is to pass a boolean isTest to the class I'm testing, and then modify the Connection class so it can be extended, write a ConnectionMock, and if the isTest is set to true, modify the call. But this makes the ManagerClass depend on a test class, ConnectionMock, which is not good

But I don't know if there might be a better workaround with mockito or something. I would like the mock to be available for other tests, rather than create it for the test only. And I'm not sure extending this class is the best approach, but I can't create an interface because all methods in Connection are private or static. Any ideas?

Tyrannogina
  • 593
  • 1
  • 4
  • 21
  • 1
    You have two options here: either you use PowerMockito (cf: https://stackoverflow.com/questions/9585323/how-do-i-mock-a-static-method-that-returns-void-with-powermock) and leave the code as-is, or you take a Runnable in construction parameter of your class and then in production you inject the runnable as `() -> Connection.get()` whereas in the tests you simply inject an empty runnable `() -> {}`. I'd go for PowerMockito (it keeps the production code cleaner) unless you're forbidden to add the dependency, in that case the runnable is your only option – Matteo NNZ Jul 03 '23 at 08:59
  • That's one of the reasons why singletons and static methods are generally discouraged. One possible solution: create a wrapper around your singleton, inject the wrapper and then replace the wrapper with a test impl. – knittl Jul 03 '23 at 11:35

1 Answers1

1

One possible solution is to hide your singleton. Singletons are inherently hard to test, because you only have a single instance and a single implementation. All clients of static methods are tightly coupled to them.

Note that your Singleton isn't a real singleton in the classical sense, because multiple instance of it exist.

Wrap your singleton and inject the wrapper to your clients to hide it.

public final ConnectionSingleton {
  private ConnectionSingleton() {
    //does some stuff  
  }

  public static void get(/*some params*/) {
    ConnectionSingleton conn = new ConnectionSingleton();
    con.privateMethodCall();
  }
}

public interface Connection {
  void get();
}

public class ProductionConnection implements Connection {
  @Override
  public void get() {
    Connection.get(); // production implementation delegates to singleton
  }
}
import Connection;

public class ManagerClass {
  private final Connection connection; // depend on the interface
  ManagerClass(Connection connection){
    this.connection = connection;
  }

  void prepareForThing(/* params */) {
    // Does stuff
    doThing();
  }

  private void doThing(/* more params */) {
    // Does some stuff
    connection.get(); // call the instance method of the interface
    // Continues doing stuff
  }
}

In your production code instantiate your manager as new ManagerClass(new ProductionConnection()). In your test, inject a mock or a custom dummy implementation:

class TestConnection implements Connection {
  @Override public void get() { /* do nothing */ }
}

@Test
void myTest() {
  ManagerClass manager = new ManagerClass(new TestConnection());
  manager.prepareForThing();
}

Since this interface only has a single method, you could pass an empty statement lambda too: new ManagerClass(() -> {}).

You might find other solutions for the underlying problem (which is using new in your methods) in the related, but not quite duplicated, question Why are my mocked methods not called when executing a unit test?

knittl
  • 246,190
  • 53
  • 318
  • 364