java - example - Burlarse de los métodos estáticos con Mockito.
powermock example (10)
He escrito una fábrica para producir objetos java.sql.Connection
:
public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {
@Override public Connection getConnection() {
try {
return DriverManager.getConnection(...);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
Me gustaría validar los parámetros pasados a DriverManager.getConnection
, pero no sé cómo simular un método estático. Estoy usando JUnit 4 y Mockito para mis casos de prueba. ¿Hay una buena manera de simular / verificar este caso de uso específico?
Como se mencionó anteriormente, no puedes simular métodos estáticos con mockito.
Si cambiar su marco de prueba no es una opción, puede hacer lo siguiente:
Cree una interfaz para DriverManager, simule esta interfaz, inyecte a través de algún tipo de inyección de dependencia y verifique en esa simulación.
La estrategia típica para esquivar métodos estáticos que no tiene forma de evitar usarlos es crear objetos envueltos y, en su lugar, utilizar objetos envoltorios.
Los objetos de envoltura se convierten en fachadas de las clases estáticas reales, y no las prueba.
Un objeto envoltorio podría ser algo así como
public class Slf4jMdcWrapper {
public static final Slf4jMdcWrapper SINGLETON = new Slf4jMdcWrapper();
public String myApisToTheSaticMethodsInSlf4jMdcStaticUtilityClass() {
return MDC.getWhateverIWant();
}
}
Finalmente, su clase bajo prueba puede usar este objeto singleton, por ejemplo, teniendo un constructor predeterminado para uso en la vida real:
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;
}
}
Y aquí tiene una clase que puede probarse fácilmente, porque no usa directamente una clase con métodos estáticos.
Si está utilizando CDI y puede hacer uso de la anotación @Inject, entonces es aún más fácil. Simplemente haga su Wrapper bean @ApplicationScoped, inyecte esa cosa como un colaborador (ni siquiera necesita constructores desordenados para la prueba), y continúe con la burla.
Mockito no puede capturar métodos estáticos, pero desde Mockito 2.14.0 puede simularlo creando instancias de invocación de métodos estáticos.
Ejemplo (extraído de sus pruebas ):
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;
}
}
}
Su objetivo no es apoyar directamente el simulacro estático, sino mejorar sus API públicas para que otras bibliotecas, como PowerMockito , no tengan que depender de las API internas o directamente tengan que duplicar algún código de Mockito. ( source )
Descargo de responsabilidad: el equipo de Mockito cree que el camino al infierno está pavimentado con métodos estáticos. Sin embargo, el trabajo de Mockito no es proteger su código de métodos estáticos. Si no te gusta que tu equipo haga burla estática, deja de usar Powermockito en tu organización. Mockito necesita evolucionar como un conjunto de herramientas con una visión de opinión sobre cómo se deben escribir las pruebas de Java (por ejemplo, ¡¡no te burles de las estadísticas !!) Sin embargo, Mockito no es dogmático. No queremos bloquear los casos de uso no recomendados como la burla estática. Simplemente no es nuestro trabajo.
Observación: cuando llama a un método estático dentro de una entidad estática, necesita cambiar la clase en @PrepareForTest.
Por ejemplo:
securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);
Para el código anterior, si necesita simular una clase MessageDigest, use
@PrepareForTest(MessageDigest.class)
Mientras que si tienes algo como el siguiente:
public class CustomObjectRule {
object = DatatypeConverter.printHexBinary(MessageDigest.getInstance(SECURITY_ALGORITHM)
.digest(message.getBytes(ENCODING)));
}
entonces, necesitarías preparar la clase en la que reside este código.
@PrepareForTest(CustomObjectRule.class)
Y luego burlarse del método:
PowerMockito.mockStatic(MessageDigest.class);
PowerMockito.when(MessageDigest.getInstance(Mockito.anyString()))
.thenThrow(new RuntimeException());
Para simular un método estático, debe usar una vista de Powermock en: https://github.com/powermock/powermock/wiki/MockStatic . Mockito no proporciona esta funcionalidad.
Puedes leer un buen artículo sobre mockito: http://refcardz.dzone.com/refcardz/mockito
Puedes hacerlo con un poco de refactorización:
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...);
}
}
Luego, puede ampliar su clase MySQLDatabaseConnectionFactory
para devolver una conexión MySQLDatabaseConnectionFactory
, hacer afirmaciones sobre los parámetros, etc.
La clase extendida puede residir dentro del caso de prueba, si se encuentra en el mismo paquete (lo cual le recomiendo que haga)
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);
}
}
También escribí una combinación de Mockito y AspectJ: https://github.com/iirekm/varia/tree/develop/ajmock
Tu ejemplo se convierte en:
when(() -> DriverManager.getConnection(...)).thenReturn(...);
Tuve un problema similar La respuesta aceptada no me funcionó hasta que hice el cambio: @PrepareForTest(TheClassThatContainsStaticMethod.class)
, según la documentación de PowerMock para mockStatic .
Y no tengo que usar BDDMockito
.
Mi clase:
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();
}
}
}
Mi clase de prueba:
@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());
}
}
Usa PowerMockito encima de Mockito.
Código de ejemplo:
@RunWith(PowerMockRunner.class)
@PrepareForTest(DriverManager.class)
public class Mocker {
@Test
public void testName() throws Exception {
//given
PowerMockito.mockStatic(DriverManager.class);
BDDMockito.given(DriverManager.getConnection(...)).willReturn(...);
//when
sut.execute();
//then
PowerMockito.verifyStatic();
DriverManager.getConnection(...);
}
Más información:
Usa el framework JMockit . Funcionó para mí. No tiene que escribir declaraciones para simular el método DBConenction.getConnection (). Sólo el código de abajo es suficiente.
@Mock a continuación es el paquete 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;
}
};