使用Mockito模拟静态方法

我写了一个工厂来产生java.sql.Connection对象:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {


@Override public Connection getConnection() {
try {
return DriverManager.getConnection(...);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}

我想验证传递给DriverManager.getConnection的参数,但我不知道如何模拟静态方法。我的测试用例使用JUnit 4和Mockito。是否有一个好的方法来模拟/验证这个特定的用例?

1052853 次浏览
要模拟静态方法,您应该使用Powermock查看: https://github.com/powermock/powermock/wiki/MockStatic

. mokito 没有提供这个功能

你可以读一篇关于mockito的文章: http://refcardz.dzone.com/refcardz/mockito < / p >

如前所述,你不能用mockito模拟静态方法。

如果改变你的测试框架不是一个选择,你可以做以下事情:

为DriverManager创建一个接口,模拟这个接口,通过某种依赖注入注入它,并在这个模拟上进行验证。

在Mockito之上使用PowerMockito

示例代码:

@RunWith(PowerMockRunner.class)
@PrepareForTest(DriverManager.class)
public class Mocker {


@Test
public void shouldVerifyParameters() throws Exception {


//given
PowerMockito.mockStatic(DriverManager.class);
BDDMockito.given(DriverManager.getConnection(...)).willReturn(...);


//when
sut.execute(); // System Under Test (sut)


//then
PowerMockito.verifyStatic();
DriverManager.getConnection(...);


}

更多信息:

避免使用无法避免的静态方法的典型策略是创建包装对象并使用包装对象。

包装器对象成为真正静态类的外观,您不需要测试它们。

包装器对象可以是这样的

public class Slf4jMdcWrapper {
public static final Slf4jMdcWrapper SINGLETON = new Slf4jMdcWrapper();


public String myApisToTheSaticMethodsInSlf4jMdcStaticUtilityClass() {
return MDC.getWhateverIWant();
}
}
最后,被测试的类可以通过以下方式使用这个单例对象,例如: 有一个用于实际使用的默认构造函数:

public class SomeClassUnderTest {
final Slf4jMdcWrapper myMockableObject;


/** constructor used by CDI or whatever real life use case */
public myClassUnderTestContructor() {
this.myMockableObject = Slf4jMdcWrapper.SINGLETON;
}


/** constructor used in tests*/
myClassUnderTestContructor(Slf4jMdcWrapper myMock) {
this.myMockableObject = myMock;
}
}

这里您有一个可以很容易测试的类,因为您不直接使用带有静态方法的类。

如果你正在使用CDI并且可以使用@Inject注释,那么它就更容易了。 只要让你的Wrapper bean @ApplicationScoped,把它作为合作者注入(你甚至不需要乱七八糟的构造函数进行测试),然后继续模拟

你可以做一点重构:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {


@Override public Connection getConnection() {
try {
return _getConnection(...some params...);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}


//method to forward parameters, enabling mocking, extension, etc
Connection _getConnection(...some params...) throws SQLException {
return DriverManager.getConnection(...some params...);
}
}

然后你可以扩展你的类MySQLDatabaseConnectionFactory来返回一个模拟的连接,对参数进行断言等等。

扩展类可以驻留在测试用例中,如果它位于相同的包中(我鼓励您这样做)

public class MockedConnectionFactory extends MySQLDatabaseConnectionFactory {


Connection _getConnection(...some params...) throws SQLException {
if (some param != something) throw new InvalidParameterException();


//consider mocking some methods with when(yourMock.something()).thenReturn(value)
return Mockito.mock(Connection.class);
}
}

我也遇到过类似的问题。接受的答案并不适合我,直到我做出了更改:@PrepareForTest(TheClassThatContainsStaticMethod.class),根据PowerMock的mockStatic文档

我不需要使用BDDMockito

我的类:

public class SmokeRouteBuilder {
public static String smokeMessageId() {
try {
return InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
log.error("Exception occurred while fetching localhost address", e);
return UUID.randomUUID().toString();
}
}
}

我的测试类:

@RunWith(PowerMockRunner.class)
@PrepareForTest(SmokeRouteBuilder.class)
public class SmokeRouteBuilderTest {
@Test
public void testSmokeMessageId_exception() throws UnknownHostException {
UUID id = UUID.randomUUID();


mockStatic(InetAddress.class);
mockStatic(UUID.class);
when(InetAddress.getLocalHost()).thenThrow(UnknownHostException.class);
when(UUID.randomUUID()).thenReturn(id);


assertEquals(id.toString(), SmokeRouteBuilder.smokeMessageId());
}
}

观察:当您在静态实体中调用静态方法时,您需要更改@PrepareForTest中的类。

例如:

securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);

对于上面的代码,如果您需要模拟MessageDigest类,请使用

@PrepareForTest(MessageDigest.class)

而如果你有如下内容:

public class CustomObjectRule {


object = DatatypeConverter.printHexBinary(MessageDigest.getInstance(SECURITY_ALGORITHM)
.digest(message.getBytes(ENCODING)));


}

然后,您需要准备这个代码所在的类。

@PrepareForTest(CustomObjectRule.class)

然后模拟这个方法:

PowerMockito.mockStatic(MessageDigest.class);
PowerMockito.when(MessageDigest.getInstance(Mockito.anyString()))
.thenThrow(new RuntimeException());

我还编写了Mockito和AspectJ: https://github.com/iirekm/varia/tree/develop/ajmock的组合

你的例子是:

when(() -> DriverManager.getConnection(...)).thenReturn(...);

Mockito不能捕获静态方法,但由于5 2.14.0,您可以通过创建静态方法的调用实例来模拟它。

示例(从他们的测试提取):

public class StaticMockingExperimentTest extends TestBase {


Foo mock = Mockito.mock(Foo.class);
MockHandler handler = Mockito.mockingDetails(mock).getMockHandler();
Method staticMethod;
InvocationFactory.RealMethodBehavior realMethod = new InvocationFactory.RealMethodBehavior() {
@Override
public Object call() throws Throwable {
return null;
}
};


@Before
public void before() throws Throwable {
staticMethod = Foo.class.getDeclaredMethod("staticMethod", String.class);
}


@Test
public void verify_static_method() throws Throwable {
//register staticMethod call on mock
Invocation invocation = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
"some arg");
handler.handle(invocation);


//verify staticMethod on mock
//Mockito cannot capture static methods so we will simulate this scenario in 3 steps:
//1. Call standard 'verify' method. Internally, it will add verificationMode to the thread local state.
//  Effectively, we indicate to Mockito that right now we are about to verify a method call on this mock.
verify(mock);
//2. Create the invocation instance using the new public API
//  Mockito cannot capture static methods but we can create an invocation instance of that static invocation
Invocation verification = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
"some arg");
//3. Make Mockito handle the static method invocation
//  Mockito will find verification mode in thread local state and will try verify the invocation
handler.handle(verification);


//verify zero times, method with different argument
verify(mock, times(0));
Invocation differentArg = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
"different arg");
handler.handle(differentArg);
}


@Test
public void stubbing_static_method() throws Throwable {
//register staticMethod call on mock
Invocation invocation = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
"foo");
handler.handle(invocation);


//register stubbing
when(null).thenReturn("hey");


//validate stubbed return value
assertEquals("hey", handler.handle(invocation));
assertEquals("hey", handler.handle(invocation));


//default null value is returned if invoked with different argument
Invocation differentArg = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
"different arg");
assertEquals(null, handler.handle(differentArg));
}


static class Foo {


private final String arg;


public Foo(String arg) {
this.arg = arg;
}


public static String staticMethod(String arg) {
return "";
}


@Override
public String toString() {
return "foo:" + arg;
}
}
}

他们的目标不是直接支持静态模拟,而是改进它的公共api,以便其他库,如Powermockito,不必依赖于内部api或直接复制一些mock代码。()

免责声明:Mockito团队认为通往地狱的道路是由静态方法铺就的。然而,Mockito的任务不是保护代码不受静态方法的影响。如果您不喜欢您的团队进行静态模拟,请停止在组织中使用Powermockito。Mockito需要作为一个工具箱来发展,并对如何编写Java测试有自己的见解(例如,不要模拟静态!!)然而,Mockito并非教条主义。我们不想阻塞不推荐的用例,比如静态模拟。这不是我们的工作。

使用JMockit框架。这对我很管用。您不必为模拟dbconnection . getconnection()方法编写语句。只要下面的代码就足够了。

下面的@Mock是mockit。模拟方案

Connection jdbcConnection = Mockito.mock(Connection.class);


MockUp<DBConnection> mockUp = new MockUp<DBConnection>() {


DBConnection singleton = new DBConnection();


@Mock
public DBConnection getInstance() {
return singleton;
}


@Mock
public Connection getConnection() {
return jdbcConnection;
}
};
由于该方法是静态的,它已经拥有使用它所需的一切,因此它违背了模拟的目的。 模仿静态方法被认为是一种不好的做法

如果您尝试这样做,这意味着您想要执行测试的方式有问题。

当然,您可以使用PowerMockito或任何其他能够做到这一点的框架,但请重新考虑您的方法。

例如:尝试模拟/提供对象,而静态方法使用这些对象。

有一个简单的解决方案,使用java FunctionalInterface,然后将该接口添加为您试图进行单元测试的类的依赖项。

自Mockito 3.4.0以来,可以在Mockito中模拟静态方法。 更多详细信息见:

https://github.com/mockito/mockito/releases/tag/v3.4.0

https://github.com/mockito/mockito/issues/1013

https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#static_mocks

assertEquals("foo", Foo.method());
try (MockedStatic mocked = mockStatic(Foo.class)) {
mocked.when(Foo::method).thenReturn("bar");
assertEquals("bar", Foo.method());
mocked.verify(Foo::method);
}
assertEquals("foo", Foo.method());

在你的例子中,是这样的:

  @Test
public void testStaticMockWithVerification() throws SQLException {
try (MockedStatic<DriverManager> dummy = Mockito.mockStatic(DriverManager.class)) {
DatabaseConnectionFactory factory = new MySQLDatabaseConnectionFactory();
dummy.when(() -> DriverManager.getConnection("arg1", "arg2", "arg3"))
.thenReturn(new Connection() {/*...*/});


factory.getConnection();


dummy.verify(() -> DriverManager.getConnection(eq("arg1"), eq("arg2"), eq("arg3")));
}
}

注意:模拟STATIC METHODS需要模拟内联依赖而不是模拟核心。

对于JUnit5也添加这个:

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>

对于那些使用JUnit 5的人来说,Powermock不是一个选项。您将需要以下依赖项来成功地使用Mockito模拟静态方法。

testCompile    group: 'org.mockito', name: 'mockito-core',           version: '3.6.0'
testCompile    group: 'org.mockito', name: 'mockito-junit-jupiter',  version: '3.6.0'
testCompile    group: 'org.mockito', name: 'mockito-inline',         version: '3.6.0'

mockito-junit-jupiter增加对JUnit 5的支持。

对模拟静态方法的支持是由mockito-inline依赖项提供的。

例子:

@Test
void returnUtilTest() {
assertEquals("foo", UtilClass.staticMethod("foo"));


try (MockedStatic<UtilClass> classMock = mockStatic(UtilClass.class)) {


classMock.when(() -> UtilClass.staticMethod("foo")).thenReturn("bar");


assertEquals("bar", UtilClass.staticMethod("foo"));
}


assertEquals("foo", UtilClass.staticMethod("foo"));
}

try-with-resource块用于使静态模拟保持临时状态,因此只在该范围内进行模拟。

当不使用try块时,一旦完成断言,请确保关闭mock。

MockedStatic<UtilClass> classMock = mockStatic(UtilClass.class)
classMock.when(() -> UtilClass.staticMethod("foo")).thenReturn("bar");
assertEquals("bar", UtilClass.staticMethod("foo"));
classMock.close();

模拟void方法:

当在类上调用mockStatic时,该类中的所有静态void方法自动被模拟为doNothing()

对于模仿静态函数,我能够这样做:

  • 在helper类/对象中创建一个包装器函数。(使用名称变体可能有助于保持内容的分离和可维护。)
  • 在代码中使用此包装器。(是的,代码的实现需要考虑到测试。)
  • 模拟包装器函数。

包装器代码片段(不是真正的功能,只是为了说明)

class myWrapperClass ...
def myWrapperFunction (...) {
return theOriginalFunction (...)
}

当然,在一个包装器类中积累多个这样的函数可能有利于代码重用。

我在《Mockito》中找到了一个解决方法。这个特性只有3.4.0的版本

< a href = " https://asolntsev.github。io / en / 2020/07/11 / mockito-static-methods noreferrer“rel = > https://asolntsev.github.io/en/2020/07/11/mockito-static-methods/ < / >

  • < p >依赖

    在你的构建中。Gradle将mock -core:3.3.3替换为mock -inline:3.4.0:

    testImplementation('org.mockito:mockito-inline:3.4.0')
    
  • 我们要模拟什么

     class Buddy
    {
    static String name()
    {
    return "John";
    }
    }
    
  • 模拟静态方法

        @Test
    void lookMomICanMockStaticMethods()
    {
    assertThat(Buddy.name()).isEqualTo("John");
    
    
    try (MockedStatic<Buddy> theMock = Mockito.mockStatic(Buddy.class))
    {
    theMock.when(Buddy::name).thenReturn("Rafael");
    assertThat(Buddy.name()).isEqualTo("Rafael");
    }
    
    
    assertThat(Buddy.name()).isEqualTo("John");
    }
    

我想这对我们有帮助。

在这里,我分享我的mockito MockStatic解决方案,它基于我在回答leokom解决方案时承诺的扩展。

那么,为什么Mockito选择了try-with-resources?嗯,只是因为他们想保持一艘整洁的船。毕竟这是很好的编程。Try-with-resources允许在保证调用close方法的情况下进行构造。但是在JUnit中,我们已经在BeforeEach和AfterEach中有了。并且可以使用实现BeforeEachCallback和AfterEachCallback的Extension轻松地将它们添加到每个测试类中。

理论到此为止。让我们做一个静态模拟

Instant.now()

我从一个注释开始,以便能够标记我的测试类中希望用作静态模拟的字段。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface StaticMock {


}

这允许我在我的测试类中为静态模拟创建一个字段,我可以很容易地在我的扩展类中找到它。

  @StaticMock
private MockedStatic<Instant> staticInstantMock;

我将我创建的扩展添加到我的测试类。你有两个选择。

  1. 为此目的创建一个扩展,并将其添加到您也需要的MockitoExtension旁边的类中。
  2. 创建一个扩展,并从MockitoExtension继承。现在可以在测试类上替换MockitoExtension。

我用的是后者。

@ExtendWith({CompanyMockitoExtension.class})
class MyExtendedTestClass {

现在我们需要在调用static时为它返回一些东西:

  @Mock
private Instant now;


staticInstantMock.when(Instant::now).thenReturn(now);

整个测试类:

@ExtendWith({CompanyMockitoExtension.class})
class MyExtendedTestClass {


@StaticMock
private MockedStatic<Instant> staticInstantMock;


@Mock
private Instant now;


@Test
void myTestMethod() {
staticInstantMock.when(Instant::now).thenReturn(now);


assertThat(Instant::now).isSameAs(now); // This would normally happen in the class you are testing...
}
}

现在让我们看一下Extension类。

import static org.mockito.Mockito.mockStatic;


import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;


public class CompanyMockitoExtension extends MockitoExtension {


@Override
public void beforeEach(ExtensionContext context) {
super.beforeEach(context); // Don't forget to call the super!!
if (context.getTestInstance().isEmpty()) { // Just to be sure...
return;
}
// Get the unit test instance
Object testSubject = context.getTestInstance().get();
initializeStaticMocks(testSubject);
}


private void initializeStaticMocks(Object testSubject) {
// Find all fields that I want to static mock
List<Field> staticMockFields = ReflectionHelper.getFieldsWithAnnotation(testSubject, StaticMock.class);
staticMockFields.forEach(field -> initializeStaticMock(field, testSubject));
}


private void initializeStaticMock(Field field, Object testSubject) {
// Get the type of the static mock. It is within the generic MockedStatic<> class type.
Class<?> typeForStaticMock = (Class<?>) ReflectionHelper.getTypesForGeneric(field)[0];
try {
// Now set the field with the mockStatic method of Mockito.
field.setAccessible(true);
field.set(testSubject, mockStatic(typeForStaticMock));
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to instantiate Static Mock with type: " + typeForStaticMock.getName());
}
}


@Override
public void afterEach(ExtensionContext context) {
super.afterEach(context); // Again, do not forget to call the super.
if (context.getTestInstance().isEmpty()) {
return;
}
Object testSubject = context.getTestInstance().get();
closeStaticMocks(testSubject); // Close all static mocks.
}


private void closeStaticMocks(Object testSubject) {
// Again find all fields we annotated
List<Field> staticMockFields = ReflectionHelper.getFieldsWithAnnotation(testSubject, StaticMock.class);
staticMockFields.forEach(field -> closeStaticMock(field, testSubject));
}


private void closeStaticMock(Field field, Object testSubject) {
// Get the instance and simply call close.
MockedStatic<?> mockedStaticInstance = ReflectionHelper.getFieldInstance(field, testSubject, MockedStatic.class);
mockedStaticInstance.close();
}
}

关于这个扩展的好处是,你可以添加额外的嘲弄的东西。我在AfterEach中添加了对所有模拟没有更多交互的验证。这现在是自动的,当我们使用这个扩展。我还为构造模拟添加了与静态模拟相似的行为。

如您所见,我创建了自己的反射助手类。我知道有一些标准的反射助手类,它们可能更好。这是我的。

public class ReflectionHelper {


public static List<Field> getFieldsWithAnnotation(
Object testSubject,
Class<? extends Annotation> annotationType
) {
Class<?> testSubjectClass = testSubject.getClass();


return Arrays.stream(testSubjectClass.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(annotationType))
.collect(toUnmodifiableList());
}


public static List<Field> getCollectionFields(Object testSubject) {
Class<?> testSubjectClass = testSubject.getClass();


return Arrays.stream(testSubjectClass.getDeclaredFields())
.filter(field -> Collection.class.isAssignableFrom(field.getType()))
.collect(toUnmodifiableList());
}


@SuppressWarnings("unchecked")
public static <T> T getFieldInstance(Field field, Object testSubject, Class<T> type) {
return (T) getFieldInstance(field, testSubject);
}


public static Object getFieldInstance(Field field, Object testSubject) {
try {
boolean isStatic = isStatic(field.getModifiers());
Object context = isStatic ? null : testSubject;
field.setAccessible(true);
return field.get(context);
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to get instance of field.");
}
}


public static Type[] getTypesForGeneric(Field field) {
ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
return parameterizedType.getActualTypeArguments();
}
}

当您尝试模拟静态方法时,必须在try块中编写测试。因为重要的是要注意,作用域模拟必须由激活模拟的实体关闭。

      try (MockedStatic<Tester> tester = Mockito.mockStatic(Tester.class)) {
tester.when(() -> Tester.testStatic("Testing..")).thenReturn(mock(ReturnObject.class));
//Here you have to write the test cases
}

在上面的例子中,我们必须模拟测试器类testStatic方法,输入参数为"Testing…"在这里,该方法将返回一个ReturnObject类类型对象。因此我们写mockito当链像上面。

不要忘记在你的Gradle/maven中添加以下依赖项

    testImplementation 'org.mockito:mockito-inline:4.3.1'

稍微重构一下:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {
ConnectionSupplier connection = () -> SupplierDriverManager.getConnection();


public void setConnSupplier(ConnectionSupplier supplier) {
this.connectionSupplier = supplier;
}


@Override
public Connection getConnection() {
try {
return connectionSupplier.conn();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@FunctionalInterface
interface ConnectionSupplier {
Connection conn();
}
}

然后你可以使用mockito:

MySQLDatabaseConnectionFactory.ConnectionSupplier connectionSupplier = mock(MySQLDatabaseConnectionFactory.ConnectionSupplier.class);
when(connectionSupplier.conn()).thenReturn(yourMockObject);
yourConnectionFactory.setConnSupplier(connectionSupplier);