2008-09-14 11 views
35

Mi lema para Java es "solo porque Java tiene bloques estáticos, 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.Burlarse de bloques estáticos en Java

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 burlarse de staticInit con JMockit para no hacer nada. Veamos esto en el ejemplo.

public class ClassWithStaticInit { 
    static { 
    System.out.println("static initializer."); 
    } 
} 

se cambiará a

public class ClassWithStaticInit { 
    static { 
    staticInit(); 
    } 

    private static void staticInit() { 
    System.out.println("static initialized."); 
    } 
} 

Así que podemos 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 su forma de realizar esta tarea? ¿O alguna solución mejor, no basada en JMockit que creas que funcionaría más limpia?

Respuesta

5

Cuando me encuentro con este problema, por lo general hacen lo mismo que usted describe, salvo que ponga el método estático protegido para que pueda invocar de forma manual. 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 bastante bien, y en realidad 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).

4

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 un poco de 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 de 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.

+0

No se puede burlar los métodos 'static' o' private' con EasyMock. Mover el cuerpo del inicializador estático es lo más parecido a una refactorización que podemos hacer por ahora. –

+0

Si la clase bajo prueba tiene el método static init/private, desea que se invoque. No hay problema. Pero si se burla de la clase, no hay problema para simulacro fácil: no se llamará porque NO HAY IMPLEMENTACIÓN. Puedes burlarte de las interfaces públicas como si las cosas privadas no existieran. –

1

Supongo que realmente quiere algún tipo de fábrica en lugar del inicializador estático.

Algunas mezclas de una fábrica simple y abstracta probablemente podrían 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 solo trate de refactorizar completamente el material estático o si al menos puede salirse con una solución menos compleja.

Sin embargo, es difícil saber si es posible sin ver su código.

3

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 maravilloso, que realmente se necesita refactorización del código (tal vez para inyectar algo así como un initializator).

Saludos cordiales

+0

Me encanta Groovy pero desafortunadamente todo nuestro código de prueba debe estar en JUnit. –

+0

Cem, Pero podría escribir pruebas junit (incluso junit4) en groovy. ;-) Saludos cordiales – marcospereira

1

No soy muy conocedor de 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? Tal como

public static class MockClassWithEmptyStaticInit { 
    public static void staticInit() { 
    } 
} 

y

public static class MockClassWithStaticInit { 
    public static void staticInit() { 
    System.out.println("static initialized."); 
    } 
} 

A continuación, puede utilizarlos en sus 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.

+0

'System.out.println (" static initialized. ");' Es lo que está haciendo el inicializador estático en primer lugar. ¿Por qué deberíamos burlarnos de lo que ya está haciendo con la misma cosa? –

+0

Cem, creo que te perdiste mi punto. Estoy intentando responder a la inquietud "Sin embargo, esta solución también presenta 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". – martinatime

+0

Básicamente, tiene dos objetos simulados diferentes para simular ClassWithStaticInit uno para usar con su DependentClassTest y el otro para usar con ClassWithStaticInitTest. – martinatime

10

Esto 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(). Así, en lugar de hacer este cambio

public class ClassWithStaticInit { 
    static { 
    staticInit(); 
    } 

    private static void staticInit() { 
    System.out.println("static initialized."); 
    } 
} 

que así podría dejar ClassWithStaticInit como está y hacemos lo siguiente en el MockClassWithStaticInit:

public static class MockClassWithStaticInit { 
    public void $clinit() { 
    } 
} 

Esto, de hecho nos permite no realizar ningún cambio en el clases existentes

+0

Esta debería ser la respuesta aceptada. Como una adición; el acceso público no es necesario para $ clinit con una anotación @Mock. – user2219808

32

PowerMock es otro marco simulado que amplía EasyMock y Mockito. Con PowerMock puede fácilmente remove unwanted behavior de una clase, por ejemplo, un inicializador estático. En su ejemplo sólo tiene que añadir las siguientes anotaciones para el caso de test 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.

9

Ocasionalmente, encuentro iniciadores estáticos en las clases de las que depende mi código.Si no puedo refactorizar el código, yo uso @SuppressStaticInitializationFor anotación PowerMock 's para suprimir 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... 
} 

Leer más sobre suppressing unwanted behaviour.

Descargo de responsabilidad: PowerMock es un proyecto de código abierto desarrollado por dos colegas míos.

0

No es realmente una respuesta, pero me pregunto: ¿hay alguna forma de "revertir" la llamada al Mockit.redefineMethods?
Si no existe tal método explícito, no debería ejecutarse de nuevo de la siguiente manera ¿hay algún truco?

Mockit.redefineMethods(ClassWithStaticInit.class, ClassWithStaticInit.class); 

Si existe un procedimiento de este tipo, se podría ejecutar en la clase @AfterClass método, y la prueba de ClassWithStaticInitTest con el bloque 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.

Cuestiones relacionadas