Mockito w akcji, czyli wykorzystanie frameworka do lifetimera

Napiszemy sobie serwis do obsługi czasu, będziemy go dodawać do repozytorium. Nie zdecydowałem jeszcze jaki nośnik pamięci to będzie dlatego do wytestowania wykorzystamy framework Mockito. Jednocześnie niektóre czasy będą się składały na czasy ogólniejsze.

Zacznijmy od stworzenia atrapy obiektu interfejsu Repository. Interfejs na razie jest pusty ponieważ nie wiem jakie metody będziemy potrzebować. Wykorzystamy do tego adnotację @Mock, ktory działa na zasadzie Dependency Injection.

package pl.devpragmatic.lifetimer.service;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.Mock;
import pl.devpragmatic.lifetimer.repository.TimeRepository;
/**
* @author devpragmatic
*
*/
@RunWith(MockitoJUnitRunner.class)
public class TimeServiceImplTest {
@Mock
private TimeRepository timeRepository;
}

Następnie utworzymy obiekt naszego serwisu. Obiekty tego typu z założenia nie powinny przechowywać stanu wewnętrznego. Wykorzystując to stworzymy jeden obiekt dla wszystkich testów. Potrzebujemy jednocześnie, żeby stworzony w teście Mock został wstrzyknięty do naszej klasy serwisowej. Używamy do tej czynności adnotacji @InjectMocks.

package pl.devpragmatic.lifetimer.service;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.Mock;
import pl.devpragmatic.lifetimer.repository.TimeRepository;
/**
* @author devpragmatic
*
*/
@RunWith(MockitoJUnitRunner.class)
public class TimeServiceImplTest {
@InjectMocks
private final TimeService timeService = new TimeServiceImpl();
@Mock
private TimeRepository timeRepository;
}

Ok. Mamy już stworzone puste obiekty. Chcielibyśmy jednak, aby coś one robiły. Bierzmy się więc do roboty. Napiszmy test, że wraz z użyciem w serwisie metody "add" z obiektem Time, uzyskamy efekt, że ten obiekt został zapisany do repozytorium. Ustalmy sobie, że zapis będzie po użyciu metody save.

package pl.devpragmatic.lifetimer.service;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import pl.devpragmatic.lifetimer.domain.Time;
import pl.devpragmatic.lifetimer.repository.TimeRepository;
import pl.devpragmatic.lifetimer.service.exception.ServiceException;
/**
* @author devpragmatic
*
*/
@RunWith(MockitoJUnitRunner.class)
public class TimeServiceImplTest {
@InjectMocks
private final TimeService timeService = new TimeServiceImpl();
@Mock
private TimeRepository timeRepository;
private Time time;
@Before
public void setUp() {
time = new Time();
}
@After
public void tearDown(){
Mockito.reset(timeRepository);
}
@Test
public void givenSomethingTimeWhenUseAddThenSaveIt() {
time = Mockito.mock(Time.class);
timeService.add(time);
Mockito.verify(timeRepository).save(time);
}
}

Wytłumaczę co tutaj stworzyliśmy. W setUp inicjalizuje się obiekt, będzie on nowy przy wywołaniu każdego testu. Dzieje się tak, ponieważ jest nad funkcją użyta adnotacja @Before. Następna metoda to tearDown. Tutaj resetuje się Mock repozytorium po każdym teście. Służy do tego funkcja reset(Mock mock). Analogicznie do poprzedniego przypadku wykonuje się to po testach, ponieważ jest umieszczona adnotacja @After. Wykorzystujemy to, ponieważ brak resetu mógłby spowodować napisanie testów od siebie zależnych lub koligujących ze sobą.

Przejdźmy teraz do samego testu. Pierwsza linijka to stworzenie nowej atrapy obiektu klasy Time. Dlaczego mock, a nie normalny obiekt? Ponieważ w trakcie wykonywania veryfi, możemy jednoznacznie sprawdzić czy ten konkretny obiekt został zapisany do repozytorium. W tym przykładzie akurat jest to niepotrzebne, ponieważ equals w obiecie Time jest na razie dziedziczony z klasy Object, ale dobre praktyki używania Mockito warto sobie wpoić od początku.

Następnie wywołujemy naszą metode, której poprawność weryfikujemy. Tutaj wywołujemy ją z naszym wcześniej utworzonym mockiem. Pomoże nam to w weryfikacji.

Po wykonaniu funkcji czas na sprawdzenie czy wykonała się poprawnie. Wiemy, że powinna wywołać funkcje save(Time time) z naszym mockiem jeden raz. Sprawdzamy, więc to w naszym teście. Używamy do tego funkcji Mockito.veryfi(Mock). Domyślnym sprawdzeniem tej funkcji jest jednorazowe wykonanie (times(1)). Do sprawdzenia innych warunków używamy jako drugi argument VerificationMode. Zdefiniowane są never(), only(), times(int wantedNumberOfInvocations), atLeast(int minNumberOfInvocations), atLeastOnce(), atMost(int maxNumberOfInvocations), calls(int wantedNumberOfInvocations). Po wykonaniu verifi z odpowiednim walidatorem oraz mockiem na zwróconym obiekcie wykonujemy metodę, którą chcemy wytestować.

Implementacja logiki do tego testu wygląda następująco:

package pl.devpragmatic.lifetimer.service;
import pl.devpragmatic.lifetimer.domain.Time;
/**
* @author devpragmatic
*
*/
public interface TimeService {
/**
* Adding time
* @param time object time
*/
public void add(Time time);
}
package pl.devpragmatic.lifetimer.service;
import javax.inject.Inject;
import pl.devpragmatic.lifetimer.domain.Time;
import pl.devpragmatic.lifetimer.repository.TimeRepository;
/**
* @author devpragmatic
*
*/
public class TimeServiceImpl implements TimeService {
@Inject
private TimeRepository timeRepository;
/* (non-Javadoc)
* @see pl.devpragmatic.lifetimer.service.TimeService.add(pl.devpragmatic.lifetimer.domain.Time)
*/
public void add(Time time) {
timeRepository.save(time);
}
}

Zaimplementujmy sobie teraz dodawanie wartości czasowych do naszego obiektu. Pola, które klasa Time powinna posiadać to dni, godziny, minuty i sekundy. Jeszcze lepiej byłoby, gdyby sekundy były przeliczane na minuty, jeżeli przekracza powyżej wartości 59 i analogicznie dla innych pól. Dodatkowo dobrą funkcjonalnością byłoby dodawanie i odejmowanie czasu od innego czasu. Kolejnym założeniem będzie, że nie ma czasów minusowych. Najmniejszy czas jaki może posiadać obiekt to czas 0. Ok, założeń jest dużo. Umieszczony jest tutaj efekt końcowy napisany w stylu TDD.

package pl.devpragmatic.lifetimer.domain;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import pl.devpragmatic.lifetimer.builder.TimeBuilder;
/**
*
* @author devpragmatic
*/
public class TimeTest {
private Time time;
@Before
public void setUp() {
time = new Time();
}
@Test
public void givenTwentyFiveHoursWhenUseSetTimeThenSetOneDayAndOneHourAndZeroOther() {
time.setTime(0,25,0,0);
equalsTime(1, 1, 0, 0, time);
}
@Test
public void givenSeventyHoursWhenUseSetTimeThenSetTwoDaysAndTwentyTwoHours() {
time.setTime(0,70,0,0);
equalsTime(2, 22, 0, 0, time);
}
@Test
public void givenTwoHundredMinutesWhenUseSetTimeThenSetThreeHoursAndTwentyMinutes() {
time.setTime(0,0,200,0);
equalsTime(0, 3, 20, 0, time);
}
@Test
public void givenThreeThousandMinutesWhenUseSetTimeThenSetTwoDaysAndTwoHours() {
time.setTime(0,0,3000,0);
equalsTime(2,2,0,0, time);
}
@Test
public void givenSixThousandTenSecondsWhenUseSetTimeThenSetOneHoursFourtyMinutesTenSeconds() {
time.setTime(0,0,0,6010);
equalsTime(0,1,40,10, time);
}
@Test
public void givenEightySixThousandAndFourHundredSecondsWhenUseSetTimeThenSetOneDay() {
time.setTime(0,0,0,86400);
equalsTime(1,0,0,0, time);
}
@Test
public void givenTimeWhenUseAddTimeThenAddThisTime(){
time.addTime(new TimeBuilder().appendSeconds(86400).toTime());
equalsTime(1,0,0,0, time);
}
@Test
public void givenMinusTimeWhenUseSetTimeThenSetTimeZero(){
time.setTime(-1, 25, -70, 0);
equalsTime(0, 0, 0, 0, time);
time.setTime(-1, 25, -59, -60);
equalsTime(0, 0, 0, 0, time);
}
@Test
public void givenTimeWhenUseSubTimeThenSubThisTime(){
time = new TimeBuilder().appendDays(2).toTime();
time.subTime(new TimeBuilder().appendHours(2).appendSeconds(4).toTime());
equalsTime(1, 21, 59, 56, time);
}
@Test
public void givenNullTimeWhenUseSubTimeThenDoNothing(){
time = new TimeBuilder().appendDays(2).appendMinutes(3).toTime();
time.subTime(null);
equalsTime(2, 0, 3, 0, time);
}
@Test
public void givenNullTimeWhenUseAddTimeThenDoNothing(){
time = new TimeBuilder().appendDays(4).appendMinutes(5).toTime();
time.addTime(null);
equalsTime(4, 0, 5, 0, time);
}
private void equalsTime(int days, int hours, int minutes, int seconds, Time time) {
assertEquals(days, time.getDays());
assertEquals(hours, time.getHours());
assertEquals(minutes, time.getMinutes());
assertEquals(seconds, time.getSeconds());
}
}
view raw TimeTest.java hosted with ❤ by GitHub
package pl.devpragmatic.lifetimer.domain;
import pl.devpragmatic.lifetimer.builder.TimeBuilder;
public class Time {
private final int HOURS_IN_DAY = 24;
private final int MINUTES_IN_HOUR = 60;
private final int SECONDS_IN_MINUTE = 60;
private int days;
private int hours;
private int minutes;
private int seconds;
private String parentId;
public void setTime(int days, int hours, int minutes, int seconds) {
minutes = seconds/SECONDS_IN_MINUTE + minutes;
hours = minutes/MINUTES_IN_HOUR + hours;
days = hours/HOURS_IN_DAY + days;
this.days = days;
this.hours = hours%HOURS_IN_DAY;
this.minutes = minutes%MINUTES_IN_HOUR;
this.seconds = seconds%SECONDS_IN_MINUTE;
if(this.seconds < 0){
this.seconds += SECONDS_IN_MINUTE;
this.minutes--;
}
if(this.minutes < 0){
this.minutes += MINUTES_IN_HOUR;
this.hours--;
}
if(this.hours < 0){
this.hours += HOURS_IN_DAY;
this.days--;
}
if(this.days < 0){
reset();
}
}
public int getDays() {
return days;
}
public int getHours() {
return hours;
}
public int getMinutes() {
return minutes;
}
public int getSeconds() {
return seconds;
}
public String getParentId() {
return parentId;
}
public boolean hasParent() {
return parentId != null;
}
public void setParentId(String parentId) {
this.parentId = parentId;
}
public void addTime(Time time) {
if(time != null){
this.days += time.days;
this.hours += time.hours;
this.minutes += time.minutes;
this.seconds += time.seconds;
this.setTime(days, hours, minutes, seconds);
}
}
public void subTime(Time time) {
if(time != null){
this.days -= time.days;
this.hours -= time.hours;
this.minutes -= time.minutes;
this.seconds -= time.seconds;
this.setTime(days, hours, minutes, seconds);
}
}
private void reset(){
this.days = 0;
this.hours = 0;
this.minutes = 0;
this.seconds = 0;
}
}
view raw Time.java hosted with ❤ by GitHub

Załóżmy, że chcielibyśmy ten czas dodawać do czasu ogólnego np. jakiejś kategorii. Jeszcze nie posiadamy podziału na kategorię, ale możemy założyć, że czas będzie miał rodzica, który będzie właśnie czasem ogólniejszym.

Test tego przypadku będzie wyglądał następująco:

package pl.devpragmatic.lifetimer.service;
import java.util.UUID;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import static org.mockito.Matchers.anyString;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import pl.devpragmatic.lifetimer.domain.Time;
import pl.devpragmatic.lifetimer.repository.TimeRepository;
import pl.devpragmatic.lifetimer.service.exception.ServiceException;
/**
* @author devpragmatic
*
*/
@RunWith(MockitoJUnitRunner.class)
public class TimeServiceImplTest {
@InjectMocks
private final TimeService timeService = new TimeServiceImpl();
@Mock
private TimeRepository timeRepository;
private Time time;
private final String parentId = UUID.randomUUID().toString();
@Before
public void setUp() {
time = new Time();
}
@After
public void tearDown() {
Mockito.reset(timeRepository);
}
@Test
public void givenSomethingTimeWhenUseAddThenSaveIt() {
time = Mockito.mock(Time.class);
timeService.add(time);
Mockito.verify(timeRepository).save(time);
}
@Test
public void givenSomethingTimeWithParentWhenUseAddThenAddTimeToParent() {
int days = 1, hours = 2, minutes = 3, seconds = 4;
time = new Time();
time.setTime(days, hours, minutes, seconds);
time.setParentId(parentId);
Time parent = Mockito.mock(Time.class);
Mockito.when(timeRepository.findOneById(time.getParentId())).thenReturn(parent);
timeService.add(time);
Mockito.verify(timeRepository).save(time);
Mockito.verify(timeRepository).findOneById(time.getParentId());
Mockito.verify(parent).addTime(time);
Mockito.verify(timeRepository).save(parent);
}
}

W teście pojawiła się nowa funkcjonalność Mockito. Funkcja when(T methodCall), która pozwala na przypisanie odpowiedniego zachowania dla podanej funkcji. Warunek jest taki, że musi być funkcją atrapą (stubbing). Możemy to osiągnąć poprzez stworzenie atrapy obiektu (mock(T class)) lub obiektu szpiegowanego (spy(T object)). Po wykonaniu tej funkcji możemy przypisać, co ma być zwracane. Zdefiniowane są then(Answer<?> answer), thenAnswer(Answer<?> answer), thenCallRealMethod(), thenReturn(T value), thenReturn(T value, T... values), thenThrow(Class<? extends Throwable> throwableType), thenThrow(Class<? extends Throwable> toBeThrown, Class<;? extends Throwable>... nextToBeThrown) i thenThrow(Throwable... throwables).

Podsumowując powiedzieliśmy testowi, że jeżeli funkcja otrzyma parentId, który został przekazany to ma zwrócić naszego zamockowanego Time parent.

Ciekawą funkcją jest jeszcze UUID.randomUUID(), tworzy ona unikatową wartość z bardzo małym prawdopodobieństwem wygenerowania identycznego. W celu nie wymyślania identyfikatora, użyliśmy właśnie tej opcji do genereacji jego.

Logika stworzona w celu zaspokojenia testu wygląda następująco:

package pl.devpragmatic.lifetimer.service;
import javax.inject.Inject;
import pl.devpragmatic.lifetimer.domain.Time;
import pl.devpragmatic.lifetimer.repository.TimeRepository;
/**
* @author devpragmatic
*
*/
public class TimeServiceImpl implements TimeService {
private final String PARENT_EXCEPTION_CODE = "bad.parent.time";
@Inject
private TimeRepository timeRepository;
/* (non-Javadoc)
* @see pl.devpragmatic.lifetimer.service.TimeService.add(pl.devpragmatic.lifetimer.domain.Time)
*/
public void add(Time time) {
if (time != null) {
timeRepository.save(time);
if (time.hasParent()) {
addTimeToParent(time);
}
}
}
private void addTimeToParent(Time time) {
Time parent = timeRepository.findOneById(time.getParentId());
parent.addTime(time);
timeRepository.save(parent);
}
}

To tyle na dzisiaj polecam zapoznać się z teorią na temat frameworku Mockito. Projekt będzie kontynuowany i jest umieszczony na github.

Comments

Popular posts from this blog

My mistakes in working with legacy code

Don't change project, change yourself.

Spring Framework - skąd jego popularność?