Unit Testing: Java Streams and Lambdas
While Java streams and lambdas can seem complex for unit testing, this tutorial demonstrates simplified instructions to perform unit testing in JDK8.
Join the DZone community and get the full member experience.
Join For FreeUnit testing is an indispensable practice for Java developers (any developer really). The primary benefit of unit testing a pipeline is the early detection of bugs, which is less expensive to fix. It not only improves the overall quality of the code but also makes future maintenance easy.
Using lambdas specifically with streams in Java makes code concise and readable. Streams are excellent for filtering, mapping, sorting, and reducing operations. The elements in the sequence are better processed in a declarative and functional manner, something which lambdas exactly fit into. The anonymous functions that are written with lambdas not only facilitate a functional style of programming but also enhance the expressiveness of the code. Streams and lambdas were introduced in Java after JDK 8, and since then, Java developers have used these features frequently in their projects.
The question around these components that the article tries to address is, "How can you unit test Java streams and lambdas?"
Importance of Unit Testing Pipelines and Lambdas
In unit testing, an individual piece of component of a software application is tested separately. This small unit of code typically is a function, method, or subroutine. The testing mechanism is automated so that they can be done repeatedly and quickly. The test cases are usually written by developers and integrated into the CI/CD pipeline in the development process. The code can be isolated and problems can be easily identified if we use lambda because the essence of it is to make the program functional, more modular, and reusable — something which makes it friendly for unit testing pipelines.
Unit Testing Stream Pipelines
Since stream pipelines combine with lambdas to form a single unit, it is not obvious how to effectively unit test the pieces of the pipeline.
I have always followed these two guidelines to unit test those stream pipelines:
- If the pipeline is simple enough, it can be wrapped in a method call, and it is enough to unit test the method call.
- If the pipeline is more complex, pieces of the pipeline can be called from support methods, and the support methods can be unit-tested.
For example, let's say we have a stream pipeline that maps all the letters of the word to uppercase and is wrapped in a java.util.function.Function<T,R>
as below:
Function<List<String>, List<String>> allToUpperCase = words -> words.stream().map(String::toUpperCase).collect(Collectors.toList());
Now, the unit test for this stream pipeline can be easily written as ordinary unit testing of the allToUpperCase
.
@Test
public void testAllToUpperCase() {
List<String> expected = Arrays.asList("JAVA8", "STREAMS");
List<String> result = allToUpperCase.apply(Arrays.asList("java8", "streams"));
assertEquals(expected, result);
}
The stream above can be wrapped in a regular function, as seen below. Also, an ordinary unit test can be written against this function:
public List<String> convertAllToUpperCase(List<String> words) {
return words.stream().map(String::toUpperCase).collect(Collectors.toList());
}
Unit Testing Lambdas
Believe me — it is very likely that you will encounter complex unit testing in real-world programming. The unit testing with complex lambdas, similar to unit testing of stream pipelines, can be simplified with the following practices:
- Replace a lambda that needs to be tested with a method reference and an auxiliary method.
- Then, test the auxiliary method.
For example, I have a stream pipeline that involves a somewhat complex lambda and a mapping class for the given string class name.
public static Class[] mapClasses(final List<String> exceptions) {
return exceptions.stream().map(className -> {
try {
return Class.forName(className);
} catch(Exception ex) {
LOGGER.error("Failed to load class for exceptionWhiteList: {}", className);
}
return null;
}).toArray(Class[]::new);
}
Here, the key point to test is whether the expression for transforming a string class name to a Class
object is working.
As mentioned above, this can replace the lambda expression with a method reference, along with an auxiliary method that can be placed in a companion class, as shown below:
public static Class[] mapClassesBetter(final List<String> exceptions) {
return exceptions.stream().map(LambdaTester::mapClass).toArray(Class[]::new);
}
public static Class mapClass(String className) {
try {
return Class.forName(className);
} catch(Exception ex) {
LOGGER.error("Failed to load class for name: {}", className);
}
return null;
}
Now, the key element of the original lambda is that it can be tested directly:
@Test
public void testMapClass() throws ClassNotFoundException {
assertEquals(null, mapClass("a"));
assertEquals(null, mapClass("apple"));
assertEquals(Object.class, mapClass("java.lang.Object"));
}
Conclusion
Writing unit tests is one of the core parts of software development. With new features introduced after JDK 8, it has become easier to write code concisely and declaratively. However, the proper use of features like streams and lambda brings value and, of course, makes writing unit tests easier. If you have any additional guidelines for unit testing these features, don't stop yourself from sharing them in the comments. Until next time, happy coding! Learn more about the best Java unit testing frameworks.
The source code for the examples presented above is available on GitHub.
Opinions expressed by DZone contributors are their own.
Comments