Whiteship's Note


[스프링 테스트 확장] static member class를 빈으로 등록하는 테스트 로더 1

모하니?/Coding : 2009.12.18 13:19


테스트를 편하게 하기위한 기발한 아이디어를 읽는 도중 조금 맘에 안 드는 부분이 있었는데.. 그건 바로 ApplicationContext를 직접 생성하는 부분이었습니다. 게다가 원래는 @Configuration이 붙어있는 클래스를 넘겨받기로 되어있는 API에 static membe class를 넣어 빈으로 만들었습니다. 굉장히 기발하지만 약간 해킹스러운 방식이고 불편한 점들이 보였습니다. 빈으로 등록할 static member class가 많아지기라도 하다면.. 흠..

그래서 static member class를 빈으로 등록하겠다는 명시적인 이름을 가지고 있는 테스트 컨텍트스 로더를 만들기로 했습니다. 몇일전에 만들었던 애노테이션 설정 컨텍스트 로더를 조금만 고치면 가능할 것 같아서 시도했습니다.

그 결과 다음과 같이 테스트를 작성할 수 있게됐습니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = StaticMemberClassLoader.class)
public class StaticInnerConfigTest {

    @Autowired ApplicationContext ac;

    static class Hello {
        @Inject Printer printer;
    }

    static class Printer {
    }

    class Foo {}

    @Test
    public void inject() {
        for(String beanName : ac.getBeanDefinitionNames()){
            System.out.println(beanName);
        }

        Hello hello = ac.getBean(Hello.class);
        assertThat(hello.printer, is(notNullValue()));
    }
}

사부님이 작성한것과 비교하자면, 우선 ApplicationContext를 직접 만들지 않아도 알아서 static member class를 빈으로 만들어주며, static member class를 ContextConfigruation으로 쓰겠다고 로더 설정이 되어있으니 좀 더 명시적이라고 할 수 있겠습니다. 음헤헷;


콘솔에는 위와 같이 찍힙니다. StaticMemberClassLoader를 확장하거나 수정하여 빈을 등록할 때 빈 이름이나, 등록할 빈을 골라내는 작업을 얼마든지 변경할 수도 있습니다.

사부님 덕분에 아침부터 잼난 코딩을 했더니 기분이좋습니다. 참고로 이 로더 구현체에는 문제가 좀 많습니다. 무슨 문제가 있는지 지적해주시는 분이 계실지 모르겠지만 퀴즈 삼아 문제가 많은 코드를 공유하고 싶어졌습니다. 발코딩을 까고 싶으신 분들에게는 절호의 기회입니다. 마음껏 까주세요!

public class StaticMemberClassLoader extends AnnotationContextLoader {

    @Override
    protected void createCustomBean(DefaultListableBeanFactory context, String[] locations) throws ClassNotFoundException {
        for(String string : locations){
            Class beanClass = ClassUtils.forName(string, getClass().getClassLoader());
            context.registerBeanDefinition(beanClass.getSimpleName(), BeanDefinitionBuilder.rootBeanDefinition(beanClass).getBeanDefinition());
        }
    }

    @Override
    protected String[] generateDefaultLocations(Class<?> clazz) {
        Class[] classes = clazz.getDeclaredClasses();
        List<String> modifiedLocations = new ArrayList<String>();
        for (int i = 0; i < classes.length; i++) {
            Class klass = classes[i];
            if(klass.getModifiers() == Modifier.STATIC)
                modifiedLocations.add(klass.getName());
        }
        return modifiedLocations.toArray(new String[modifiedLocations.size()]);
    }

}


top

  1. Favicon of http://helols.pe.kr BlogIcon is_Yoon 2009.12.18 13:31 PERM. MOD/DEL REPLY

    Class klass = classes[i];
    if(klass.getModifiers() == Modifier.STATIC)
    modifiedLocations.add(klass.getName());
    }

    klass 가 인상깊음.. ㅡㅡ;;

    Favicon of https://whiteship.tistory.com BlogIcon 기선 2009.12.18 14:03 신고 PERM MOD/DEL

    뭐 그런걸 가지고.. clazz에 비하면;

  2. Favicon of http://whiteship.me BlogIcon 보아뱀 2009.12.18 16:12 PERM. MOD/DEL REPLY

    locations를 주면 뭔가 꼬일 것 같은데, 그런 경우를 방지해야 하지 않을까요?
    그리고 확장한 클래스가 AnnotationContextLoader 인데, 그것보다 상위의 클래스를 확장하는게 더 어울린다고 생각합니다.

    Favicon of http://whiteship.me BlogIcon 기선 2009.12.18 16:12 PERM MOD/DEL

    아.. 그렇군요.
    수정하겠습니다!

  3. Favicon of http://toby.epril.com BlogIcon 토비 2009.12.18 16:14 PERM. MOD/DEL REPLY

    이렇게 만들면 테스트 당 하나의 빈 조합 밖에 안나온 다는 사실.

    학습 테스트를 만들려면 매 테스트 마다 다른 빈 클래스들을 써야 하니까 직접 만들어 쓴 거지.

    Favicon of http://whiteship.me BlogIcon 기선 2009.12.18 16:35 PERM MOD/DEL

    아핫;; 그렇군요.ㅋ

Write a comment.


[스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 4 - 일단 끝

모하니?/Coding : 2009.12.16 13:53


/**
 * @author Keesun Baik(Whiteship)
 * @author Seongyoon Kim(Is윤군)
 */
public class AnnotationContextLoader extends AbstractContextLoader {

    private static final String JAVA_FILE_SUFFIX = ".java";
    private static final String APP_CONFIG_FILE_PREFIX = "AppConfig";

    @Override
    public String getResourceSuffix() {
        return APP_CONFIG_FILE_PREFIX + JAVA_FILE_SUFFIX;
    }

    @Override
    protected String[] generateDefaultLocations(Class<?> clazz) {
        Assert.notNull(clazz, "Class must not be null");
        String suffix = getResourceSuffix();
        Assert.hasText(suffix, "Resource suffix must not be empty");
        return new String[] { clazz.getName() + suffix };
    }

    @Override
    protected String[] modifyLocations(Class<?> clazz, String... locations) {
        String[] modifiedLocations = new String[locations.length];
        for (int i = 0; i < locations.length; i++) {
            String path = locations[i];

            if(path.endsWith("/"))
                path = path.substring(0, path.length() - 1);

            if (path.startsWith("/")) {
                modifiedLocations[i] = ClassUtils.convertResourcePathToClassName(path.substring(1));
            }
            else if (!ResourcePatternUtils.isUrl(path)) {
                modifiedLocations[i] = getClassName(clazz, path);
            }
            else {
                throw new UnsupportedOperationException();
            }
        }
        return modifiedLocations;
    }

    private String getClassName(Class clazz, String path) {
        return ClassUtils.convertResourcePathToClassName(
            StringUtils.cleanPath(ClassUtils.classPackageAsResourcePath(clazz) + "/" + path));
    }

    public final ConfigurableApplicationContext loadContext(String... locations) throws Exception {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        prepareContext(context);
        customizeBeanFactory(context.getDefaultListableBeanFactory());
        context.register(getAppConfigClasses(context.getClassLoader(), locations));
        context.scan(getAppConfigPackages(context.getClassLoader(), locations));
        AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
        context.refresh();
        context.registerShutdownHook();
        return context;
    }

    private String[] getAppConfigPackages(ClassLoader classLoader, String[] locations) {
        List<String> packages = new ArrayList<String>();
        for(String location : locations){
            if(!location.contains(JAVA_FILE_SUFFIX))
                packages.add(location);
        }
        return packages.toArray(new String[packages.size()]);
    }

    private Class<?>[] getAppConfigClasses(ClassLoader classLoader, String[] locations) throws ClassNotFoundException {
        List<Class<?>> classes = new ArrayList<Class<?>>();
        for(String location : locations){
            if(location.contains(JAVA_FILE_SUFFIX))
                classes.add(ClassUtils.forName(location.replace(JAVA_FILE_SUFFIX, ""), classLoader));
        }
        return classes.toArray(new Class<?>[classes.size()]);
    }

    protected void prepareContext(GenericApplicationContext context) {
    }
    protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
    }
    protected void customizeContext(GenericApplicationContext context) {
    }

이걸 사용하시면 됩니다. 이제 스프링 이슈에 올려야지. 캬캬캬
스프링 레퍼런스 공부할 때 오타 찾아서 이슈 등록하던게 엊그제 같은데 이젠 코드도 기여할 수 있으려나..

코드를 보시면 아시겠지만, classpath:, file:, url: 등의 prefix 지원은 포기했습니다. 그래도..

1. 스프링 애노테이션 설정 파일만 가지고 쉽게 테스트 할 수 있으며
2. 스프링 애노테이션 설정 파일을 명시적으로 설정할 수 있으며
3. 임의의 패키지를 명시적으로 설정할 수 있습니다.

딱 제가 원하던 만큼이니 이정도면 저는 만족합니다. 이걸 스프링에서 고쳐서 넣어주던 말던~ 일단은 try.

ps: 같이 코딩해주고 상의해준 성윤군과, 스프링 러너와 로더 설정 방법 알려주신 사부님 썡큐!

top

  1. Favicon of http://helols.pe.kr BlogIcon is_Yoon 2009.12.16 14:44 PERM. MOD/DEL REPLY

    유겐이 쌩만 안해줘도 성공..ㅋㅋ

    Favicon of https://whiteship.tistory.com BlogIcon 기선 2009.12.16 15:12 신고 PERM MOD/DEL

    내가 원하는 기능은 만들었으니 이미 성공ㅋ

  2. Favicon of http://blog.lckymn.com BlogIcon Kevin 2009.12.17 19:49 PERM. MOD/DEL REPLY

    본문중...
    『코드를 보시면 아시겠지만, classpath:, file:, url: 등의 prefix 지원은 포기했습니다. 그래도..』

    http://pds3.egloos.com/pds/200707/27/07/c0067007_0307454.jpg

    Favicon of http://whiteship.me BlogIcon 기선 2009.12.17 20:30 PERM MOD/DEL

    throw new UnSupportedException()을 던지고 있지요. 크하핫

Write a comment.


[스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 3

모하니?/Coding : 2009.12.15 19:01


하악 하악.. 배가 너무 고파요. 언능 집에가서 집안일 도와줘야 하는데;; 오늘 너무 늦게 들어가서 와이프 고생할까봐 마음이 아프네요. ㅠ.ㅠ 미안해 내가 코딩을 못해서.. 너무 늦게 들어가게 생겼어;; 나머진 내일 해야지;; 후딱 갈께;; (이것만 쓰고;;)

public class AnnotationContextLoaderTest {

    AnnotationContextLoader acl = new AnnotationContextLoader();

    @Test
    public void generateDefaultLocations(){
        String[] result = acl.generateDefaultLocations(SpringAnnotationConfigTest.class);
        assertThat(result[0], is("sandbox.springtest.sample.SpringAnnotationConfigTestAppConfig.java"));

    }

    @Test
    public void modifyLocations(){
        // absolute location
        String[] result = acl.modifyLocations(SpringAnnotationConfigTest.class, "/sandbox/springtest/sample/SpringAnnotationConfigTestAppConfig.java");
        assertThat(result[0], is("sandbox.springtest.sample.SpringAnnotationConfigTestAppConfig.java"));
        result = acl.modifyLocations(SpringAnnotationConfigTest.class, "/sandbox/springtest/sample/");
        assertThat(result[0], is("sandbox.springtest.sample"));
        result = acl.modifyLocations(SpringAnnotationConfigTest.class, "/sandbox/springtest/sample");
        assertThat(result[0], is("sandbox.springtest.sample"));
        // relative location
        result = acl.modifyLocations(SpringAnnotationConfigTest.class, "./SpringAnnotationConfigTestAppConfig.java");
        assertThat(result[0], is("sandbox.springtest.sample.SpringAnnotationConfigTestAppConfig.java"));
        result = acl.modifyLocations(SpringAnnotationConfigTest.class, "./");
        assertThat(result[0], is("sandbox.springtest.sample"));
        result = acl.modifyLocations(SpringAnnotationConfigTest.class, ".");
        assertThat(result[0], is("sandbox.springtest.sample"));
        // classpath: prefix
//        result = acl.modifyLocations(SpringAnnotationConfigTest.class, "classpath:./SpringAnnotationConfigTestAppConfig.java");
//        assertThat(result[0], is("sandbox.springtest.sample.SpringAnnotationConfigTestAppConfig.java"));
    }

}


경로 가져오는 부분이 복잡하므로, TDD로 테스트! 이런건 테스트 먼저 만들기도 좀 쉬운편이죠.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"/sandbox/springtest/sample/SpringAnnotationConfigTestAppConfig.java"})
//@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"/sandbox/springtest/sample/"})
//@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"/sandbox/springtest/sample"})
//@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"./SpringAnnotationConfigTestAppConfig.java"})
//@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"./"})
//@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"."})
//@ContextConfiguration(loader = AnnotationContextLoader.class)
public class SpringAnnotationConfigTest {

    @Autowired ApplicationContext ac;

    @Test
    public void di(){
        assertNotNull(ac);
        String name = ac.getBean("name", String.class);
        assertNotNull(name);
    }

}

현재 구현한 것으로 사용할 수 있는 방법을 나열해 봤습니다. 주석을 바꿔가면서 테스트 해볼 수 있지요;; 물론 이미 테스트에서 경로 확인은 했지만, 그 뒤에 벌어지는 일에 대해서는 저걸로 통합 테스트;;

구현체는 비공개!!

prefix 붙이는 부분만 처리되면 공개할지도?? 말지도??

top

Write a comment.


[스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 2

모하니?/Coding : 2009.12.15 17:35


가장 먼저 떠올랐던 기본적인 시나리오는 다음과 같습니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationContextLoader.class)
public class SpringAnnotationConfigTest {

    @Autowired ApplicationContext ac;

    @Test
    public void di(){
        assertNotNull(ac);
        String name = ac.getBean("name", String.class);
        assertNotNull(name);
    }

}

이런 테스트가 있고, @ContextConfigruation에 아무런 locations를 지정하지 않았을 때는 테스트 클래스 이름 + AppConfig.java 파일을 찾아서 해당 클래스를 애노테이션 설정 클래스로 인식하는 겁니다.

@Configuration
public class SpringAnnotationConfigTestAppConfig {

    @Bean
    public String name(){
        return "keesun";
    }

}

즉 위와같은 애노테이션 설정 클래스를 기본 설정으로 인식하는거죠. 물론 AppConfig라는 이름을 다른 이름으로 변경할 수 있도록 확장성을 고려해야겠습니다.

그 다음 시나리오는 조금 복잡한데, @ContextConfiguration에 AppConfig 같은 @Configuration을 사용한 자바 설정 파일과 springsprout.org.config 같은 패키지 명을 설정할 수 있게 하는 겁니다.

@ContextConfiguration(locations = {"AppConfig.java", "classpath:../"})

@ContextConfiguration(locations = {"classpath:./AppConfig.java", "../"})

.java로 끝나는 location 정보는 AnnotationConfigApplicationContext의 register를 이용하며, .java로 끝나지 않는 location 정보는 패키지로 인식하여 AnnotationConfigApplicationContext의 scan을 이용해주는 겁니다. 물론, 각각의 resource 정보는 스프링의 Resource prefix인 classpath:, file, url:을 이용할 수 있어야겠습니다.

이제 구현 ㄱㄱㅆ

top

  1. Favicon of http://helols.pe.kr BlogIcon is_Yoon 2009.12.15 17:40 PERM. MOD/DEL REPLY

    성공해야 하는데;;ㅋㅋㅋㅋ

    Favicon of https://whiteship.tistory.com BlogIcon 기선 2009.12.15 18:40 신고 PERM MOD/DEL

    힘들다

Write a comment.


[스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 1

모하니?/Coding : 2009.12.15 17:17


스프링 3.0 @Configuration에 대한 학습 테스트를 만들다 제일 먼저 찾게 된 것이 애노테이션 설정 기반 테스트 러너입니다. 스프링 XML 설정 파일 말고 애노테이션 설정 자바 파일만 주면 되는게.. 있을 줄 알았습니다.

하지만 없었습니다. 그래서 만들기로 마음 먹었습니다. 그리고 잘 만들어지면 스프링 이슈에 올려서 소스 코드를 기증할 생각도 가지고 있습니다.(받아 준다면 말이죠.ㅋ)

일단 처음 할 일은 적당한 확장 지점을 찾는 일인데, 이게 아주 재밌있습니다. 어제 사부님이 올려주신 주옥같은 글에서 스프링 내부에서의 DI 활용이 얼마나 멋진가를 보여주셨는데, 테스트 쪽에서도 그런 모습을 볼 수 있습니다.


스프링 최신 코드(3.0 RC3) 기준이기는 한데 2.5에 추가된 것들이고 그 후에는 변화가 없었습니다. 제가 만들려는 기능과 관련이 있는 클래스만 보고 있습니다. 소스 분석까지 쓰면 너무 길어지고 힘들기 때문에;; 그냥 바로 결론으로 넘어가겠습니다.

1. 처음에는 AGCL 화장을 시도했지만, AnnotationConfigApplicationContext가 내부에서 사용하고 있는 AnnotatedBeanDefinitionReader와 Scanner는 스프링의 BeanDefinitionReader 인터페이스를 따르고 있지 않아서 그럴 수가 없었습니다.

2. 한단계 올라가 AbstractContextLoader를 확장하기로 했습니다.

흠.. 다음은 시나리오를 좀 더 구체적으로 생각해봐야겠습니다.




top

Write a comment.