java - test - mockito static method without powermock
Mocking Static Blocks en Java (9)
Mi lema para Java es "solo porque Java tiene bloques estáticos, eso no significa que deba usarlos". Bromas aparte, hay muchos trucos en Java que hacen que las pruebas sean una pesadilla. Dos de los que más odio son las Clases anónimas y los Bloques estáticos. Tenemos un montón de código heredado que hace uso de Static Blocks y estos son uno de los puntos molestos en nuestras pruebas de unidad de escritura. Nuestro objetivo es poder escribir pruebas unitarias para las clases que dependen de esta inicialización estática con cambios mínimos de código.
Hasta ahora, mi sugerencia para mis colegas es mover el cuerpo del bloque estático a un método estático privado y llamarlo staticInit
. Este método puede ser llamado desde dentro del bloque estático. Para las pruebas unitarias, otra clase que dependa de esta clase podría fácilmente staticInit
con JMockit para no hacer nada. Veamos esto en el ejemplo.
public class ClassWithStaticInit {
static {
System.out.println("static initializer.");
}
}
Será cambiado a
public class ClassWithStaticInit {
static {
staticInit();
}
private static void staticInit() {
System.out.println("static initialized.");
}
}
Para que podamos hacer lo siguiente en un JUnit.
public class DependentClassTest {
public static class MockClassWithStaticInit {
public static void staticInit() {
}
}
@BeforeClass
public static void setUpBeforeClass() {
Mockit.redefineMethods(ClassWithStaticInit.class, MockClassWithStaticInit.class);
}
}
Sin embargo, esta solución también viene con sus propios problemas. No puede ejecutar DependentClassTest
y ClassWithStaticInitTest
en la misma JVM, ya que realmente desea que el bloque estático se ejecute para ClassWithStaticInitTest
.
¿Cuál sería tu forma de lograr esta tarea? ¿O alguna solución mejor, no basada en JMockit que creas que funcionaría más limpia?
No estoy muy bien informado en los frameworks de Mock así que por favor corrígeme si estoy equivocado, ¿pero no podrías tener dos objetos Mock diferentes para cubrir las situaciones que mencionas? Como
public static class MockClassWithEmptyStaticInit {
public static void staticInit() {
}
}
y
public static class MockClassWithStaticInit {
public static void staticInit() {
System.out.println("static initialized.");
}
}
Entonces puedes usarlos en tus diferentes casos de prueba
@BeforeClass
public static void setUpBeforeClass() {
Mockit.redefineMethods(ClassWithStaticInit.class,
MockClassWithEmptyStaticInit.class);
}
y
@BeforeClass
public static void setUpBeforeClass() {
Mockit.redefineMethods(ClassWithStaticInit.class,
MockClassWithStaticInit.class);
}
respectivamente.
Cuando me encuentro con este problema, generalmente hago lo mismo que describes, excepto que protejo el método estático para poder invocarlo manualmente. Además de esto, me aseguro de que el método se pueda invocar varias veces sin problemas (de lo contrario, no es mejor que el inicializador estático en lo que respecta a las pruebas).
Esto funciona razonablemente bien, y puedo probar que el método del inicializador estático hace lo que espero / quiero que haga. A veces es más fácil tener algún código de inicialización estático, y simplemente no vale la pena construir un sistema demasiado complejo para reemplazarlo.
Cuando uso este mecanismo, me aseguro de documentar que el método protegido solo se expone con fines de prueba, con la esperanza de que no sea utilizado por otros desarrolladores. Esto, por supuesto, puede no ser una solución viable, por ejemplo, si la interfaz de la clase es externamente visible (ya sea como un subcomponente de algún tipo para otros equipos o como un marco público). Sin embargo, es una solución simple al problema y no requiere una biblioteca de terceros para configurar (lo que me gusta).
Me parece que está tratando un síntoma: diseño deficiente con dependencias de inicialización estática. Quizás alguna refactorización es la verdadera solución. Parece que ya ha hecho una pequeña refactorización con su función staticInit()
, pero tal vez esa función deba ser llamada desde el constructor, no desde un inicializador estático. Si puede eliminar el período de inicializadores estáticos, estará mejor. Solo usted puede tomar esta decisión ( no puedo ver su código base ) pero algunas refactorizaciones definitivamente ayudarán.
En cuanto a burlarse, uso EasyMock, pero me he encontrado con el mismo problema. Los efectos secundarios de los inicializadores estáticos en el código heredado dificultan las pruebas. Nuestra respuesta fue refactorizar el inicializador estático.
Puede escribir su código de prueba en Groovy y simular fácilmente el método estático mediante la metaprogramación.
Math.metaClass.''static''.max = { int a, int b ->
a + b
}
Math.max 1, 2
Si no puede usar Groovy, realmente necesitará refactorizar el código (tal vez para inyectar algo como un inicializador).
Saludos cordiales
Supongo que realmente quieres algún tipo de fábrica en lugar del inicializador estático.
Es probable que una mezcla de una fábrica única y una abstracta pueda ofrecerle la misma funcionalidad que hoy, y con buena capacidad de prueba, pero eso agregaría bastante código de placa de caldera, por lo que podría ser mejor intentar refactorizar las cosas estáticas de distancia por completo o si al menos podría salir con una solución menos compleja.
Sin embargo, es difícil saber si es posible sin ver tu código.
Esto va a entrar en JMockit más "avanzado". Resulta que puede redefinir bloques de inicialización estáticos en JMockit creando un método public void $clinit()
. Entonces, en lugar de hacer este cambio
public class ClassWithStaticInit {
static {
staticInit();
}
private static void staticInit() {
System.out.println("static initialized.");
}
}
también podríamos dejar ClassWithStaticInit
como está y hacer lo siguiente en MockClassWithStaticInit
:
public static class MockClassWithStaticInit {
public void $clinit() {
}
}
Esto de hecho nos permitirá no hacer ningún cambio en las clases existentes.
PowerMock es otro marco simulado que amplía EasyMock y Mockito. Con PowerMock puede eliminar fácilmente el comportamiento no deseado de una clase, por ejemplo, un inicializador estático. En su ejemplo, simplemente agrega las siguientes anotaciones a su caso de prueba JUnit:
@RunWith(PowerMockRunner.class)
@SuppressStaticInitializationFor("some.package.ClassWithStaticInit")
PowerMock no utiliza un agente de Java y, por lo tanto, no requiere la modificación de los parámetros de inicio de JVM. Usted simplemente agrega el archivo jar y las anotaciones anteriores.
Ocasionalmente, encuentro iniciadores estáticos en las clases de las que depende mi código. Si no puedo refactorizar el código, utilizo la @SuppressStaticInitializationFor de @SuppressStaticInitializationFor
para @SuppressStaticInitializationFor
el inicializador estático:
@RunWith(PowerMockRunner.class)
@SuppressStaticInitializationFor("com.example.ClassWithStaticInit")
public class ClassWithStaticInitTest {
ClassWithStaticInit tested;
@Before
public void setUp() {
tested = new ClassWithStaticInit();
}
@Test
public void testSuppressStaticInitializer() {
asserNotNull(tested);
}
// more tests...
}
Lea más sobre la supresión del comportamiento no deseado .
Descargo de responsabilidad: PowerMock es un proyecto de código abierto desarrollado por dos colegas míos.
Realmente no es una respuesta, pero me pregunto: ¿no hay alguna forma de "revertir" la llamada a Mockit.redefineMethods
?
Si no existe dicho método explícito, ¿no debería ejecutarlo de la siguiente manera?
Mockit.redefineMethods(ClassWithStaticInit.class, ClassWithStaticInit.class);
Si existe dicho método, puede ejecutarlo en el método de clase @AfterClass
y probar ClassWithStaticInitTest
con el bloque de inicializador estático "original", como si nada hubiera cambiado, desde la misma JVM.
Esto es sólo una corazonada, así que me puede estar perdiendo algo.