2008-09-03 28 views
26

Tengo un objeto Singleton/Factory para el que me gustaría escribir una prueba JUnit. El método Factory decide qué clase de implementación instanciar en función de un nombre de clase en un archivo de propiedades en el classpath. Si no se encuentra ningún archivo de propiedades, o si el archivo de propiedades no contiene la clave del nombre de clase, la clase crea una instancia de una clase de implementación predeterminada.¿Usando diferentes clasificadores para diferentes pruebas JUnit?

Como la fábrica conserva una instancia estática de Singleton para usar una vez que se ha instanciado, para poder probar la lógica de "conmutación por error" en el método Factory necesitaría ejecutar cada método de prueba en un cargador de clases diferente.

¿Hay alguna manera con JUnit (o con otro paquete de prueba de la unidad) para hacer esto?

edición: aquí es una parte del código de fábrica que está en uso:

private static MyClass myClassImpl = instantiateMyClass(); 

private static MyClass instantiateMyClass() { 
    MyClass newMyClass = null; 
    String className = null; 

    try { 
     Properties props = getProperties(); 
     className = props.getProperty(PROPERTY_CLASSNAME_KEY); 

     if (className == null) { 
      log.warn("instantiateMyClass: Property [" + PROPERTY_CLASSNAME_KEY 
        + "] not found in properties, using default MyClass class [" + DEFAULT_CLASSNAME + "]"); 
      className = DEFAULT_CLASSNAME; 
     } 

     Class MyClassClass = Class.forName(className); 
     Object MyClassObj = MyClassClass.newInstance(); 
     if (MyClassObj instanceof MyClass) { 
      newMyClass = (MyClass) MyClassObj; 
     } 
    } 
    catch (...) { 
     ... 
    } 

    return newMyClass; 
} 

private static Properties getProperties() throws IOException { 

    Properties props = new Properties(); 

    InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(PROPERTIES_FILENAME); 

    if (stream != null) { 
     props.load(stream); 
    } 
    else { 
     log.error("getProperties: could not load properties file [" + PROPERTIES_FILENAME + "] from classpath, file not found"); 
    } 

    return props; 
} 
+0

Singletons conducen a todo un mundo de dolor. Evite los singletons y su código se vuelve mucho más fácil de probar y simplemente más completo. –

Respuesta

3

Cuando me encuentro con este tipo de situaciones que prefiero utilizar lo que es un poco de un truco. En su lugar, podría exponer un método protegido como reiniciar(), y luego invocarlo desde la prueba para establecer de manera efectiva la fábrica a su estado inicial. Este método solo existe para los casos de prueba, y lo documentamos como tal.

Es un truco, pero es mucho más fácil que otras opciones y no necesitará una lib de terceros para hacerlo (aunque si prefiere una solución más limpia, es probable que haya algún tipo de tercero) herramientas que podrías usar).

3

Puede usar Reflection para establecer myClassImpl llamando al instantiateMyClass() nuevamente. Eche un vistazo a this answer para ver ejemplos de patrones para jugar con métodos privados y variables.

36

Esta pregunta puede ser antigua, pero dado que esta fue la respuesta más cercana que encontré cuando tuve este problema, pensé que describiría mi solución.

El uso de JUnit 4

Divida sus pruebas de modo que hay un solo método de prueba por clase (esta solución sólo cambia cargadores de clases entre las clases, no entre métodos como el corredor padres reúne todos los métodos de una vez por clase)

Agregue la anotación @RunWith(SeparateClassloaderTestRunner.class) a sus clases de prueba.

Cree el SeparateClassloaderTestRunner a tener este aspecto:

public class SeparateClassloaderTestRunner extends BlockJUnit4ClassRunner { 

    public SeparateClassloaderTestRunner(Class<?> clazz) throws InitializationError { 
     super(getFromTestClassloader(clazz)); 
    } 

    private static Class<?> getFromTestClassloader(Class<?> clazz) throws InitializationError { 
     try { 
      ClassLoader testClassLoader = new TestClassLoader(); 
      return Class.forName(clazz.getName(), true, testClassLoader); 
     } catch (ClassNotFoundException e) { 
      throw new InitializationError(e); 
     } 
    } 

    public static class TestClassLoader extends URLClassLoader { 
     public TestClassLoader() { 
      super(((URLClassLoader)getSystemClassLoader()).getURLs()); 
     } 

     @Override 
     public Class<?> loadClass(String name) throws ClassNotFoundException { 
      if (name.startsWith("org.mypackages.")) { 
       return super.findClass(name); 
      } 
      return super.loadClass(name); 
     } 
    } 
} 

Nota que tenía que hacer esto para probar código que se ejecuta en un marco legado que no podía cambiar. Dada la opción, reduciría el uso de estática y/o pondría ganchos de prueba para permitir que el sistema se reinicie. Puede que no sea lindo, pero me permite probar una gran cantidad de código que de otra forma sería difícil.

También esta solución rompe cualquier otra cosa que se base en trucos de carga de clases como Mockito.

+0

En lugar de buscar "org.mypackages". en loadClass() también puedes hacer algo como esto: return name.startsWith ("java") || name.startsWith ("org.junit")? super.loadClass (nombre): super.findClass (nombre); – Gilead

+1

¿Cómo hacemos de esto la respuesta aceptada? Esto responde la pregunta mientras que la 'respuesta aceptada' actual no. – irbull

+0

Gracias por la respuesta. Estoy intentando recrear esto, pero todas las clases son cargadas por el cargador de clases principal de todos modos, incluso si son del paquete excluido. –

2

Si la ejecución de Junit a través de la Ant task puede establecer fork=true para ejecutar toda clase de pruebas en su propia JVM. Además, coloque cada método de prueba en su propia clase y cada uno cargará e inicializará su propia versión de MyClass. Es extremo pero muy efectivo.

0

A continuación puede encontrar una muestra que no necesita un corredor de prueba JUnit por separado y que también funciona con trucos de carga de clase como Mockito.

package com.mycompany.app; 

import static org.junit.Assert.assertEquals; 
import static org.mockito.Mockito.mock; 
import static org.mockito.Mockito.verify; 

import java.net.URLClassLoader; 

import org.junit.Test; 

public class ApplicationInSeparateClassLoaderTest { 

    @Test 
    public void testApplicationInSeparateClassLoader1() throws Exception { 
    testApplicationInSeparateClassLoader(); 
    } 

    @Test 
    public void testApplicationInSeparateClassLoader2() throws Exception { 
    testApplicationInSeparateClassLoader(); 
    } 

    private void testApplicationInSeparateClassLoader() throws Exception { 
    //run application code in separate class loader in order to isolate static state between test runs 
    Runnable runnable = mock(Runnable.class); 
    //set up your mock object expectations here, if needed 
    InterfaceToApplicationDependentCode tester = makeCodeToRunInSeparateClassLoader(
     "com.mycompany.app", InterfaceToApplicationDependentCode.class, CodeToRunInApplicationClassLoader.class); 
    //if you want to try the code without class loader isolation, comment out above line and comment in the line below 
    //CodeToRunInApplicationClassLoader tester = new CodeToRunInApplicationClassLoaderImpl(); 
    tester.testTheCode(runnable); 
    verify(runnable).run(); 
    assertEquals("should be one invocation!", 1, tester.getNumOfInvocations()); 
    } 

    /** 
    * Create a new class loader for loading application-dependent code and return an instance of that. 
    */ 
    @SuppressWarnings("unchecked") 
    private <I, T> I makeCodeToRunInSeparateClassLoader(
     String packageName, Class<I> testCodeInterfaceClass, Class<T> testCodeImplClass) throws Exception { 
    TestApplicationClassLoader cl = new TestApplicationClassLoader(
     packageName, getClass(), testCodeInterfaceClass); 
    Class<?> testerClass = cl.loadClass(testCodeImplClass.getName()); 
    return (I) testerClass.newInstance(); 
    } 

    /** 
    * Bridge interface, implemented by code that should be run in application class loader. 
    * This interface is loaded by the same class loader as the unit test class, so 
    * we can call the application-dependent code without need for reflection. 
    */ 
    public static interface InterfaceToApplicationDependentCode { 
    void testTheCode(Runnable run); 
    int getNumOfInvocations(); 
    } 

    /** 
    * Test-specific code to call application-dependent code. This class is loaded by 
    * the same class loader as the application code. 
    */ 
    public static class CodeToRunInApplicationClassLoader implements InterfaceToApplicationDependentCode { 
    private static int numOfInvocations = 0; 

    @Override 
    public void testTheCode(Runnable runnable) { 
     numOfInvocations++; 
     runnable.run(); 
    } 

    @Override 
    public int getNumOfInvocations() { 
     return numOfInvocations; 
    } 
    } 

    /** 
    * Loads application classes in separate class loader from test classes. 
    */ 
    private static class TestApplicationClassLoader extends URLClassLoader { 

    private final String appPackage; 
    private final String mainTestClassName; 
    private final String[] testSupportClassNames; 

    public TestApplicationClassLoader(String appPackage, Class<?> mainTestClass, Class<?>... testSupportClasses) { 
     super(((URLClassLoader) getSystemClassLoader()).getURLs()); 
     this.appPackage = appPackage; 
     this.mainTestClassName = mainTestClass.getName(); 
     this.testSupportClassNames = convertClassesToStrings(testSupportClasses); 
    } 

    private String[] convertClassesToStrings(Class<?>[] classes) { 
     String[] results = new String[classes.length]; 
     for (int i = 0; i < classes.length; i++) { 
     results[i] = classes[i].getName(); 
     } 
     return results; 
    } 

    @Override 
    public Class<?> loadClass(String className) throws ClassNotFoundException { 
     if (isApplicationClass(className)) { 
     //look for class only in local class loader 
     return super.findClass(className); 
     } 
     //look for class in parent class loader first and only then in local class loader 
     return super.loadClass(className); 
    } 

    private boolean isApplicationClass(String className) { 
     if (mainTestClassName.equals(className)) { 
     return false; 
     } 
     for (int i = 0; i < testSupportClassNames.length; i++) { 
     if (testSupportClassNames[i].equals(className)) { 
      return false; 
     } 
     } 
     return className.startsWith(appPackage); 
    } 

    } 

} 
Cuestiones relacionadas