2008-09-03 11 views
26

Ich habe ein Singleton/Factory-Objekt, für das ich gerne einen JUnit-Test schreiben würde. Die Factory-Methode entscheidet, welche implementierende Klasse basierend auf einem Klassennamen in einer Eigenschaftendatei im Klassenpfad instanziiert wird. Wenn keine Eigenschaftendatei gefunden wird oder die Eigenschaftendatei den Schlüssel classname nicht enthält, instanziert die Klasse eine implementierende Standardklasse.Verschiedene Klassenlader für verschiedene JUnit-Tests verwenden?

Da die Factory eine statische Instanz des Singleton verwendet, sobald sie instanziiert wurde, um die "Failover" -Logik in der Factory-Methode testen zu können, müsste ich jede Testmethode in einem anderen Classloader ausführen.

Gibt es eine Möglichkeit mit JUnit (oder mit einem anderen Unit-Testpaket), dies zu tun?

edit: hier ist ein Teil der Fabrik Code, der in Gebrauch ist:

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 führen zu einer ganzen Welt der Verletzung. Vermeiden Sie Singletons und Ihr Code wird viel einfacher zu testen und einfach viel schöner. –

Antwort

3

Als ich in diese Art von Situationen, laufe ich zu bevorzugen, was ein bisschen wie ein Hack ist. Ich könnte stattdessen eine geschützte Methode wie reinitialize() verfügbar machen und dann diese aus dem Test aufrufen, um die Factory zurück in den ursprünglichen Zustand zu setzen. Diese Methode existiert nur für die Testfälle und ich dokumentiere sie als solche.

Es ist ein bisschen wie ein Hack, aber es ist viel einfacher als andere Optionen und Sie brauchen keine 3rd-Party-lib zu tun (obwohl, wenn Sie eine sauberere Lösung bevorzugen, gibt es wahrscheinlich eine Art von Drittpartei Werkzeuge da draußen könnten Sie verwenden).

3

Sie können Reflection verwenden, um myClassImpl durch erneutes Aufrufen von instantiateMyClass() festzulegen. Werfen Sie einen Blick auf this answer, um Beispielmuster zum Spielen mit privaten Methoden und Variablen zu sehen.

36

Diese Frage könnte alt sein, aber da dies die nächste Antwort war, die ich fand, als ich dieses Problem hatte, würde ich meine Lösung beschreiben.

mit JUnit 4

Sie Ihre Tests Split, so dass es eine Testmethode pro Klasse ist (diese Lösung nur ändert Classloader zwischen den Klassen, nicht zwischen den Methoden als Mutter Läufer sammelt alle Methoden einmal pro Klasse)

Fügen Sie die @RunWith(SeparateClassloaderTestRunner.class) Annotation zu Ihren Testklassen hinzu.

Erstellen der SeparateClassloaderTestRunner wie folgt aussehen:

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); 
     } 
    } 
} 

Hinweis Ich hatte dies zu tun Code zu testen in einem Legacy-Rahmen laufen, die ich nicht ändern konnte. Angesichts der Wahl würde ich den Einsatz von Statik reduzieren und/oder Testhaken einlegen, um das System zurückzusetzen. Es ist vielleicht nicht schön, aber es erlaubt mir, eine Menge Code zu testen, der sonst schwierig wäre.

Auch diese Lösung bricht alles andere, das auf Classloading-Tricks wie Mockito angewiesen ist.

+0

Anstatt nach "org.mypackages" zu suchen. In loadClass() können Sie auch so etwas tun: return name.startsWith ("java") || name.startsWith ("org.junit")? super.loadClass (Name): super.findClass (Name); – Gilead

+1

Wie machen wir das zur akzeptierten Antwort? Dies beantwortet die Frage, während die aktuelle "akzeptierte Antwort" dies nicht tut. – irbull

+0

Danke für die Antwort. Ich versuche, das neu zu erstellen, aber alle meine Klassen werden sowieso vom übergeordneten Classloader geladen, selbst wenn sie vom ausgeschlossenen Paket stammen. –

2

Wenn Junit über die Ant task Ausführung kann eingestellt werden, fork=true jede Klasse von Tests in einem eigenen JVM auszuführen. Fügen Sie auch jede Testmethode in eine eigene Klasse ein und sie laden und initialisieren jeweils ihre eigene Version von MyClass. Es ist extrem, aber sehr effektiv.

0

Unten finden Sie ein Beispiel, das keinen separaten JUnit-Testlauf benötigt und auch mit Classloading-Tricks wie Mockito funktioniert.

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); 
    } 

    } 

}