Sample application for the test
For my series of blog posts about Spring Boot testing, I’ve prepared a sample application.
One of the app functionality is formatting input values in the form of two sings after decimal points - XX.YY.
It has a UI part as well as a REST API part. UI part takes an input:
and returns a result:
For API part the same functionality is implemented for an “/format” endpoint:
The result object is:
Formatter service
The feature code for format operation is in FormatterService class.
It has one method “formatMoney()” which takes a String as an input and returns the value as a formatted String.
public String formatMoney(String inputMoney) {
log.info("Received value for formatting: {}", inputMoney);
if (inputMoney == null || inputMoney.isEmpty()) {
return inputMoney;
}
inputMoney = inputMoney.replace(',', '.');
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance();
symbols.setGroupingSeparator(' ');
symbols.setDecimalSeparator('.');
DecimalFormat formatter = new DecimalFormat("###,##0.00", symbols);
String formatted = "";
try {
formatted = formatter.format(Double.parseDouble(inputMoney));
} catch (NumberFormatException ex) {
log.error(ex.getMessage());
}
log.info("Formatted result: {}", formatted);
return formatted;
}
In order to cover the test on the unit test level, we can write a lot of tests even for a single method, like:
public class FormatterTest {
private FormatterService formatterService;
@Before
public void beforeTest() {
formatterService = new FormatterService();
}
@Test
public void shouldFormatValue(){
String input = "123.234";
String result = "123.23";
assertThat(formatterService.formatMoney(input)).isEqualTo(result);
}
@Test
public void shouldFormatNegativeValue(){
String input = "-123.236";
String result = "-123.24";
assertThat(formatterService.formatMoney(input)).isEqualTo(result);
}
// and so on ...
}
But in all cases, it will be testing the behavior of the method from an input data point of view.
Maybe it is a better way how to deal with data-driven unit tests?
Parameterized tests with JUnit
JUNit offers an ability to test method with multiple test data using Parameterized runner.
The steps for adding a parameterized test for formatter are the following:
-
Declare test data
@Parameterized.Parameters public static Collection<String[]> data() { return Arrays.asList(new String[][]{ {"2310000.159897", "2 310 000.16"}, {"1600", "1 600.00"}, {"0", "0.00"}, {"-123456.456", "-123 456.46"}, {"123234,456", "123 234.46"}, {".", ""}, {",", ""}, {"111.", "111.00"}, {".222", "0.22"}, {null, null} }); }
-
Declare input and output parameters
@Parameterized.Parameter public String input; @Parameterized.Parameter(1) public String expected;
-
Initialize service and implement the test
private FormatterService formatterService; @Before public void beforeTest() { formatterService = new FormatterService(); } @Test public void shouldConvertValue() { assertThat(expected) .as("Invalid result of money conversion") .isEqualTo(formatterService.formatMoney(input)); }
-
Mark test class to be executed with Parameterized.class from JUnit
@RunWith(Parameterized.class) public class FormatterServiceTest {}
Full source code of the test available here
Benefits of the data-driven approach
Parameterized testing provides the following benefits:
-
Increased focus on test data and corner cases
-
Better code reusability and less code duplication
-
Can be applicable at any test level: unit, component, integration, end-to-end
Conclusion
The data-driven approach in unit testing can potentially decrease the number of copy-paste code for unit tests and focus developer more on the test data and corner cases.
But as with any other testing approaches - it is not a silver bullet and should be used appropriately.
Service code, as well as the test code, can be found in the Boot-testing-examples repository.