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


当前回答

稍微重构一下:

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

其他回答

在这里,我分享我的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();
  }
}

在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,将它作为协作器注入(甚至不需要复杂的构造函数进行测试),然后继续模拟。

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

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

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

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.