2008-09-03 17 views
26

私は、JUnitテストを記述したいSingleton/Factoryオブジェクトを持っています。 Factoryメソッドは、クラスパス上のプロパティファイル内のクラス名に基づいて、どの実装クラスをインスタンス化するかを決定します。プロパティファイルが見つからない場合、またはプロパティファイルにクラス名キーが含まれていない場合、クラスはデフォルトの実装クラスをインスタンス化します。異なるJUnitテストに異なるクラスローダーを使用していますか?

ファクトリメソッドで "フェイルオーバー"ロジックをテストできるようにするために、Factoryがインスタンス化された後で使用するSingletonの静的インスタンスを保持しているので、別のクラスローダーで各テストメソッドを実行する必要があります。

JUnit(または他のユニットテストパッケージ)にはこれを行う方法はありますか?

編集:ここで使用されている工場のコードの一部です:

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

シングルトンは世界中の傷つきを引き起こします。シングルトンを避けて、コードをテストするのがずっと簡単になり、全面的に面白くなります。 –

答えて

3

私はハックのビットが何であるかを使用することを好む状況のこれらの並べ替えに実行します。私は、代わりにreinitialize()のような保護されたメソッドを公開し、テストからこれを呼び出して、ファクトリを効果的に初期状態に戻すように設定することができます。このメソッドはテストケースにのみ存在し、そのようにドキュメント化します。

ハックのビットですが、他のオプションよりもはるかに簡単で、サードパーティ製のライブラリは必要ありません(ただし、クリーナーソリューションを希望する場合は、おそらくサードパーティツールを使用することができます)。

3

Reflectionを使用してmyClassImplを設定するには、instantiateMyClass()を再度呼び出します。 this answerを見て、プライベートなメソッドや変数で遊ぶためのパターン例を見てください。

36

この質問は古いかもしれませんが、これは私がこの問題を抱えていたときに私が見つけた最も近い答えでしたので、私は私の解決策を説明します。クラスごとに一つの試験方法があるように、JUnitの4

は、あなたのテストを分割使用

(親ランナーは、クラスごとに一度、すべてのメソッドを収集して、このソリューションは唯一のクラス間ではなく、メソッド間のクラスローダを変更します)

@RunWith(SeparateClassloaderTestRunner.class)アノテーションをテストクラスに追加します。

このように見えるようにSeparateClassloaderTestRunnerを作成します。私は変更できませんでしたレガシー枠組みの中で実行されているテストコードにこれをしなければならなかった

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

注意。選択肢があれば、統計の使用を減らしたり、テストフックを入れてシステムをリセットすることができます。それはきれいではないかもしれませんが、そうでなければ難しいコードをたくさん試すことができます。

また、このソリューションは、Mockitoなどのクラスロードトリックに依存する他のものを壊します。

+0

"org.mypackages"を検索する代わりにloadClass()では、次のようなこともできます:return name.startsWith( "java")|| name.startsWith( "org.junit")? super.loadClass(名前):super.findClass(名前); – Gilead

+1

これをどのように受け入れるのですか?これは質問に答えますが、現在の「受け入れられた答え」はそうではありません。 – irbull

+0

答えをありがとう。私はこれを再作成しようとしていますが、除外されたパッケージのものであっても、すべてのクラスは親クラスローダーによってロードされます。 –

2

Ant taskでJunitを実行する場合、fork=trueには、それ自身のJVM内のすべてのクラスのテストを実行できます。また、各テストメソッドを独自のクラスに配置し、それぞれ独自のバージョンのMyClassをロードして初期化します。極端ではあるが非常に効果的です。

0

以下に、別のJUnitテストランナーを必要としないサンプルがあります。また、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); 
    } 

    } 

} 
関連する問題