JUnit 5 – When to use ArgumentsSource

Published on February 6, 2024

Introduction

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 ArgumentSource feature, in which we will use the data source from a different class.

ArgumentsSource

The @ArgumentsSource annotation expects a class, where it must implement the ArgumentProvider interface which will have a single method that will return a Stream of Arguments (Stream) in the same way you create the data factory method using the @MethodSource.

At this point, it’s good to explain, at a high level, what’s the internals.

What’s an Argument

Argument is an abstraction that provides access to an array of objects to be used for invoking a @ParameterizedTest method.

Think about arguments as the data, implicitly or explicitly, used by any parameterized source of arguments. All the annotations presented in this article series are sources of arguments.

Why explicitly? Because sources like @NullSource, and @EmptySource do not require any parameters. Why explicitly? Because sources like the @ValueSource get the supported values from the annotation parameter or the @MethodSource which gets the factory method and extracts its arguments.

What’s an ArgumentsSource

All the sources of arguments have this pattern: The @ArgumentsSource annotation is used to indicate it @ArgumentsProvider which is responsible for providing a stream of arguments to be passed and used by the @ParameterizedTest.

In short, each source of arguments implements its ArgumentsSource.

The ArgumentsSource is a way to provide a custom data implementation the way you want by implementing the ArgumentsProvider interface.

Most of the implemented ArgumentsProvider get the ExtensionContext object and extracts the test method, and then the parameter types. You can see this approach in the EmptyArgumentsProvider that checks if each supported type is empty.

The application of the ArgumentsSource

There’s one example, and it will be always like this.

First, we define a class that will hold its data, but different from the External MethodSource approach, this class must implement the ArgumentProvider interface.

public class ProductsDataArgumentProvider implements ArgumentsProvider {

    @Override
    public Stream provideArguments(ExtensionContext extensionContext) {
        return Stream.of(
                arguments("Micro SD Card 16Gb", new BigDecimal("6.09")),
                arguments("JBL GO 2", new BigDecimal("22.37")),
                arguments("iPad Air Case", new BigDecimal("14.99"))
        );
    }
}

On the first line, you can see that we are implementing the ArgumentProvider interface. This will make you implement the provideArguments method, which you can see in detail on line 4.

Another difference compared to the External MethodSource is that the method has the EXtensionContext parameter, which will understand what are the class, method, and objects on this data factory method.

The code will add two different parameters to the test: a String related to a product name and a BigDecimal related to its amount.

Below you can see one of the possible tests.

class JUnitArgumentsProviderTest {

    private static final String MAXIMUM_PRICE = "30.0";

    @DisplayName("Products should not exceed the maximum price")
    @ParameterizedTest(name = "product ''{0}'' of amount ${1} does not exceeds $" + MAXIMUM_PRICE)
    @ArgumentsSource(ProductsDataArgumentProvider.class)
    void cheapProducts(String product, BigDecimal amount) {
        assertThat(product).isNotEmpty();
        assertThat(amount).isLessThanOrEqualTo(new BigDecimal(MAXIMUM_PRICE));
    }
}

This test asserts that all the expected product amounts are less or equal to $ 30, and uses the data from the ProductsDataArgumentProvider class we just created as the data to be used in the test. Line 7 shows you this.

Line 8 shows the two parameters used: String product and BigDecimal amount which matches the data type we have in the ProductsDataArgumentProvider class.

When should I use the ArgumentsSource?

That’s a tricky question because you might find the External MethodSource and the ArgumentsSource the same.
Of course, they have a few implementation differences, and I will highlight them:

ArgumentsSource External MethodSource
Implementation External class, where the data factory method must be 1-1 as it implements the ArgumentsProvider interface External class, where we can have multiple data factory methods
Pros The association of the @MethodSource annotation is on its fully qualified path, making it slightly difficult for refactoring and fast class access

You can make an explicit argument conversion

You can extend it behavior by extending the AnnotationBasedArgumentsProvider class
Central point for related data factory methods
Cons One data factory method per class The association of the @MethodSource annotation is on its fully qualified path, making it slightly difficult for refactoring and fast class access

Now it’s up to you to decide which approach you can take.
This author prefers the @ArgumentsSource as it’s more flexible and separates well the data responsibilities.

The post JUnit 5 – When to use ArgumentsSource appeared first on Elias Nogueira.