I'm writing my unit tests using the Spock and Mockito frameworks and have stumbled across a limitation in Mockito which I cannot solve elegantly.
The following code parses a .csv file and returns a Collection of TradedInstrument objects:
@ManagedOperation
public Collection<TradedInstrument> loadTradedInstrumentFromBatchFile() {
Collection<TradedInstrument> tradedInstrumentsFrombatchFile = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new InputStreamReader(
this.getClass().getResourceAsStream("/" + tradedInstrumentBatchFilename)))) {
String line;
while ((line = br.readLine()) != null) {
String[] instrumentAttributes = line.split(",");
TradedInstrument tradedInstrument = new TradedInstrument();
tradedInstrument.setInstId(instrumentAttributes[0]);
tradedInstrument.setPriceSource(PriceSource.valueOf(instrumentAttributes[1]));
tradedInstrument.setPrice(new BigDecimal(instrumentAttributes[2]));
tradedInstrument.setDateCreated(businessDateDao.getBusinessDate());
insertTradedInstrument(tradedInstrument);
tradedInstrumentsFrombatchFile.add(tradedInstrument);
LOGGER.info("Loaded traded instrument: " + tradedInstrument + " from batch file: " + tradedInstrumentBatchFilename);
}
} catch (NullPointerException e) {
String errorMessage = "An ERROR occurred locating the traded instrument upload batch file: " + tradedInstrumentBatchFilename;
LOGGER.error(errorMessage);
throw new PriceServiceException(errorMessage, e);
} catch (IOException e) {
String errorMessage = "An ERROR occurred reading the traded instrument upload batch file: " + tradedInstrumentBatchFilename;
LOGGER.error(errorMessage);
throw new PriceServiceException(errorMessage, e);
} catch (Exception e) {
String errorMessage = "An ERROR occurred creating traded instrument using data from upload batch file: " + tradedInstrumentBatchFilename;
LOGGER.error(errorMessage, e);
throw new PriceServiceException(errorMessage, e);
}
return tradedInstrumentsFrombatchFile;
}
I have a batch file called "tradedInstrumentBatchFile.csv" with the following content:
1234,ICAP,0.4956 2345,BBG,0.8456 8456,NASDAQ,0.3567 5967,REUTERS,0.8675
I've written the following Spock test in Groovy:
def "should load traded instrument from batch file"() {
given:
TradedInstrumentSubscriber tradedInstrumentSubscriber = Mock()
TradedInstrumentTable tradedInstrumentDao = Mock()
VendorTopicDao vendorTopicDao = Mock()
TradedInstrumentListener listener = Mock()
BusinessDateDao businessDateDao = Mock()
PriceService priceService = new PriceService(tradedInstrumentSubscriber, listener, vendorTopicDao, tradedInstrumentDao, businessDateDao)
priceService.setTradedInstrumentBatchFilename("tradedInstrumentBatchFile.csv")
businessDateDao.getBusinessDate() >> new Date()
when:
priceService.loadTradedInstrumentFromBatchFile()
then:
1 * tradedInstrumentDao.insert(_ as TradedInstrument) >> { TradedInstrument arg ->
assert arg.instId == "1234"
assert arg.priceSource == PriceSource.ICAP
assert arg.price == BigDecimal.valueOf(0.4956)
}
1 * tradedInstrumentDao.insert(_ as TradedInstrument) >> { TradedInstrument arg ->
assert arg.instId == "2345"
assert arg.priceSource == PriceSource.BBG
assert arg.price == BigDecimal.valueOf(0.8456)
}
1 * tradedInstrumentDao.insert(_ as TradedInstrument) >> { TradedInstrument arg ->
assert arg.instId == "8456"
assert arg.priceSource == PriceSource.NASDAQ
assert arg.price == BigDecimal.valueOf(0.3567)
}
1 * tradedInstrumentDao.insert(_ as TradedInstrument) >> { TradedInstrument arg ->
assert arg.instId == "5967"
assert arg.priceSource == PriceSource.REUTERS
assert arg.price == BigDecimal.valueOf(0.8675)
}
}
I've written an attempt at the equivalent Junit test using Mockito:
@Test
public void shouldLoadTradedInstrumentFromBatchFile() {
// Given
VendorTopicDao vendorTopicDao = mock(VendorTopicDao.class);
TradedInstrumentSubscriber tradedInstrumentSubscriber = mock(TradedInstrumentSubscriber.class);
TradedInstrumentTable tradedInstrumentDao = mock(TradedInstrumentTable.class);
TradedInstrumentListener listener = mock(TradedInstrumentListener.class);
BusinessDateDao businessDateDao = mock(BusinessDateDao.class);
PriceService priceService = new PriceService(tradedInstrumentSubscriber, listener, vendorTopicDao, tradedInstrumentDao);
priceService.setTradedInstrumentBatchFilename("tradedInstrumentBatchFile.csv");
ArgumentCaptor<TradedInstrument> captor = ArgumentCaptor.forClass(TradedInstrument.class);
when(businessDateDao.getBusinessDate()).thenReturn(new Date());
// When
priceService.loadTradedInstrumentFromBatchFile();
// Then
verify(tradedInstrumentDao, times(4)).insert(captor.capture());
List<TradedInstrument> tradedInstruments = captor.getAllValues();
TradedInstrument tradedInstrument1 = new TradedInstrument();
tradedInstrument1.setInstId("1234");
tradedInstrument1.setPriceSource(PriceSource.ICAP);
tradedInstrument1.setPrice(BigDecimal.valueOf(0.4956));
TradedInstrument tradedInstrument2 = new TradedInstrument();
tradedInstrument2.setInstId("2345");
tradedInstrument2.setPriceSource(PriceSource.BBG);
tradedInstrument2.setPrice(BigDecimal.valueOf(0.8456));
TradedInstrument tradedInstrument3= new TradedInstrument();
tradedInstrument2.setInstId("8456");
tradedInstrument2.setPriceSource(PriceSource.NASDAQ);
tradedInstrument2.setPrice(BigDecimal.valueOf(0.3567));
TradedInstrument tradedInstrument4= new TradedInstrument();
tradedInstrument2.setInstId("5967");
tradedInstrument2.setPriceSource(PriceSource.REUTERS);
tradedInstrument2.setPrice(BigDecimal.valueOf(0.8675));
assertThat(tradedInstruments, hasItem(tradedInstrument1));
assertThat(tradedInstruments, hasItem(tradedInstrument2));
assertThat(tradedInstruments, hasItem(tradedInstrument3));
assertThat(tradedInstruments, hasItem(tradedInstrument4));
}
The Junit test fails because the test includes the field "dateCreated" in the comparison whereas, the Spock test selectively omits this field.
The field "dateCreated" is supposed to be the actual time the TradedInstrument was created so it intentionally needs to be excluded from the comparison.
The only solution I can see is to add the following interaction on the businessDateDao:
when(businessDateDao.getBusinessDate()).thenReturn(null);
Is there an equivalent way of using a Matcher in Junit/ Mockito to selectively omit the comparison of a field?
You could use a combination of hasItem
and hasProperty
to focus your assertions on the attributes of TradedInstrument
which are populated. For example:
assertThat(tradedInstruments, hasItem(hasProperty("instId", is(1234))));
assertThat(tradedInstruments, hasItem(hasProperty("priceSource", is(PriceSource.ICAP))));
assertThat(tradedInstruments, hasItem(hasProperty("price", is(BigDecimal.valueOf(0.4956)))));
Or you could declare a custom matcher ...
private Matcher<TradedInstrument> isEquivalent(final String instId, final PriceSource priceSource, final BigDecimal price) {
return new BaseMatcher<TradedInstrument>() {
@Override
public boolean matches(final Object item) {
final TradedInstrument tradedInstrument = (TradedInstrument) item;
// your custom equality implementation e.g.
return instId.equals(tradeInstrument.getInstId()) && priceSource == tradeInstrument.getPriceSource() && price.equals(tradeInstrument.getPrice());
}
@Override
public void describeTo(final Description description) {
description.appendText(String.format("the given object should contain id=%s, priceSource=%s, price=%s ", id, priceSource, price));
}
};
}
... and use it like this:
assertThat(tradedInstruments, hasItem(isEquivalent(1, PriceSource.ICAP, BigDecimal.valueOf(0.4956))));