sobota 14. února 2009

Springframework mockujeme beany

V tomto článku si ukážeme, jakým způsobem je možné docílit mockování bean (POJO managovaná Springem). Představme si situaci, kdy chceme některé z naších bean pro běh testů nahradit mocky.

Mějme rozhraní FooInterface, jeho implementaci FooBean a třídu BeanWorkingWithFoo, která používá toto rozhraní pro svojí činnost.

public interface FooInterface {
  public String getSomething();
}

public class FooBean implements FooInterface {

  private String something;
  
  public FooBean(String something) {
    super();
    this.something = something;
  }

  public String getSomething() {
    return something;
  }

}

public class BeanWorkingWithFoo {
  private FooInterface fooInterface;

  public FooInterface getFooInterface() {
    return fooInterface;
  }

  public void setFooInterface(FooInterface fooInterface) {
    this.fooInterface = fooInterface;
  }
}      

Konfigurace Springu pak vypadá následovně.



<beans>

  <bean id="foo" class="FooBean">

    <constructor-arg value="foo" />   

  </bean>

  

  <bean class="BeanWorkingWithFoo">

    <property name="fooInterface" ref="foo"/>

  </bean>

</beans>

K testování třídy BeanWorkingWithFoo máme následující kód.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
    "classpath*:META-INF/applicationContext.xml",
     })
public class PrimaryBeanTest {

  @Autowired
  private BeanWorkingWithFoo beanWorkingWithFoo;

  
  @Test
  public void testSomething() {
  }
}

Nyní by jsme rádi zajistili, aby námi testovaná instance BeanWorkingWithFoo používala mock beanu FooInterface. Jaké máme možnosti. Asi nejjednodušší postup spočívá v setup metodě testu, kde nastavíme do instance BeanWorkingWithFoo programově vytvořenou instanci mocku. Bohužel né vždy tomuto řešení kód nahrává. Například nemáme potřebný setter a nebo mockovaný objekt je uhnízděn někde hlouběji v objektové struktuře. Což je sice problém řešitelný, nicméně nikoliv elegantní.

Elegantní řešení by spočívalo v tom, že bychom si nadefinovali testovací kontext, v tomto kontextu nadeklarovali náš mock jako beanu a nechali Spring původně deklarovanou beanu překrýt mockem. To můžeme udělat tak, že námi deklarovaná beana mocku bude mít stejné id jako beana původní a zároveň pokud se kontext s mockem nahraje až jako druhý. BeanFactory se pak postará o to, že původní beana daného id bude překrytá.

Toto řešení má dvě nevýhody. Musíme vždy zaručit, že přepisovaná beana má id, a kontext s definovanými mocky je nahráván jako poslední. O nevýhodu se jedná protože nám specifické požadavky testů diktují implementační detaily. Né vždy totiž chceme, aby naše beany byly referencovány idčkem, ale preferujeme autowiring typem. Druhou nevýhodou je fakt, že při nahrávání kontextů přes wildcard na classpath je složité zaručit jejich správné pořadí (rozhoduje pořadí JARů na classpath).

O autowiringu typem jsem mluvil především ve spojitosti s řízením dependency injection anotacemi, kde je typ jejich defaultním určovatelem. V našem případě, beana BeanWorkingWithFoo může být místo v XML zedklarována pomocí anotací (opomeňmě fakt, že by id referencované beany by šlo vynutit anotaci Qualifier).

@Component
public class BeanWorkingWithFoo {
   @Autowired
  private FooInterface fooInterface;

  public FooInterface getFooInterface() {
    return fooInterface;
  }

  public void setFooInterface(FooInterface fooInterface) {
    this.fooInterface = fooInterface;
  }
}

Kvízová otázka. Co se stane a jak tomu zabránit pokud máme více bean (mock a reálnou implementaci) stejného rozhraní (FooInterface)?

IoC kontejner nám při startu zahlásí, že se nedokáže rozhodnout jaká z bean má být použita a skončí s chybovou hláškou. V tomto případě (vícenásobný výskyt při autowiringu typem) si můžeme pomoci způsobem, na který mě upozornil kolega (díky Sváťo :-), a který by mi asi jinak zůstal skryt v dokumentaci Springu viz sekce Autowiring collaborators. My totiž můžeme beanu našeho mocku označit takzvaně jako primary. Pokud je totiž jedna z bean označena jako primary, pak je automaticky zvolena jako nejvhodnější kandidát pro autowiring.



<beans>

  <bean class="FooBeanMock" primary="true" />       

</beans>

Tímto způsobem jsme ošetřili případy, kdy se používá autowiring typem. Nyní nám zbývá dořešit případy, kdy ovlivnění pořadí kontextů není jednoduché. V tomto případě si pomůžeme využitím jednoho z rozšiřitelných míst IoC kontejneru. Naimplementujeme rozhraní BeanFactoryPostProcessor a díky tomu budeme moci překrytí bean programově. K překrytí pojmenovaných bean využijeme aliasování. To znamená, že beana našeho mocku bude mít alias na idčko původní beany. Tím pádem všechny reference jménem na původní implementační beanu skončí na našem mocku.

public class MockPostProcessor implements BeanFactoryPostProcessor {
  
  public void postProcessBeanFactory(
      ConfigurableListableBeanFactory beanFactorythrows BeansException {
    //Ziskame jmena vsech bean s rozhranim FooInterface
    String beanNames[] = beanFactory.getBeanNamesForType(FooInterface.class);
    
    //Jedna z techto bean je nas mock a tu mi musime najit 
    //resp. jeji jmeno, na ktere ukaze alias. Pro lepsi rozpoznavani
    //jestli je bean mock a nebo neni jsem si udelal specialni
    //anotaci Mock. Diky tomu bezpecne poznam beanu mocku.  
    String mockBeanId = null;
    for(String beanName : beanNames) {
      Object bean =  beanFactory.getBean(beanName);
      if(bean.getClass().getAnnotation(Mock.class!= null) {
        mockBeanId = beanName;
        break;
      }
    }      
    
    //Nyni uz staci nastavit  idcko naseho mocku jako alias
    //pro vsechny puvodni idcka.  
    if(mockBeanId != null) {
      for(String beanName : beanNames) {          
        if(!beanName.equals(mockBeanId)) {//osetren alias sam na sebe
          beanFactory.registerAlias(mockBeanId, beanName);
        }   
      }
    }
  }
}

Takto vytvořená postprocessor přidáme do kontextu k definici mocku.



<beans>

    <bean class="MockPostProcessor"/>

    <bean class="FooBeanMock" primary="true" />    

</beans>

Samozřejmě postprocessor lze neimplementovat obecně, aby dokázal aliasovat všechny nadeklarované mocky.