Mockowanie z użyciem Spring i Mockito
Paweł Kaczor - 3 stycznia 2011
W poniższym wpisie chciałbym przedstawić jak efektywnie skonfigurować testy integracyjne w Spring z użyciem mocków.
Testy integracyjne w Spring
Tworzenie i uruchamianie testów integracyjnych w Spring jest dziecinnie proste dzięki dobrodziejstwom jakie dostarcza Spring TestContext Framework. Zakładając, że plik xml konfiguracji kontekstu aplikacji Spring (Spring Application Context) w module aplikacji, który chcemy testować, nazwaliśmy applicationContext.xml
, uruchomienie testu integracyjnego JUnit dla takiego moduły wygląda następująco:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
"classpath:applicationConfig.xml"
})
public class DoSthTest {
@Test
public void shouldDoSth() {}
}
Jest to najprostszy sposób na przetestowanie poprawności pliku konfiguracji kontekstu aplikacji, w którym zdefiniowane są zależności pomiędzy klasami zarządzanymi przez Spring.
No dobrze, ale chcemy przetestować konkretny serwis, który stworzyliśmy. Zatem do naszego testu wstrzykujemy serwis i tworzymy dla niego metodę testującą:
@Autowired
private DoSthService serviceToTest
@Test
public void shouldReturnSth() {
// when
Object result = serviceToTest.doSth();
// then
assertNotNull(result)
}
Tworzenie mocka
Nasz serwis korzysta z kilku innych serwisów, w tym z serwisu HttpClient
, który w naszym teście musimy zastąpić mockiem. Jednym ze sposobów jest ręczna zmiana zależności w kodzie testu:
@Test
public void shouldReturnSth() {
// given
HttpClient mockHttpClient = Mockito.mock(HttpClient.class);
serviceToTest.setHttpClient(mockHttpClient);
...
}
Sposób ten ma jednak kilka wad:
- Dla każdego testu (metody testującej) musimy sami tworzyć i przypisywać mocka
-
Serwis musi dostarczać settera dla zależności mockowanej, podczas gdy w przypadku wstrzykiwania zależności za pomocą adnotacji setter nie jest wymagany
-
Mockujemy tylko konkretną zależność między dwoma beanami. Jeżeli serwis który mockujemy jest używany przez inny serwis biorący udział w teście (np. DoSthService
używa HttpClient
i OtherService
, a OtherService
używa HttpClient
) , musimy mockować każdą zależność osobno
Lepszym rozwiązaniem jest podmiana serwisu HttpClient
bezpośrednio w kontekście aplikacji. Oczywiście nie możemy zmienić pliku applicationContext.xml
, musimy stworzyć odrębny kontekst aplikacji i wskazać go w konfiguracji naszego testu:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
"classpath:applicationConfig-test.xml"
})
public class DoSthTest {
@Test
public void shouldReturnSth() {
// when
Object result = serviceToTest.doSth();
...
}
}
No dobrze, ale jak stworzyć mocka w pliku konfiguracji kontekstu aplikacji, skoro nasz mock nie jest utworzoną przez nas osobną klasą, ale został wygenerowany automatycznie przez framework Mockito ( Mockito.mock(HttpClient.class)
)?
Rozwiązanie jest proste. Metodę Mockito.mock
należy zadeklarować jako metodę fabrykującą naszego mocka podając jako argument tej metody klasę obiektu mockowanego:
<bean id="httpClient" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.apache.http.client.HttpClient"/>
</bean>
Dostrajanie konfiguracji
Kopiowanie całej konfiguracji kontekstu aplikacji w celu nadpisania jednego serwisu prowadzi do redundancji kodu (kodu konfiguracji), a zatem zwiększa koszt utrzymania aplikacji. Jeżeli chcemy tego uniknąć, możemy zdefiniować dwa pliki konfiguracji kontekstu aplikacji w konfiguracji naszego testu:
@ContextConfiguration(locations = {
"classpath:applicationConfig.xml",
"classpath:applicationConfig-test.xml"
})
public class DoSthTest {
...
}
W pliku applicationConfig-test.xml
definiujemy tylko mocki, które zastąpią istniejące beany w konfiguracji głównej. Zwróćmy uwagę na kolejność deklaracji plików konfiguracji. Ważne jest aby deklaracja pliku konfiguracji mocków następowała po deklaracji pliku głównego konfiguracji.
Autowstrzykiwanie mocków
W przypadku gdy korzystamy z autowstrzykiwania zależności, nasz mock będzie dostępny tylko gdy wstrzykiwanie zależności odbywa się na podstawie nazwy. A zatem mock nie zostanie znaleziony jeśli używamy adnotacji @Autowired
, dla której wstrzykiwanie odbywa się na podstawie typu. Rozwiązaniem może być dodanie kwalifikatora wskazującego nazwę zależności:
@Autowired
@Qualifier(value="httpClient")
private HttpClient httpClient;
Jednak lepszym rozwiązaniem jest zastosowanie adnotacji @javax.annotation.Resource
. W tym przypadku zależność najpierw wyszukiwana jest po nazwie, a w przypadku braku pasującej nazwy, po typie.
Włączanie/wyłączanie mocków
Czasami ten sam test integracyjny chcemy uruchamiać zarówno z serwisem w postaci mocka jak i z serwisem rzeczywistym. Jeśli stosujemy opisaną powyżej metodę nadpisywania beanów z konfiguracji głównej mockami z konfiguracji testowej, włączenie/wyłączenie mocka można łatwo osiągnąć modyfikując plik konfiguracji mocków (dodając bądź usuwając mocka). Jednak co z kodem sterującym zachowaniem mocka (patrz kod poniżej)?
@Autowired
private DoSthService serviceToTest
@Resource
private HttpClient httpClient;
@Test
public void shouldReturnSth() {
// given
//httpClient must be a mock!
given(httpClient.execute(any(HttpUriRequest.class)))
.willReturn(myResponse);
// when
Object result = serviceToTest.doSth();
...
}
Uruchomienie tego kodu na obiekcie nie będącym mockiem zakończy się wyjątkiem. Należy zatem kod sterujący mockiem wykonać warunkowo tylko jeśli obiekt rzeczywiście jest mockiem. Sprawdzenie implementujemy następująco:
// given
if (!httpClient.getClass().isAssignableFrom(HttpClient.class)) {
given(httpClient.execute(any(HttpUriRequest.class)))
.willReturn(myResponse);
}
Współdzielenie mocków
Jeżeli chcielibyśmy użyć tego samego mocka w kilku metodach testowych musimy pamiętać o zresetowania stanu mocka przed każdym testem. Najprostszym rozwiązaniem jest dodanie do klasy testu metody resetMocks
z adnotacją @org.junit.Before
:
@Before
public void resetMocks() {
Mockito.reset(httpClient);
}
Mockowanie częściowe
Na koniec przedstawię w jaki sposób utworzyć w konfiguracji kontekstu aplikacji częściowego mocka, czyli obiekt, którego zachowanie tylko w części chcemy mockować (np. zmieniając tylko rezultat wywołania metody):
<bean id="httpClient" class="org.apache.http.client.HttpClient"/>
<bean id="httpClientPartiallyMocked" class="org.mockito.Mockito" factory-method="spy">
<constructor-arg ref="httpClient" />
</bean>
W tym przypadku tworzymy mocka przy pomocy metody fabrykującej Mockito.spy
, podając jako argument tej metody referencję do istniejącego bean-a.
http://pkaczor.blogspot.com/2010/12/mockowanie-przy-uzyciu-spring-i-mockito.html
Paweł Kaczor
Programista, pasjonat.
Twórca Akka-DDD – frameworku do budowy skalowalnych systemów w architekturze DDD/CQRS/ES. Interesuje się programowaniem funkcyjnym. W wolnym czasie kraulista, szachista amator.
@PavelKaczor
pkaczor.blogspot.com
github.com/pawelkaczor
Comments