How can I easily mock out a static method in Java

2020-02-11 21:43发布

问题:

How do I easily mock out a static method in Java?

I'm using Spring 2.5 and JUnit 4.4

@Service
public class SomeServiceImpl implements SomeService {

    public Object doSomething() {
        Logger.getLogger(this.class); //a static method invoked.
        // ...
    }
}

I don't control the static method that my service needs to invoke so I cannot refactor it to be more unit-testable. I've used the Log4J Logger as an example, but the real static method is similar. It is not an option to change the static method.

Doing Grails work, I'm used to using something like:

def mockedControl = mockFor(Logger)
mockControl.demand.static.getLogger{Class clazz-> … }
…
mockControl.verify()

How do I do something similar in Java?

回答1:

Do you mean you can't control the calling code? Because if you control the calls to the static method but not the implementation itself, you can easily make that testable. Create a dependency interface with a single method with the same signature as the static method. Your production implementation will just call through to the static method, but anything which currently calls the static method will call via the interface instead.

You can then mock out that interface in the normal way.



回答2:

The JMockit framework promises to allow mocking of static methods.

https://jmockit.dev.java.net/

In fact, it makes some fairly bold claims, including that static methods are a perfectly valid design choice and their use should not be restricted because of the inadequacy of testing frameworks.

Regardless of whether or not such claims are justifiable, the JMockit framework itself is pretty interesting, although I've yet to try it myself.



回答3:

PowerMock has this ability. It can also mock instantiations of objects inside the class under test. If your tested method calls new Foo(), you can create a mock object for that Foo and replace it in the method you are testing.

Things like suppressing constructors and static initializers are also possible. All of these things are considered untestable code and thus not recommended to do but if you have legacy code, changing it is not always an option. If you are in that position, PowerMock can help you.



回答4:

public interface LoggerWrapper {
    public Logger getLogger(Class<?> c);
    }
public class RealLoggerWrapper implements LoggerWrapper {
    public Logger getLogger(Class<?> c) {return Logger.getLogger(c);}
    }
public class MockLoggerWrapper implements LoggerWrapper {
    public Logger getLogger(Class<?> c) {return somethingElse;}
    }


回答5:

As another answer above stated, JMockit can mock static methods (and anything else, as well).

It even has direct support for logging frameworks. For example, you could write:


@UsingMocksAndStubs(Log4jMocks.class)
public class SomeServiceTest
{
    // All test methods in this class will have any calls
    // to the Log4J API automatically stubbed out.
}

Support for JUnit 4.4 was dropped, however. JUnit 3.8, JUnit 4.5+ and TestNG 5.8+ are supported.



回答6:

That's one of the reason static methods are bad.

We re-architect ed most of our factories to have setters as well so that we could set mock objects into them. In fact, we came up with something close to dependency injection where a single method acted as a factory for all our singletons.

In your case, adding a Logger.setLogger() method (and storing that value) could work. If you have to you could extend the logger class and shadow the getLogger method with your own as well.



回答7:

You could use AspectJ to intercept the static method call and do something useful for your test.



回答8:

Basically, There isn't an easy way to do this in Java + Spring 2.5 & JUnit 4.4 at the moment.

Although it is possible to refactor, and abstract away the static call, Refactoring the code isn't the solution that I was looking for.

JMockit looked like it would work, but is incompatibile with Spring 2.5 and JUnit 4.4.