我写了一个工厂来产生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。是否有一个好的方法来模拟/验证这个特定的用例?


要模拟静态方法,您应该使用Powermock查看: https://github.com/powermock/powermock/wiki/MockStatic。 Mockito不提供这种功能。

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


如前所述,你不能用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(...);

    }

更多信息:

为什么Mockito不模拟静态方法?


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

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

包装器对象可以是这样的

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

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

我不需要使用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不能捕获静态方法,但是由于Mockito 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或直接复制一些Mockito代码。(源)

Disclaimer: Mockito team thinks that the road to hell is paved with static methods. However, Mockito's job is not to protect your code from static methods. If you don’t like your team doing static mocking, stop using Powermockito in your organization. Mockito needs to evolve as a toolkit with an opinionated vision on how Java tests should be written (e.g. don't mock statics!!!). However, Mockito is not dogmatic. We don't want to block unrecommended use cases like static mocking. It's just not our job.


使用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的支持。

对模拟静态方法的支持由模拟内联依赖项提供。

例子:

@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版本中提供

https://asolntsev.github.io/en/2020/07/11/mockito-static-methods/

dependency In your build.gradle replace mockito-core:3.3.3 by mockito-inline:3.4.0: testImplementation('org.mockito:mockito-inline:3.4.0') what are we going to mock class Buddy { static String name() { return "John"; } } Mock the static method @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;

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

为此目的创建一个扩展,并将其添加到您也需要的MockitoExtension旁边的类中。 创建一个扩展,并从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 connectionSupplier = () -> 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);