JUnit 5 – When to use @ValueSource
April 4, 2023Assert with Grace: Custom Assertions using AssertJ for Cleaner Code
January 3, 2024Introduction
This article belongs to the Managing Test Data series.
I will show you different ways to smartly use testing framework features when the date changes but the test output is the same.
In this article, you will see one use case for the MethodSource
feature, which we will use in the same test class.
MethodSource
In a nutshell, the MethodSource feature allows you to use the data in your tests from a method.
The @MethodSource
feature will use factory methods internally (in the same test class) or externally (from a different class, not necessarily a test one). These methods are the ones that will have the data used in the test. They must be static
and return a Stream where we can type it as a single object or an Arguments object in the case of multiple data. I recommend you also read the JUnit 5 explanation of the MethodSource.
To use the MethodSource feature you must:
- use the
@MethodSource
annotation, which will be connected to a factory method - create a static factory method that:
- returns a
Stream
ofArguments
- have Arguments, expressing as many parameters as you need
- returns a
- match the Arguments parameters in the test method
The application of the MethodSource
Good news: you can use it at any time! This is one of the most flexible data-driven methods in JUnit 5.
If the @ValueSource
does not apply to you given more than one data/attribute to handle, go for the @MethodSource
usage.
We are calling the MethodSource here internal as you will create the factory method in the same test class.
Is it possible to create a separation, and is explained in the JUnit 5 – How to use the External MethodSource article.
Simple example
Let’s say we need to check if a given date list is from 2023.
I advise starting with the data you need and then creating the test class.
class MyTestClass {
@DisplayName("Date must be from the current year")
@ParameterizedTest(name = "Check the current year of {0}")
@MethodSource("dateList")
void myTest(LocalDate date) {
assertThat(date).hasYear(2023);
}
static Stream<Arguments> dateList() {
return Stream.of(
Arguments.arguments(LocalDate.parse("2023-08-15")),
Arguments.arguments(LocalDate.parse("2023-03-01")),
Arguments.arguments(LocalDate.parse("2023-10-23"))
);
}
}
The dateList
method, in line 10, is the factory method. It expects a Stream
of Arguments
that will have the data to use in the tests.
In line 11, a Stream of Arguments
is returned, where we can add any Argument we want. In this example lines 12 to 15 show that the argument (or data) is a LocalDate, so each Argument.argument()
is a different set of data that will be used in the test, so the test will run 3 times.
We must associate the factory method with the test, and this is done by adding the @MethodSource
annotation and the factory method name as a parameter. We can see it in line 5 as the test method myTest()
has a parameter LocalDate date
.
The last thing to do is to add, in the test method, the parameter that matches the Argument
. In this case, the data is a LocalDate
object, so we need to add the same parameter type. You can see it in line 6.
This test does a simple check: if all the Arguments
(data) are in 2023.
Case 1: you have a single data type that’s not the supported one in the @ValueSource
In the JUnit5 – When to use @ValueSource we learned that it supports short
, byte
, int
, long
, float
, double
, char
, boolean
, java.lang.String
and java.lang.Class
. So if your data type is not one of these, go for the @MethodSource
usage.
This is precisely what we have in the Simple example: we need to use the LocalDate
class in the test that is not supported in the @ValueSource
.
Case 2: you have a set of data to use
When you have a different set of data that belongs to the same context the @MethodSource
is a good choice because you can add as much as arguments as you need in the factory method, which will be associated with the test.
Let’s imagine you have an algorithm where different inputs generate different results.
In the example below we are searching for a payment status, which will return a message.
class MethodSourceCase2ExampleTest {
@DisplayName("Payment status")
@ParameterizedTest(name = "The payment with status {0} returns the message {1}")
@MethodSource("statusList")
void paymentStatus(Status status, String message) {
String auditedPayment = auditService.getPayments(status);
assertThat(status).isEqualTo(message);
}
static Stream<Arguments> statusList() {
return Stream.of(
Arguments.arguments(STATUS_PROCESSING, "The payment is being processed."),
Arguments.arguments(STATUS_PENDING, "The payment is pending."),
Arguments.arguments(STATUS_APPROVED, "The payment is approved."),
Arguments.arguments(STATUS_FAILED, "The payment failed.")
);
}
}
Lines 13 to 16 show that we have two arguments: a status
and a message
. We are associating these arguments in the test method, as parameters, in line 6.
Line 7 uses the status
to search for payment in an audit service, returning a message as String.
Line 8 asserts that the message matches the expectation, which is associated with the statusList
(the factory method) by the pair of arguments, for example, STATUS_PROCESSING
and the "The Payment is being processed."
.
You can, based on your context, add as many arguments as you want, meaning as many parameters in the Arguments.argument(...);
This is a silly example only to give you an idea of what you can do with the @MethodSouce
feature.
Case 3: you can use any object to hold the data
The @MethodSource
can use Arguments
, we just learned. The arguments can be any object. We are using the Status
object (an enum) and the String object in the previous example.
You can also add your custom object to transport the data to the test. Also, remember you can use the data-driven approach to test edge cases/negative scenarios.
Let’s assume you have a Simulation
object that has a lot of attributes, and two of them are installments
and amount
. They have the following rules:
Attribute | Rule | Error Message |
installments | Installments must be less than or equal to 48 | Installments must be less than or equal to 48 |
installments | Maximum value: 48 | Installments must be less than or equal 48 |
amount | Minimum value: 1.000 | Amount must be greater than or equal to 1.000 |
amount | Maximum value: 40.000 | Amount must be greater less or equal to 40.000 |
We can now test the edge cases based on the rules. We need to create the factory method using the Simulation
class holding the data based on the edge cases, plus the error message to assert it in the test. During a post request, we will send the Simulation object, expecting an HTTP 422 – Unprocessable Entity, where we will validate the response body matching with the expected error message.
The code below is generic and not based on any API Testing framework:
class MethodSourceCase3ExampleTest {
@DisplayName("Unpronounceable entity tests")
@ParameterizedTest(name = "The Simulation {0} shows the error {1}")
@MethodSource("expensiveProducts")
void edgeCases(Simulation simulation, String errorMessage) {
Response response = postSimulation(simulation);
assertThat(response.statusCode).isEqualTo(422);
assertThat(response.body().errorMessage()).isEqualsTo(errorMessage);
}
static Stream<Arguments> edgeCases() {
Simulation installmentsMinValue = Simulation.builder().installments(1).build();
Simulation installmentsMaxValue = Simulation.builder().installments(41).build();
Simulation amountMinValue = Simulation.builder().amount(new BigDecimal("1.001")).build();
Simulation amountMinMax = Simulation.builder().amount(new BigDecimal("40.001")).build();
return Stream.of(
arguments(installmentsMinValue, "Installments must be less than or equal to 48"),
arguments(installmentsMaxValue, "Installments must be less than or equal to 48"),
arguments(amountMinMax, "Amount must be greater than or equal to 1.000"),
arguments(amountMinValue, "Amount must be greater less or equal to 40.000")
);
}
}
Lines 13 to 16 are Simulation
objects holding the different data values for the edge cases.
Lines 19 to 22 add the Simulation
objects as Arguments
, associating them with the related error message.
Line 7 creates a post request, expecting a Response
object.
Line 9 asserts that the response status code is 422 (Unprocessable Entity)
Line 10 asserts that the response body has an error message as the expected one by the factory class.
Is there any possible problem using the Internal Method Source?
Actually, no 🙂
What can happen is that you might have a factory method that could be used by another test.
We will understand how to solve this problem in the upcoming “External Method Source” and “Argument Provider” articles.
When should I use the Internal Method Source?
Anytime when:
- the
@ValueSource
is not sufficient to create your test - when you have more than 2 or more sets of data to use in your test
What are the benefits of using the Internal Method Souce?
- readability: you will understand the data required without the necessity to go to any other class or method to see its data
- edge-cases: you will be able to create the happy paths and edge cases given the ability to add more arguments to the factory method
- isolation: your factory method will be available only in your test, where you can have more control over its data