Assert with Grace: Custom Assertions using AssertJ for Cleaner Code
January 3, 2024No more driver management in Selenium WebDriver
January 27, 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, in which we will use the data source from a different class than the test one.
External MethodSource
The External MethodSource is the name given to the use of the @MethodSource, but using the data factory method from a different class rather than inside the test class.
To learn about how the @MethodSource
works, as we it as the Internal Method Source, please look at this article JUnit 5 – When to use the Internal MethodSource, as I strongly recommend you to read it first.
You learned that the Internal MethodSouce has the data factory method, which is the method that has the data to be used in this data-driven approach, inside the test class. The External MethodSource has its data factory method outside the test class where the external class must provide a static factory method. The @MethodSource
annotation will expect a fully qualified name as a parameter, meaning the full path to the class and method.
You can learn about what is a full qualified name, in Java, by looking at the Fully Qualified Names and Canonical Names in the Chapter 6. Names of the Java Language Specification.
The application of the External MethodSource
Simple Example
A basic implementation of the External MethodSource starts with a class containing the data factory method.
public final class ProductsDataFactory {
private ProductsDataFactory() {
}
public static Stream<Arguments> cheapProducts() {
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"))
);
}
}
Line 6 of the above class shows the static data factory method containing a list of arguments (data) to be used in the test.
The test class will use the fully qualified name, including its method, to be able to consume the data factory method from the ProdctsDataFactory
class.
public class ProductsTest {
@DisplayName("All products should be cheap")
@ParameterizedTest(name = "product ''{0}'' of amount ${1} is cheap")
@MethodSource("com.eliasnogueira.datadriven.JUnitExternalData#cheapProducts")
void cheapProducts(String product, BigDecimal amount) {
final BigDecimal maximumPrice = new BigDecimal("30.0");
assertThat(product).isNotEmpty();
assertThat(amount).isLessThanOrEqualTo(maximumPrice);
}
}
Line 5 shows the @MethodSource
annotation referring to the fully qualified path to the method in the ProductsDataFactory
class. Remember that it has a hash sign #
to differentiate the method name.
Case: Add the related data factory method used in or across test classes
You can apply the External MethodSource when you want to remove all the data factory methods from the test class to give them a single responsibility: the test class will have tests and the external class will have the data. This approach might benefit you when you want this separation or when you have a data factory method used in different tests.
Data factory methods inside the test class (Internal MethodSource)
In the below example, we have two tests: one verifying the cheap products and another the expensive ones. Note that the data factory methods are inside the class, and the tests are using the Internal MethodSource approach.
public class ProductsTest {
@DisplayName("All products should be cheap")
@ParameterizedTest("product ''{0}'' of amount ${1} is cheap")
@MethodSource("cheapProducts")
void cheapProducts(String product, BigDecimal amount) {
final BigDecimal maximumPrice = new BigDecimal("30.0");
assertThat(product).isNotEmpty();
assertThat(amount).isLessThanOrEqualTo(maximumPrice);
}
@DisplayName("All products should be expensive")
@ParameterizedTest("product ''{0}'' of amount ${1} is expensive")
@MethodSource("expensiveProducts")
void expensiveProducts(String product, BigDecimal amount) {
final BigDecimal minimumPrice = new BigDecimal("799");
assertThat(product).isNotEmpty();
assertThat(amount).isGreaterThanOrEqualTo(minimumPrice);
}
public static Stream<Arguments> cheapProducts() {
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"))
);
}
public static Stream<Arguments> expensiveProducts() {
return Stream.of(
arguments("iPhone 11 Pro", new BigDecimal("999.00")),
arguments("MacBook Pro 16", new BigDecimal("2799.00")),
arguments("Ipad Air Pro", new BigDecimal("799.00"))
);
}
}
This approach is ok when we have a few tests, but when we add more tests related to its context, you might end up with hundreds of lines, half of them data-related.
How to change it to an External MethodSource
The process is simple:
- Step 1: Create a class that will be the data factory
- remember to add the
final
keyword in the class and a private constructor
- remember to add the
- Step 2: Change the
@MethodSource
value to the fully qualified path to the method
To achieve Step 1 the below class, named ProductsDataFactory has both methods: cheapProducts()
and expensiveProducts()
.
public final class ProductsDataFactory {
private ProductsDataFactory() {
}
public static Stream<Arguments> cheapProducts() {
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"))
);
}
public static Stream<Arguments> expensiveProducts() {
return Stream.of(
arguments("iPhone 11 Pro", new BigDecimal("999.00")),
arguments("MacBook Pro 16", new BigDecimal("2799.00")),
arguments("Ipad Air Pro", new BigDecimal("799.00"))
);
}
}
Lines 6 to 16 show the data factory method for the cheapProducts()
as lines 14 to 20 show for the expensiveProducts()
method.
Now, to achieve Step 2, we remove the data factory methods from the test class and replace the value of the @MethodSource
annotation with the fully qualified name of the methods.
class ProductsTest {
@DisplayName("All products should be cheap")
@ParameterizedTest("product ''{0}'' of amount ${1} is cheap")
@MethodSource("com.eliasnogueira.datadriven.JUnitExternalData#cheapProducts")
void cheapProducts(String product, BigDecimal amount) {
final BigDecimal maximumPrice = new BigDecimal("30.0");
assertThat(product).isNotEmpty();
assertThat(amount).isLessThanOrEqualTo(maximumPrice);
}
@DisplayName("All products should be expensive")
@ParameterizedTest("product ''{0}'' of amount ${1} is expensive")
@MethodSource("com.eliasnogueira.datadriven.JUnitExternalData#expensiveProducts")
void expensiveProducts(String product, BigDecimal amount) {
final BigDecimal minimumPrice = new BigDecimal("799");
assertThat(product).isNotEmpty();
assertThat(amount).isGreaterThanOrEqualTo(minimumPrice);
}
}
You now can see that lines 5 and 15 refer to the full qualified name for the respective methods in the ProductsDataFactory
class.
When should I use the External Method Source?
At any time 🙂
You might prefer using it, which can be read as the possible benefits, when:
- your class gets bigger, meaning hundreds of lines of code
- to concentrate all related data factory methods in a class
- to separate responsibilities: test classes have only the tests, data classes have the data
Note that the usage of the External MethodSource works with the fully qualified name as its value in the @MethodSource
annotation, as it is a String.
Your IDE, probably, won’t apply the features of automatic refactoring when you change the method names in the data factory class.