Może od razu przykład. Dla klasy:
public class Dorosly {
@Range(min = 18, max = 100)
private Integer wiek;
}
Definicja komunikatu może wyglądać tak:
org.hibernate.validator.constraints.Range.message = pole musi byc z przedzialu {min} - {max}
"In theory, there is no difference between theory and practice. But, in practice, there is." Jan L. A. van de Snepscheut
public class Dorosly {
@Range(min = 18, max = 100)
private Integer wiek;
}
Definicja komunikatu może wyglądać tak:
org.hibernate.validator.constraints.Range.message = pole musi byc z przedzialu {min} - {max}
@GenerateBuilder
public class Order {
private List<OrderItem> items;
private Date createDate;
private boolean realized;
public boolean isRealized() {
return realized;
}
public void orderRealized() {
// very complex implementation
realized = true;
}
}
Żeby w jakiś sposób automatycznie aktualizować istniejące buildery o nowe pola, które doszły w klasie Order (lub odpowiednio usuwać metody inicjujące nieistniejące pola), builder został rozbity na 2 klasy. Jedna z nich jest aktualizowana zawsze przy uruchomieniu procesora, druga generowana tylko raz, dzięki czemu możemy do niej dodawać własne metody budujące. Dla klasy Order powstaną następujące klasy:
public abstract class AbstractOrderBuilder<B> extends AbstractBuilder<Order, B> {
public abstract B withItems(List<OrderItem> items);
public abstract B withCreateDate(Date createDate);
public abstract B withRealized(boolean realized);
public B withItems(OrderItem... items){
return withItems(new ArrayList<OrderItem>(Arrays.asList(items)));
}
}
public abstract class OrderBuilder extends AbstractOrderBuilder<OrderBuilder> {
public static OrderBuilder anOrder(){
return AbstractBuilderFactory.createImplementation(OrderBuilder.class);
}
}
Samo użycie może wyglądać następująco:
@Test
public void shouldCreateRealizedOrder() {
// when
Order order = anOrder().withRealized(true).build();
// then
assertTrue(order.isRealized());
}
Klasę OrderBuilder można rozszerzać, wywołując rzeczywiste metody domenowe, np:
public abstract class OrderBuilder extends AbstractOrderBuilder<OrderBuilder> {
public static OrderBuilder create() {
return AbstractBuilderFactory.createImplementation(OrderBuilder.class);
}
public OrderBuilder realized() {
Order order = targetObject();
// invoking real domain method
order.orderRealized();
// other methods
return builder();
}
}
@Test
public void shouldCreateRealizedOrder() {
// when
Order order = anOrder().realized().build();
// then
assertTrue(order.isRealized());
}
Do podłączenia procesora do projektu może zostać wykorzystany ant, maven, eclipse. Jest również możliwość użycia generatora w starym stylu, czyli wygenerowania ciała klasy buildera do konsoli.@Rule
public Clock clock = Clock.standard();
@Test
public void should() {
// given
DateTime firstDate = new DateTime();
DateTime secondDate = firstDate.plusDays(1);
clock.setFixedTime(firstDate);
Order firstOrder = new Order();
clock.setFixedTime(secondDate);
Order secondOrder = new Order();
// when
// then
}
W rezultacie mamy 2 zamówienia z różnymi datami ich utworzenia. Jeśli nie zależny nam na pełnej kontroli tych dat, to można to zrobić trochę bardziej fancy, np.
clock.afterDay(); Order secondOrder = new Order();Można również wykorzystać MillisProvider'a i zaimplementować go tak aby każde kolejne wywołanie zwracało "datę" (tj. longa) przesuniętą względem poprzedniej np. o 1 milisekundę. Symulujemy tym samym upływ czasu i mamy pewności, że wywołanie new DateTime() zawsze zwróci nam późniejsza datę.
public class Clock extends ExternalResource {
private static DateTime currentTime;
private MillisProvider millisProvider;
public static final MillisProvider ONE_MILLIS_INTERVAL = new MillisProvider() {
@Override
public long getMillis() {
currentTime = currentTime.plusMillis(1);
return currentTime.getMillis();
}
};
private Clock() {}
public Clock(MillisProvider millisProvider) {
this.millisProvider = millisProvider;
}
public static Clock standard() {
return new Clock();
}
public static Clock alwaysNewTime() {
return new Clock(ONE_MILLIS_INTERVAL);
}
@Override
protected void before() throws Throwable {
currentTime = new DateTime();
setProviderIfExists();
}
private void setProviderIfExists() {
if (millisProvider != null) {
DateTimeUtils.setCurrentMillisProvider(millisProvider);
}
}
@Override
protected void after() {
DateTimeUtils.setCurrentMillisSystem();
}
/**
* set date and stop the clock
*/
public void setFixedTime(Date date) {
currentTime = new DateTime(date);
DateTimeUtils.setCurrentMillisFixed(currentTime.getMillis());
}
/**
* set date and stop the clock
*/
public void setFixedTime(DateTime dateTime) {
setFixedTime(dateTime.toDate());
}
public DateTime getCurrentDateTime() {
return currentTime;
}
public Date getCurrentDate() {
return currentTime.toDate();
}
}
I sam test:
@Rule
public Clock clock = Clock.alwaysNewTime();
@Test
public void should() {
// given
Order firstOrder = new Order();
Order secondOrder = new Order();
// when
// then
assertThat(firstOrder.getCreateDate().before(secondOrder.getCreateDate()).isTrue()
}
Kiedy to się może przydać? Na pewno jeśli gdzieś w teście zamówienia lądują w HashMapie, której kluczem jest data utworzenia zamówienia.
@Test
public void shouldCreateOrderWithCurrentCreateDate() {
//given
Order order = new Order();
//when
Date createDate = order.getCreateDate();
//then
assertThat(createDate).isEqualTo(new Date());
}
dla takiej klasy:
public class Order {
private Date createDate;
public Order() {
this.createDate = new Date();
}
public Date getCreateDate() {
return createDate;
}
}
będzie on bardzo niedeterministyczny i raz zadziała a raz nie. Dotychczas, w celu pozbycie się problemu, zamiast new Date(), wywoływałem statyczną metodę z JodaTime (this.createDate = DateTime.now().toDate();), którą następnie mockowałem za pomocą PowerMockito. I test mógł wyglądać tak:
@Test
public void shouldCreateOrderWithCurrentCreateDate() {
//given
DateTime currentDate = new DateTime();
PowerMockito.mockStatic(DateTime.class);
PowerMockito.when(DateTime.now()).thenReturn(currentDate);
Order order = new Order();
//when
Date createDate = order.getCreateDate();
//then
assertThat(createDate).isEqualTo(currentDate.toDate());
}
Minusem takiego podejścia jest fakt, że rezerwujemy sobie runnera na PowerMocka i już innego nie będziemy mogli użyć, np. dla testów w kontekście springa. Żeby nie wiało nudą, zainspirowany ostatnim szkoleniem TDD z Rafełem Jamrózem, proponuje inne podejście, a konkretnie wykorzystanie junitowych @Rule. Mechanizm ten jest alternatywą dla @Before i @After, czyli poprzez implementację odpowiedniego interfejsu pozwala na zrobienie czegoś przed i po metodzie testowej. Dlaczego jest to lepsze od @Before i @After - ponieważ raz zaimplementowane, może być używane potem w wielu testach bez dziedziczenia i bez copy-paste.
Korzystając z JodaTime mamy do dyspozycji utilsa do sterowania źródłem aktualnego czasu. Implementacja klasy sterującej czasem w testach może wyglądać następująco:
public class Clock extends ExternalResource {
private static DateTime currentTime;
private Clock() {}
public static Clock standard() {
return new Clock();
}
@Override
protected void before() throws Throwable {
currentTime = new DateTime();
}
@Override
protected void after() {
DateTimeUtils.setCurrentMillisSystem();
}
/**
* set date and stop the clock
*/
public void setFixedTime(Date date) {
currentTime = new DateTime(date);
DateTimeUtils.setCurrentMillisFixed(currentTime.getMillis());
}
public DateTime getCurrentDateTime() {
return currentTime;
}
public Date getCurrentDate() {
return currentTime.toDate();
}
}
A test z wykorzystaniem rula:
@Rule
public Clock clock = Clock.standard();
@Test
public void shouldCreateOrderWithCurrentCreateDate() {
// given
Date date = new Date();
clock.setFixedTime(date);
Order order = new Order();
// when
Date createDate = order.getCreateDate();
// then
assertThat(createDate).isEqualTo(date);
}
Należy tylko pamiętać, że do tworzenia daty w zamówieniu używamy JodaTime. Implementacja Clocka nie wpływa na inne metody testowe (patrz. metoda after()), więc możemy w zależności od potrzeb korzystać z jego funkcjonalności lub nie.