Parameterise Your Tests

Test cases usually include a number of repeated steps, especially in the process of setting up given scenarios. This means that often test cases can become bloated in repetition, and it can be cumbersome to find the functionality being tested.
The example below shows two small test cases, that given their functionality could be moved into a parameterised test case, making the test cases far clearer. Although the example is simple, it shows the complication that can come from multiple services interacting with each other.
public class ProcessDataServiceTest {
  
  private final DataConfigurationService dataConfigService = mock(DataService.class);
  private final DataProcessor dataProcessor = mock(DataProcessor.class);
  private final ProcessDataService underTest = new ProcessDataService();
  
  @Test
  void should_not_process_data_if_data_check_config_is_disabled() {
    // Given
    String badParameter = "so_bad";
    final List<DataToBeProcessed> request = List.of(
        aPieceOfData(badParameter), 
        aPieceOfData(badParameter)
    );
    given(dataConfigService.isDataCheckRequired()).willReturn(false);
    
    // When
    List<ProcessedData> result = underTest.process(request);
    
    // Then
    assertThat(result).isNotNull().isEmpty();
    verify(dataProcessor, never()).process();
  }
  
  @Test
  void should_return_an_empty_list_if_the_data_is_malformed() {
    // Given
    String badParameter = "so_bad";
    final List<DataToBeProcessed> request = List.of(
        aPieceOfData(badParameter), 
        aPieceOfData(badParameter)
    );
    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_a_processed_list_if_the_data_is_formed_correctly() {
    // Given
    String goodParameter = "so_good";
    final List<DataToBeProcessed> request = List.of(
        aPieceOfData(goodParameter), 
        aPieceOfData(goodParameter)
    );
    given(dataConfigService.isDataCheckRequired()).willReturn(true);
    given(dataProcessor.process()).willReturn(...);
    
    // When   
    List<ProcessedData> result = underTest.process(request);
    
    // Then
    assertThat(result).isNotNull().isEmpty();
    verify(dataProcessor).process();
  }
  
  private DataToBeProcessed aPieceOfData(String parameter) {
    return new DataToBeProcessed(parameter, ...);
  }
}
Now if we compare the above to the example below - it clearly states the intended changing parameters, and 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. Furthermore, the pure content of the test is far smaller, meaning reviews are likely to be faster
public class ProcessDataServiceTest {
  
  private final DataConfigurationService dataConfigService = mock(DataService.class);
  private final DataProcessor dataProcessor = mock(DataProcessor.class);
  private final ProcessDataService underTest = new ProcessDataService();
  
  @ParameterizedTest
  @MethodSource
  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 static Stream<Arguments> should_handle_data_processing_correctly() {
    return Stream.of(
        Arguments.of(listOfDataToBeProcessed("good_param"), false, 0, List.of(... non-processed data)),
        Arguments.of(listOfDataToBeProcessed("bar_param"), true, 1, emptyList()),
        Arguments.of(listOfDataToBeProcessed("good_param"), true, 1, List.of(... processed data))
    );
  }
  
  private List<DataToBeProcessed> listOfDataToBeProcessed(String parameter) {
    return List.of(new DataToBeProcessed(parameter, ...), new DataToBeProcessed(parameter, ...));
  }
  
  private DataToBeProcessed aPieceOfData(String parameter) {
    return new DataToBeProcessed(parameter, ...);
  }
}
maintainability
readability
optional
comparisons