29 February 2012

Создание spring beans из обычных классов и юнит тесты

Java
У нас и rich client, и сервер активно используют Spring. И очень быстро возникла проблема — как использовать спринг бины из обычных классов (которые сами — не бины).

Сначала возникли две идеи — передавать им нужные бины как аргументы в конструкторе или использовать какое то статическое поле для хранения Spring context.
Первая идея была признана порочной. Получается, что ныжные сервисы надо тянуть через длинную череду конструкторов.
Вторая идея тоже была признана порочной — возникает вопрос кто и когда будет инициализировать это поле и что будет происходить с юнит тестами.

Вскоре я нагуглил в интернетах такой красивый вариант:


@Service
public class StaticContextHolder implements BeanFactoryAware {

    public static BeanFactory CONTEXT;

    public StaticContextHolder() {
    }

    public static Object getBean(String s) throws BeansException {
        return CONTEXT.getBean(s);
    }

    public static <T> T getBean(String s, Class<T> tClass) throws BeansException {
        return CONTEXT.getBean(s, tClass);
    }

    public static <T> T getBean(Class<T> tClass) throws BeansException {
        return CONTEXT.getBean(tClass);
    }

    public static Object getBean(String s, Object... objects) throws BeansException {
        return CONTEXT.getBean(s, objects);
    }

    public static boolean containsBean(String s) {
        return CONTEXT.containsBean(s);
    }


    @Override
    public void setBeanFactory(BeanFactory applicationContext) throws BeansException {
        logger.assertNull(CONTEXT, "CONTEXT is not null. Double Spring context creation?");
        CONTEXT = applicationContext;
    }

}



И это работает отлично.
Однако же для юнит тестов пришлось это всё немного модифицировать.

У нас есть тесты, которые создают спринг контекст. Поэтому я добавил в этот класс такое метод:

   @PreDestroy
    public void resetStatics() {
                  CONTEXT=null;
    }


Во вторых, если юнит тест не создаёт спринг котекст, но тестируемый класс использует StaticContextHolder, надо чтобы зависимости он получал из него.

Я сделал свой фиктивный контекст:
public class FakeBeanFactory implements BeanFactory {

    private Map<String, Object> beans;

    public FakeBeanFactory (Map<String, Object> beans) {
         this.beans = beans;
    }

    @Override
    public Object getBean(String s) throws BeansException {
        return beans.get(s);
    }

    @Override
    public <T> T getBean(String s, Class<T> tClass) throws BeansException {
        return (T) beans.get(s);
    }

    @Override
    public <T> T getBean(Class<T> tClass) throws BeansException {
        return (T) beans.get(tClass.getName());
    }

    @Override
    public Object getBean(String s, Object... objects) throws BeansException {
        return beans.get(s);
   }

    @Override
    public boolean containsBean(String s) {
        return false; // мне он не нужен
    }

    @Override
    public boolean isSingleton(String s) throws NoSuchBeanDefinitionException {
        return false; // мне он не нужен
    }

    @Override
    public boolean isPrototype(String s) throws NoSuchBeanDefinitionException {
        return false; // мне он не нужен
    }

    // .... 
}
   



Теперь инициализация юнит теста выглядит так:
  @Before
  public void init() {
       Map<String,Object> beans = new Map<String,Object>();
       beans.put("service-dependency", new MockupDependencyImpl());
       StaticContextHolder.CONTEXT = new FakeBeanFactory(beans));
  }


Теперь остаётся еще одна проблема: prototype beans, которые создаются через init method, например
 <bean id="PlanDefinitionReader" class="com.example.PlanDefinitionReader"
              scope="prototype"
              factory-method="createPlanDefinitionReader">
            <constructor-arg index="0" value="null"/>
            <constructor-arg index="1" value="null"/>
            <constructor-arg index="2" value="null"/>
        </bean>


Для этого заведём в FakeBeanfactory еще один Map:
Map<String s, Method m> initMethods


и перепишем один метод:
    public Object getBean(String s, Object... objects) throws BeansException {
        return initMethods.get(s).invoke(null, objects);
   }


и инициализируем этот Map статических методов его в инициализации юнит теста.

Ну вот. Как то так.

Буду рад, если кто то подскажет как лучше решать все эти проблемы.
Tags:javaspringunit testingunit test
Hubs: Java
+1
5.1k 19
Comments 9
Popular right now
Top of the last 24 hours