12. dubna 2010

Udělátko na vytváření mock objektů

Pro vytváření mock objektů používám knihovnu Mockito. Pokud mám testovaný objekt O1, který obsahuje referenci na objekt O2, ze kterého chci vytvořit mock objekt, pak není žádný problém. Vytvořím si mock objektu O2, který pak nasetuji do objektu O1.

Co když ale potřebuji vytvořit mock objekt, který je volán až někde na desáté úrovni hierarchie volání? V tomto případě není možné se k cílovému objektu dostat přes hierarchii objektů a nasetovat mock objekt. Proto mě napadlo si udělat takové udělátko, které to dokáže.

Uvedená funkce má následující omezení:
- třídy resp. beany musí být inicializovány pomocí Spring kontejneru
- aby bylo možné najít beanu požadovaného typu, tak daného typu musí být právě jedna
- pro vytváření mock objektů se používá knihovna Mockito (ale není žádný problém to upravit na libovolnou jinou mockovací knihovnu)

Zdrojový kód:

import static org.mockito.Mockito.spy;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.test.util.ReflectionTestUtils;

...


/**
* Umoznuje mockovat realny Spring bean a tim si upravit cilovy objekt dle sve potreby.
*
* <p><b>Pokud cilovy objekt (field) bude AOP proxy, pak se bude vytvaret mock nad "skutecnym" objektem
* a ne proxy - vysledne volani tohoto objektu nebude tedy pres AOP proxy, ale primo.</b>
*
* @param bf Bean factory pro pristup ke Spring kontejneru
* @param beanClass Typ Spring beany, kterou chceme v kontejneru najit a u ktere chceme zmenit
* nejaky field a misto toho podstrcit mock instanci
* @param fieldName Nazev fieldu (pristup k fieldu je pres Java reflexi)
* @param spyCallback Rozhrani pro implementaci mocku
* @param <BC> Typ beanu, ktery obsahuje nejaky field typu FC, u ktereho chceme menit chovani
* @param <FC> Typ objektu fieldu
* @return mock objekt (hodi se napriklad pro overeni volani metod, vstupnich parametru metod atd.
* @see <a href="http://mockito.googlecode.com/svn/tags/latest/javadoc/org/mockito/Mockito.html#13">Spying on real objects</a>
*/
public static <BC, FC> FC spyBean(ListableBeanFactory bf,
Class<BC> beanClass,
String fieldName,
SpyCallback<FC> spyCallback) {
return TestUtils.spyBean(bf, beanClass, fieldName, null, spyCallback);
}


/**
* Umoznuje mockovat realny Spring bean a tim si upravit cilovy objekt dle sve potreby.
*
* <p><b>Pokud cilovy objekt (field) bude AOP proxy, pak se bude vytvaret mock nad "skutecnym" objektem
* a ne proxy - vysledne volani tohoto objektu tedy nebude pres AOP proxy, ale primo.</b>
*
* @param bf Bean factory pro pristup ke Spring kontejneru
* @param beanClass Typ Spring beany, kterou chceme v kontejneru najit a u ktere chceme zmenit
* nejaky field a misto toho podstrcit mock instanci
* @param fieldName Nazev fieldu (pristup k fieldu je pres Java reflexi)
* @param fieldClass Typ fieldu, muze byt null. Pokud bude definovan a field bude null, tak pak se
* automaticky vytvori nova instance objektu.
* @param spyCallback Rozhrani pro implementaci mocku
* @param <BC> Typ beanu, ktery obsahuje nejaky field typu FC, u ktereho chceme menit chovani
* @param <FC> Typ objektu fieldu
* @return mock objekt (hodi se napriklad pro overeni volani metod, vstupnich parametru metod atd.
* @see <a href="http://mockito.googlecode.com/svn/tags/latest/javadoc/org/mockito/Mockito.html#13">Spying on real objects</a>
*/
@SuppressWarnings("unchecked")
public static <BC, FC> FC spyBean(ListableBeanFactory bf,
Class<BC> beanClass,
String fieldName,
Class<? extends FC> fieldClass,
SpyCallback<FC> spyCallback) {
//najdeme beanu daneho typu (danemu typu musi odpovidat prace jedna beana)
Map beanMap = bf.getBeansOfType(beanClass);

if (beanMap.size() != 1) {
throw new IllegalStateException("Zadanemu typu " + beanClass + " odpovida vice nebo zadny bean(u).");
}


//mame (pravdepodobne) proxy objekt - pokud ano, tak najdeme cilovy objekt
BC proxy = (BC) beanMap.values().iterator().next();

BC target = proxy;
if (AopUtils.isAopProxy(proxy)) {
try {
target = (BC) ((Advised)proxy).getTargetSource().getTarget();
}
catch (Exception ex) {
throw new RuntimeException("Problem pri ziskavani ciloveho objektu proxy objektu.", ex);
}
}


//ziskame cilovy objekt, ktery chceme mockovat
FC field = (FC) ReflectionTestUtils.getField(target, fieldName);

if (field == null && fieldClass == null) {
throw new IllegalStateException("Nactena hodnotu fieldu '" + fieldName + "' nemuze byt null.");
}
else if (field == null) {
//vytvorim automaticky instanci fieldu
try {
field = fieldClass.newInstance();
}
catch (Exception ex) {
throw new IllegalArgumentException("Nepodarilo se vytvorit instanci tridy '" + fieldClass + "'.");
}
}
else {
//field mame nacteny - kontrola, zda to opet neni AOP proxy (potrebuji mockovat skutecny cilovy objekt)
if (AopUtils.isAopProxy(field)) {
try {
field = (FC) ((Advised)field).getTargetSource().getTarget();
}
catch (Exception ex) {
throw new RuntimeException("Problem pri ziskavani ciloveho objektu fieldu.", ex);
}
}
}


FC spyObjekt = spy(field);

//klient musi urcit cilove chovani objektu
spyCallback.spy(spyObjekt);

//upraveny objekt nasetujeme zpatky do ciloveho objektu
ReflectionTestUtils.setField(target, fieldName, spyObjekt);

return spyObjekt;
}


/**
* Rozhrani pro implementaci mocku nad instanci objektu.
*
* @param <T> Typ objektu, u ktereho chceme zmenit chovani (vytvorit mock)
* @author <a href="mailto:petr.juza@marbes.cz">Petr Juza</a>
* @see <a href="http://mockito.googlecode.com/svn/tags/latest/javadoc/org/mockito/Mockito.html#13">Spying on real objects</a>
*/
public interface SpyCallback<T> {

/**
* Metoda pro implemtaci "noveho chovani" vstupniho mock objektu.
*
* <p>Na vstupnim objektu byla jiz zavolana metoda {@link org.mockito.Mockito#spy}, takze
* staci jen implementovat upravu chovani zadaneho objektu.
*
* @param spyObjekt Objekt
*/
void spy(T spyObjekt);
}


Pro názornost uvedu příklad:
Bean typu AaSavePripad.class má atribut (field) se jménem "typPripadService" typu TypPripadService. Já pro test potřebuji, aby volání metody TypPripadService.mohuVytvoritPripadTypu vracelo vždy true.

//potrebuji, aby volani typPripadService.mohuVytvoritPripadTypu bylo TRUE (z duvodu dokonceni ulozeni pripadu)
TypPripadService spyObjekt = TestUtils.spyBean(applicationContext, AaSavePripad.class, "typPripadService",
new SpyCallback<TypPripadService>() {
@Override
public void spy(TypPripadService spyObjekt) {
doReturn(true).when(spyObjekt).mohuVytvoritPripadTypu(eq(odpad.getPripad().getTyp_Pripad()), anyInt());
}
});



K tomuto tématu jsem našel článek od Dagiho "Springframework mockujeme beany", který řeší problém trochu jiným způsobem. Mě se na uvedeném řešení moc nelíbí, že pokud chci upravit chování nějaké metody, tak si musím vytvořit celý mock objekt a musím vše konfigurovat. Navíc nemám možnost mít různé podoby resp. chování jednoho mock objektu pro různé testy v rámci jedné testovací třídy (protože XML kontext se definuje na úrovni třídy a ne jednotlivých testů).

Žádné komentáře: