Jak se zbavit nepříjemných závislostí v testech

Dnešní příspěvek bude velmi krátký. Je dost pravděpodobné, že podobné řešení už dávno máte ve svých tetovacích utilitkách, ale mě tato kombinace napadla relativně nedávno a jsem nadšený z toho, o jak elegantní řešení se pro testy jedná.

V některých testech potřebuji vytvořit část Spring aplikačního kontextu, jehož některé beany mají závislost na nějaké další beaně, kterou je pro mne obtížné do testu zahrnout. Buď z důvodu, že její samotné vytvoření s vyžaduje další komplexní infrastrukturu okolo ní nebo třeba proto, že její zařazení do testovacího kontextu způsobuje při běhu testu vedlejší efekty (např. odeslání e-mailu).

Pro tyto případy jsem si vytvořil jednoduchou Spring FactoryBean, která používá mockovací knihovnu Mockito a která vytvoří místo zmíněné obtížné beany, na kterou mám v kontextu závislosti, dynamickou proxy imitující její chování:


import org.springframework.beans.factory.FactoryBean;
import static org.mockito.Mockito.mock;
/**
 * Simple factory bean creating mock object for specified class.
 * Mock object is a prototype that means it is created
 * everytime Spring bean retrieval occurs. In practice it means
 * that each test method has its own new and pretty mock
 * object created before it starts.
 */
public class MockitoFactoryBean implements FactoryBean {
	private Class mockClass;
	public void setMockClass(Class mockClass) {
		this.mockClass = mockClass;
	}
	public Object getObject() throws Exception {
		return mock(mockClass);
	}
	public Class getObjectType() {
		return mockClass;
	}
	public boolean isSingleton() {
		return false;
	}
}

V úplně nejjednodušším případě mi potom stačí v kontextu místo původní beany definovat beanu zástupnou - díky níž umožním naběhnutí celého kontextu aniž bych musel nějak výrazněji šachovat se Spring konfiguráky:


<bean id="mailService" class="com.fg.support.test.MockitoFactoryBean">
<property name="mockClass" value="cz.novoj.mail.MailService"/>
</bean>

Druhé hezké použití tohoto přístupu je ve chvíli, kdy potřebujeme pro test nainstruovat chování konkrétní mockované beany. Díky tomu, že MockitoBeanFactory vytváří beany typu "prototype" (tj. vždy, když požádáme kontext o referenci na danou beanu, vznikne nová instance konkrétní třídy), má každá testovací metoda na začátku svou vlastní "čistou" instanci tohoto mocku, který už stačí jen naskriptovat:


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
		"classpath:/META-INF/spring/business.xml",
		"classpath:/META-INF/spring/mocks.xml"
})
public class BusinessObjectTest {
	@Autowired private BusinessObject businessObject;
	@Autowired private MailService mailService;
	@Test
	@DirtiesContext
	public void testBusinessMethod() {
		Mockito.when(mailService.sendMail(Mockito.anyObject())).thenReturn(Boolean.TRUE);
		businessObject.setMailService(mailService);
		assertTrue(businessObject.doWork());
		Mockito.verify(mailService).sendMail(Mockito.anyObject());
	}
	@Test
	@DirtiesContext
	public void testBusinessMethodMailFail() {
		Mockito.when(mailService.sendMail(Mockito.anyObject())).thenThrow(new MailSendException("test"));
		businessObject.setMailService(mailService);
		assertFalse(businessObject.doWork());
	}
}

Jak jsem psal už zkraje - není to nic zázračného, ale použití je skutečně velmi elegantní. Nechápu, že mi něco podobného nedoteklo daleko dřív. Alternativně by se dala použít ještě ProxyFactoryBean, jenže ta je spíš určena na hrátky s AOP než jako podpora testů. Mockito (respektive libovolná jiná mock knihovna) se pro tyto účely hodí výrazně lépe.