33

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

ファクトリはインスタンス化された後に使用するシングルトンの静的インスタンスを保持するため、ファクトリ メソッドで「フェイルオーバー」ロジックをテストできるようにするには、各テスト メソッドを異なるクラスローダーで実行する必要があります。

JUnit (または別の単体テスト パッケージ) でこれを行う方法はありますか?

編集:使用中のFactoryコードの一部を次に示します。

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

5 に答える 5

44

この質問は古いかもしれませんが、これは私がこの問題を抱えたときに見つけた最も近い答えだったので、私の解決策を説明します.

JUnit 4 の使用

クラスごとに 1 つのテスト メソッドが存在するようにテストを分割します (このソリューションはクラス間でのみクラスローダーを変更し、親ランナーはクラスごとに 1 回すべてのメソッドを収集するため、メソッド間では変更しません)

@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 などのクラスローディング トリックに依存する他のすべてのものを壊します。

于 2012-02-08T11:07:08.507 に答える
3

このような状況に遭遇したとき、私はちょっとしたハックを使用することを好みます. 代わりに、reinitialize() などの保護されたメソッドを公開し、テストからこれを呼び出して、ファクトリを初期状態に効果的に戻すことができます。このメソッドはテスト ケース用にのみ存在し、そのように文書化します。

これは少しハックですが、他のオプションよりもはるかに簡単で、それを行うためにサードパーティのライブラリは必要ありません (ただし、よりクリーンなソリューションを好む場合は、サードパーティのツールがいくつかある可能性があります)。使用できます)。

于 2008-09-03T16:50:56.210 に答える
3

Reflection を使用して再度myClassImpl呼び出すことで設定できますinstantiateMyClass()この回答を見て、プライベート メソッドと変数をいじるパターンの例を確認してください。

于 2008-09-03T17:10:19.563 に答える
1

以下に、別個の 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);
    }

  }

}
于 2015-12-08T11:02:31.437 に答える