2008-09-03 6 views
26

JUnit 테스트를 작성하려는 Singleton/Factory 객체가 있습니다. Factory 메소드는 클래스 패스의 속성 파일에있는 클래스 이름을 기반으로 인스턴스화 할 구현 클래스를 결정합니다. 특성 파일이 없거나 특성 파일에 클래스 이름 키가 들어 있지 않으면, 클래스는 기본 구현 클래스를 인스턴스화합니다.다른 JUnit 테스트에 다른 클래스 로더 사용?

factory가 인스턴스화 된 후에 사용할 Singleton의 정적 인스턴스를 유지하기 때문에 Factory 메서드에서 "failover"논리를 테스트하려면 다른 클래스 로더에서 각 테스트 메서드를 실행해야합니다.

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 자 리브를 사용하지 않아도됩니다. (더 깨끗한 솔루션을 원한다면 아마 제 3 자 도구를 사용할 수 있습니다.)

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 (name) : super.findClass (name); – Gilead

+1

이 대답을 어떻게 받아들입니까? 이것은 현재의 '수용된 대답'이 아니라면 질문에 답합니다. – irbull

+0

답해 주셔서 감사합니다. 나는 이것을 다시 만들려고 노력하고 있지만 제외 된 패키지에서 가져온 것이라도 모든 클래스는 부모 클래스 로더에 의해로드됩니다. –

2

만약 당신이 fork=true가 자신의 JVM에서 테스트의 모든 클래스를 실행하도록 설정할 수 있습니다 Ant task을 통해 Junit와를 실행. 또한 각 테스트 메소드를 자체 클래스에 넣고 각각 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); 
    } 

    } 

}