我编写了一个工厂来生产 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 用于我的测试用例。有没有一种模拟/验证这个特定用例的好方法?
static
方法并非设计使然,而是偶然。这种限制(以及不支持模拟 final
类/方法或 new
-ed 对象)是用于实现模拟的方法的自然(但非预期)结果,其中动态创建实现/扩展的新类要模拟的类型;其他模拟库使用其他方法来避免这些限制。这也发生在 .NET 世界中。
在 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 不模拟静态方法?
从 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")));
}
}
注意:模拟静态方法需要 mockito-inline 依赖而不是 mockito-core。
对于 JUnit5,还要添加:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
避开您无法避免使用的静态方法的典型策略是创建包装对象并改用包装对象。
包装器对象成为真正的静态类的外观,您无需对其进行测试。
包装器对象可能类似于
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,将该东西作为协作者注入(您甚至不需要凌乱的构造函数来进行测试),然后继续进行模拟。
我有一个类似的问题。接受的答案对我不起作用,直到我做出更改:@PrepareForTest(TheClassThatContainsStaticMethod.class)
,根据 PowerMock's documentation for 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());
}
}
如前所述,您不能使用 mockito 模拟静态方法。
如果更改测试框架不是一种选择,您可以执行以下操作:
为 DriverManager 创建一个接口,模拟这个接口,通过某种依赖注入将其注入并在该模拟上进行验证。
对于使用 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 块时,请确保在完成断言后关闭模拟。
MockedStatic<UtilClass> classMock = mockStatic(UtilClass.class)
classMock.when(() -> UtilClass.staticMethod("foo")).thenReturn("bar");
assertEquals("bar", UtilClass.staticMethod("foo"));
classMock.close();
模拟 void 方法:
在类上调用 mockStatic
时,该类中的所有静态 void 方法都会自动模拟为 doNothing()
。
观察:在静态实体中调用静态方法时,需要更改@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(...);
您可以通过一些重构来做到这一点:
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);
}
}
Mockito 无法捕获静态方法,但从 Mockito 2.14.0 开始,您可以通过创建静态方法的调用实例来模拟它。
示例(从 their tests 中提取):
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 代码。 (source)
免责声明:Mockito 团队认为通往地狱的道路是用静态方法铺就的。但是,Mockito 的工作不是保护您的代码免受静态方法的影响。如果您不喜欢您的团队进行静态模拟,请停止在您的组织中使用 Powermockito。 Mockito 需要发展为一个工具包,对应该如何编写 Java 测试有一个固执的愿景(例如,不要模拟静态!!!)。然而,Mockito 并不是教条主义的。我们不想阻止不推荐的用例,例如静态模拟。这不是我们的工作。
要模拟静态方法,您应该使用 Powermock 查看:https://github.com/powermock/powermock/wiki/MockStatic。 Mockito doesn't provide 这个功能。
您可以阅读一篇关于 mockito 的精彩文章:http://refcardz.dzone.com/refcardz/mockito
我在 Mockito 中找到了一种解决方案。此功能仅来自 3.4.0
的版本
https://asolntsev.github.io/en/2020/07/11/mockito-static-methods/
在您的 build.gradle 中,将 mockito-core:3.3.3 替换为 mockito-inline:3.4.0: testImplementation('org.mockito:mockito-inline:3.4.0')
我们要模拟什么类 Buddy { static String name() { return "John"; } }
模拟静态方法 @Test void lookMomICanMockStaticMethods() { assertThat(Buddy.name()).isEqualTo("John");尝试 (MockedStatic
我认为这可以帮助我们。
由于该方法是静态的,因此它已经拥有您使用它所需的一切,因此它违背了模拟的目的。模拟静态方法被认为是一种不好的做法。
如果您尝试这样做,则意味着您执行测试的方式有问题。
当然,您可以使用 PowerMockito 或任何其他能够做到这一点的框架,但请尝试重新考虑您的方法。
例如:尝试模拟/提供该静态方法使用的对象。
当您尝试模拟静态方法时,您必须在 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
}
在上面的例子中,我们必须模拟 Tester 类 testStatic 方法,输入参数为“Testing...”。在这里,该方法将返回一个 ReturnObject 类类型的对象。因此,我们在像上面那样链接时编写 mockito。
不要忘记在 Gradle/maven 中添加以下依赖项
testImplementation 'org.mockito:mockito-inline:4.3.1'
使用 JMockit 框架。它对我有用。您不必为模拟 DBConenction.getConnection() 方法编写语句。只需下面的代码就足够了。
@Mock 下面是 mockit.Mock 包
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;
}
};
使用 java FunctionalInterface 有一个简单的解决方案,然后将该接口添加为您尝试进行单元测试的类的依赖项。
对于模拟静态函数,我可以这样做:
在一些帮助类/对象中创建一个包装函数。 (使用名称变体可能有利于保持事物的分离和可维护性。)
在你的代码中使用这个包装器。 (是的,代码需要在考虑到测试的情况下实现。)
模拟包装函数。
包装器代码片段(不是真正的功能,仅用于说明)
class myWrapperClass ...
def myWrapperFunction (...) {
return theOriginalFunction (...)
}
当然,在单个包装类中累积多个此类函数可能有利于代码重用。
在这里,我根据我对 leokom 解决方案的回答中承诺的扩展来分享我的 mockito MockStatic 解决方案。
那么,为什么 Mockito 选择 try-with-resources 呢?好吧,仅仅是因为他们想保持一艘整洁的船。毕竟这是一个很好的编程。 Try-with-resources 允许在保证调用 close 方法的情况下进行构造。但是在 JUnit 中,我们已经在 BeforeEach 和 AfterEach 中拥有了它。并且可以使用实现 BeforeEachCallback 和 AfterEachCallback 的扩展轻松地将这些用于通用目的添加到每个测试类。
理论就这么多。让我们为
Instant.now()
我从一个注释开始,以便能够在我的测试类中标记我想用作静态模拟的字段。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface StaticMock {
}
这允许我在我的测试类中创建一个用于静态模拟的字段,我可以在我的扩展类中轻松找到它。
@StaticMock
private MockedStatic<Instant> staticInstantMock;
我将我创建的扩展添加到我的测试类中。你有两个选择。
为此目的创建一个 Extension 并将其添加到您还需要的 MockitoExtension 旁边的类中。创建一个扩展并让它从 MockitoExtension 继承。现在你可以在你的测试类上替换 MockitoExtension。
我使用了两者中的后者。
@ExtendWith({CompanyMockitoExtension.class})
class MyExtendedTestClass {
现在我们需要在调用静态时为它返回一些东西:
@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();
}
}
不定期副业成功案例分享
@RunWith(PowerMockRunner.class)
及低于该@PowerMockRunnerDelegate(JUnit4.class)
。