Skip to main content

Parameterise your Tests

Test cases usually include a number of repeated steps, especially in the process of creating data. This means that often test cases can become bloated in repetition, and it can be cumbersome to find the functionality where the test is failing.

In many Java testing libraries, variable test parameters are a standard utility, and these should be used when appropriate (JUnit 5 in this example).

The example below shows two small test cases, that given their functionality could be moved into a parameterised test case. Although the example is simple, it shows the complication that can come from multiple services interacting with each other.

class PaymentProcessorServiceTest {

private final CurrencyConverterService currencyConverterService = mock(CurrencyConverterService.class);
private final PaymentProcessorService underTest = new PaymentProcessorService(currencyConverterService);

@Test
void should_return_zero_for_negative_value() {
// Given
final List<Long> payments = List.of(-150, -50)
given(currencyConverterService.euroToSterling()).willReturn(-145, -45);

// When
long result = underTest.process(request);

// Then
assertThat(result).isEqualTo(0);
verify(currencyConverterService, times(2)).process();
}

@Test
void should_return_zero_for_value_above_threshold() {
// Given
final List<Long> payments = List.of(10_000, 100_000)
given(currencyConverterService.euroToSterling()).willReturn(-145, -45);

// When
long result = underTest.process(request);

// Then
assertThat(result).isEqualTo(0);
verify(currencyConverterService, times(2)).process();


// Given
final List<DataToBeProcessed> request = List.of(
aPieceOfData("bad"),
aPieceOfData("worse")
);
given(dataConfigService.isDataCheckRequired()).willReturn(true);
given(dataProcessor.process()).willReturn(emptyList());

// When
List<ProcessedData> result = underTest.process(request);

// Then
assertThat(result).isNotNull().isEmpty();
verify(dataProcessor).process();
}

@Test
void should_return_processed_values() {
// Given
final String goodParameter = "so_good";
final List<DataToBeProcessed> request = List.of(
aPieceOfData("good"),
aPieceOfData("better")
);
given(dataConfigService.isDataCheckRequired()).willReturn(true);
given(dataProcessor.process()).willReturn(...);

// When
List<ProcessedData> result = underTest.process(request);

// Then
assertThat(result).isNotNull().usingRecursiveComparison().isEqualTo();
verify(dataProcessor).process();
}

private DataToBeProcessed aPieceOfData(String parameter) {
return new DataToBeProcessed(parameter, ...);
}
}

Refactoring

Now if we compare the above to the example below:

  • It clearly states the intended changing parameters
  • If any of the tests fail in future it will be clear to a future developer what the intended outcome of the original functionality was
  • The pure content of the test is far smaller, meaning code reviews are likely to be faster
  • Future test cases are easy to add
class ProcessDataServiceTest {

private final DataConfigurationService dataConfigService = mock(DataService.class);
private final DataProcessor dataProcessor = mock(DataProcessor.class);
private final ProcessDataService underTest = new ProcessDataService(dataConfigService, dataProcessor);

private static Stream<Arguments> test_data() {
return Stream.of(
Arguments.of(List.of(aPieceOfData("good"), aPieceOfData("better")), false, 0, List.of(...non-processed data)),
Arguments.of(List.of(aPieceOfData("bad"), aPieceOfData("worse")), true, 1, emptyList()),
Arguments.of(List.of(aPieceOfData("good"), aPieceOfData("better")), true, 1, List.of(...processed data))
);
}

@ParameterizedTest
@MethodSource("test_data")
void should_handle_data_processing_correctly(
List<DataToBeProcessed> request,
boolean isDataCheckRequired,
int timesDataProcessorCalled,
List<ProcessedData> expectedResult
) {
// Given
given(dataConfigService.isDataCheckRequired()).willReturn(isDataCheckRequired);

// When
List<ProcessedData> outcome = underTest.process(request);

// Then
assertThat(outcome).isEqualTo(expectedResult);
verify(dataConfigService).isDataCheckRequired();
verify(dataProcessor, times(timesDataProcessorCalled)).process();
}

private DataToBeProcessed aPieceOfData(String parameter) {
return new DataToBeProcessed(parameter, ...);
}
}