Lambda Expressions and Streams in Test Automation Projects

Lambda Expressions are introduced in JAVA 8. They are one of the most popular features of Java 8 and they brought functional programming capabilities to JAVA. In this article, I will try to give some examples of lambda expressions which may help us in our micro frameworks and projects.

By using lambda expressions we can directly write the implementation for a method. For these expressions, the compiler doesn’t create a specific class file, it executes lambda expressions as a function. We can also manipulate collections such as performing iteration, extracting and filtering data, etc. Let’s go on with examples.

Example-1: Function

The lambda expression accepts one Integer argument and returns another Integer.

import java.util.function.Function;

public class FunctionExample {

    public static void main(String[] args) {
        // Create a Function from a lambda expression.
        // It returns the argument multiplied by itself.
        Function<Integer, Integer> func = x -> x * x;

        //Apply the function to an argument of given number.
        //We call apply() on the Function object. This executes and returns the result.
        int result = func.apply(20);
        System.out.println(result);
    }
}

Output: 400

The left side of Lambda Expression:
On the left of a lambda expression, we have the parameters.  Two or more parameters can be surrounded by “(” and “)” chars.

The right side of Lambda Expression:
This is the return expression. It is evaluated by parameters. Here, we multiplied x parameter with itself.

Apply Function:
We call apply() on the Function object. This executes and returns the expression lambda expression.

Example-2: Supplier

A Supplier object receives no arguments. We should use empty arguments to construct lambda expression without any arguments. It provides vales and we can retrieve the values by calling get() method.

import java.util.function.Supplier;

public class SupplierExample {

    static void print (Supplier<String> text) {
        System.out.println(text.get());
    }

    public static void main (String[] args) {
        //Pass lambdas to the print counter.
        //Each returns an String.
        print(() -> "Hello");
        print(() -> "World!");
    }
}

Output:
Hello
World!

Example-3: Predicate 

A boolean-returning method is called as a Predicate. A predicate object gets a value and returns a boolean result as true or false. In below example, we will use RemoveIf predicate method and remove an element in an ArrayList.

import java.util.ArrayList;

public class PredicateExample {
    public static void main(String[] args) {

        //Create ArrayList and add String elements.
        ArrayList<String> arrayList = new ArrayList<>();
        arrayList.add("onur");
        arrayList.add("swtestacademy");
        arrayList.add("testing");
        arrayList.add("automation");

        //Print the array list before remove operation.
        System.out.println(arrayList.toString());

        //Remove element which has a value of "onur" and print the array list.
        arrayList.removeIf(arrayElement -> arrayElement.equalsIgnoreCase("onur"));
        System.out.println(arrayList.toString());
    }
}

Output:

[onur, swtestacademy, testing, automation]
[swtestacademy, testing, automation]

As you see above, in removeIf method, we used the lambda expression to remove the element “onur” in the ArrayList.

Example-4: Consumer

Consumer acts opposite of Supplier. It gets a value but returns void. We should use consumer to call void-returning methods. Also, by using consumer we can manipulate data in collections or in classes.

import java.util.function.Consumer;

//In contrasts to Supplier, Consumer gets value but returns nothing (void).
//We can use a consumer for void methods. We can use Consumer to modify data.
public class ConsumerExample {

    static void print (String text) {
        System.out.println("Text is: " + text);
    }

    public static void main(String[] args) {

        // This consumer calls a void counter with the value.
        Consumer<String> consumer1 = a -> print ("Consumer-1 " + a);

        // This consumer calls a void counter with the value.
        Consumer<String> consumer2 = b -> print ("Consumer-2 " + b);

        //Consumer-1
        consumer1.accept("Hi, I am consumer-1!");

        //Consumer-1 and then Consumer-2
        consumer1.andThen(consumer2).accept("Common text for consumers!");
    }
}

Output:

Text is: Consumer-1 Hi, I am consumer-1!
Text is: Consumer-1 Common text for consumers!
Text is: Consumer-2 Common text for consumers!

Example-5: Unary Operator

We can add or insert values in an ArrayList with UnaryOperator such as replaceAll. In the below example, we add “modified” text to all ArrayList items. In other terms, we replace the “ArrayList element value” with “ArrayList element value” + “modified”.

import java.util.ArrayList;

public class UnaryExample {
    public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<>();
        arrayList.add("onur");
        arrayList.add("swtestacademy");
        arrayList.add("java");

        //Before Replace Operation
        System.out.println(arrayList);

        //Replace operation
        arrayList.replaceAll(element -> element + " modified");

        //After Replace Operation
        System.out.println(arrayList);
    }
}

Output:

[onur, swtestacademy, java]
[onur modified, swtestacademy modified, java modified]

Example-6: BiConsumer

BiConsumer gets two parameters. In below example, I used BiConsumer in forEach method on a HashMap to print the content of the HashMap.

import java.util.HashMap;

public class BiConsumerExample {
    public static void main(String[] args) {

        HashMap<String, Integer> hashMap = new HashMap<>();
        hashMap.put("Bill", 33);
        hashMap.put("Jack", 42);
        hashMap.put("Michael", 28);

        //We  use a lambda expression that matches BiConsumer to print HashMap.
        hashMap.forEach((name, age) -> System.out.println("Name: " + name + ", " + "Age: " + age));
    }
}

Output:

Name: Michael, Age: 28
Name: Bill, Age: 33
Name: Jack, Age: 42

Example-7: Performance Benchmark of Function Apply vs Method Call

In below code, we compare the performance of a Function object vs static method. They are both doing the same thing.

import java.util.function.Function;

public class lambdaVSMethodBenchmark {

    static int counter (int counter) {
        return counter + 1;
    }

    public static void main(String[] args) {

        Function<Integer, Integer> function = counter -> counter + 1;

        //Start time
        long time1 = System.currentTimeMillis();

        //Lambda expression
        for (int i = 0; i < 500000000; i++) {
            function.apply(i);
        }

        //Lambda Apply Method End Time
        long time2 = System.currentTimeMillis();

        //Static counter
        for (int i = 0; i < 500000000; i++) {
            counter(i);
        }

        //Static Method End Time
        long time3 = System.currentTimeMillis();

        //Comparison
        System.out.println("Apply Method - Lambda Performance: " + (time2 - time1));
        System.out.println("Static Method: " + (time3 - time2));
    }
}

Output:

Apply Method – Lambda Performance: 23
Static Method: 3

As you see the output, Function object’s apply() method is much slower than the static method. The lambda syntax has less optimization. If a method can be called without any loss of code clarity, this performs better than a lambda or functional object.

Example-8: Stream – Filter

By using a Filter, we remove elements which don’t match a given condition in a stream. Thus, the stream may become shorter after filtering. It also returns a stream and on returned stream, we can use methods such as findFirst to get the first element of the stream or using average to find the average value of the stream. In below code, we will convert an array to a stream by using Arrays.stream and then we use a lambda expression in the filter method and calculate the average value of the numbers which are bigger than 5.

import java.util.Arrays;
import java.util.OptionalDouble;
import java.util.stream.IntStream;

public class FilterExample {
    public static void main(String[] args) {

        //Array Declaration
        int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

        // Convert array to Stream.
        IntStream intStream = Arrays.stream(array);

        //Filter & lambda usage: Average value of the numbers bigger than 5.
        OptionalDouble average = intStream.filter(value -> value > 5).average();

        //If a result is present, display it as an double.
        if (average.isPresent()) {
            //Average value returned by the filter.
            //If we call getAsDouble on an OptionalDouble and If no element exists, we got a NoSuchElementException.
            System.out.println(average.getAsDouble());
        }
    }
}

Output: 8.0

Example-9: Stream – Reduce

By using reduce method from the IntStream class, we apply a method to all elements of the stream. First, we need to convert the array into an InsStream with Arrays.stream and then we use reduce method to perform summation operation for the numbers. By using reduce method, the left part should be 0 and the right part should be the accumulator method.

Sum. An array contains numbers (such as ints). In Java, methods can be used to sum these. A for-loop can instead be used. The approaches have different advantages.

With reduce, a method from the IntStream class, we apply a method to all elements. With an accumulator, we sum the numbers. This approach is more complex than the for-loop.

import java.util.Arrays;
import java.util.stream.IntStream;

public class StreamReduceLambdaExample {

    public static void main(String[] args) {

        //Integer Array
        int[] array = { 2, 4, 8 };

        //Convert array to a stream.
        IntStream intStream = Arrays.stream(array);

        //Use reduce to sum all elements in the array.
        //The left part should be 0. The second part is the accumulator method.
        int total = intStream.reduce(0, (a, b) -> a + b);

        // Print the result.
        System.out.println(total);
    }
}

Output: 14

Also, instead of using lambda function we can also use Integer::sum method which is provided by JAVA.

import java.util.Arrays;
import java.util.stream.IntStream;

public class StreamReduceIntegerSumExample {

    public static void main(String[] args) {

        int[] array = { 8, 9, 10 };
        IntStream intStream  = Arrays.stream(array);

        //Reduce and Integer::sum
        int total = intStream.reduce(0, Integer::sum);

        System.out.println(total);
    }
}

Output: 27

Performance comparison between Integer::sum, Lamda, and for loop is implemented in below code.

import java.util.Arrays;
import java.util.stream.IntStream;

public class SumBenchmark {


    static int reduceIntegerSum (int[] array) {
        //Reduce Integer::sum Method
        IntStream intStream = Arrays.stream(array);
        int summation = intStream.reduce(0, Integer::sum);
        return summation;
    }

    static int reduceLambdaSum (int[] array) {
        //Reduce (a,b) -> a + b Lambda Method
        IntStream intStream = Arrays.stream(array);
        int summation = intStream.reduce(0, (a, b) -> a + b);
        return summation;
    }

    static int foorLoopSum (int[] array) {
        //For loop sum
        int summation = 0;
        for (int i = 0; i < array.length; i++) {
            summation += array[i];
        }
        return summation;
    }

    public static void main(String[] args) {

        int[] array = { 10, 20, 30 };

        long time1 = System.currentTimeMillis();

        //Call reduce Integer::sum
        for (int i = 0; i < 10000000; i++) {
            reduceIntegerSum(array);
        }

        long time2 = System.currentTimeMillis();

        //Call reduce lambda
        for (int i = 0; i < 10000000; i++) {
            reduceLambdaSum(array);
        }

        long time3 = System.currentTimeMillis();

        //Call "for loop" sum
        for (int i = 0; i < 10000000; i++) {
            foorLoopSum(array);
        }

        long time4 = System.currentTimeMillis();

        //Comparison
        System.out.println("Ingeter::sum performance: " + (time2 - time1));
        System.out.println("Lambda sum performance: " + (time3 - time2));
        System.out.println(time4 - time3);
    }
}

Output:

Ingeter::sum performance: 1024
Lambda sum performance: 791
For Loop sum performance: 10

Note:

As you see that for loop performance is much faster than reduction constructs. Also, the Java documentation states that using functional constructs (such as reduce) is slower and more complex than for-loops. However, these constructs are more advantageous in parallel computations. By using them, we can run big computations in multiple threads (parallel) and in these kinds of cases, these constructs become faster. Reduction operations parallelize more gracefully. They don’t need additional synchronization and they are safe against race conditions. One of the main purposes of lambdas is parallel computing – which means that they’re really helpful when it comes to thread-safety.

Lambda Expressions and Stream Usage Examples In Test Projects

Example-1: Map Key, Value Check

In below code, we got a Map type values and convert them to a stream then check the map’s values are not null, then again convert them from stream to a map. I used this code in my API Testing frameworks.

    public static Map<String, String> get (Map<String, String> formParams) {
        return formParams
                .entrySet()
                .stream()
                .filter(entry -> entry.getValue() != null)
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

Example-2: Switch Window

In below code, I call the switchToWindow method with title parameter. Inside the method, I get all the window handles. Then, I convert the handles to stream, then I am mapping handles and the title of the pages. If the given title parameter is found in the stream, window switch operation will be performed. If not, an exception will be thrown. We can use this method in BasePage class or in a helper/util classes where the driver instance must be available and not null.

public void switchToWindow (String title) {
        driver.getWindowHandles()
                .stream()
                .map(windowHandle -> driver.switchTo().window(windowHandle).getTitle())
                .filter(title::contains)
                .findFirst()
                .orElseThrow(() -> {
                    throw new RuntimeException("No Such Window Exists!");
                });
}

Example-3: Consumer Usage

We can also use Consumer to fulfill a set of operations in test classes as shown in below example. consumer.accept() performs all operations which are defined in the test class.

FormPage.java (Page Class)

//.......

//fillTheFields method in the FormPage class
public void fillTheFields (Consumer<FormPage> consumer){
    consumer.accept(this);
}

//...... 
//setName, setSurname, setWebsite, setCity methods are defined in this class.

FormPageTest.java (Test Class)
//.....

FormPage formPage = New FormPage(driver);  //You need to create page instance.


formPage.fillTheFields(page -> {
    page.setName("Onur");
    page.setSurname("Baskirt");
    page.setWebsite("swtestacademy");
    page.setCity("Dubai");
});

//.....

Example-4: allMatch (Example by Canberk Akduygu)

We need to make sure that every type and location attributes should be filled out for every record. So we use below stream operation.

Response Data:

    {
        "data": {
        "records": [
        {
            "date": 1534940126000,
                "type": "sale",
                "location": "AMS",
                "data": null
        },
        {
            "date": 1534940127000,
                "type": "rental",
                "location": "AMS",
                "data": null
        },
        {
            "date": 1534940127000,
                "type": "rental",
                "location": "Haarlem",
                "data": null
        }
        ],
        "totalCount": 3
    },
        "status": "success"
    }

First, we export the records array into a Collection of HashMap.

Collection<HashMap<String, ?>> resultMap = r.path(“data.records”);

Then we use stream.allMatch() check if every specified value is empty or not.

Assert.assertTrue(!resultMap.stream().allMatch(s -> s.get(“type”).toString().isEmpty()));

Assert.assertTrue(!resultMap.stream().allMatch(s -> s.get(“location”).toString().isEmpty()));

Example 5: anyMatch (Example by Canberk Akduygu)

By using this expression you check if there is at least one element matching your need.

We added a new user into the system and we check if he is in user list or not.

Response Data:

    {
        "records": [
        {
            "id": 11095,
                "firstName": "John",
                "lastName": "Doe",
                "email": "user1@gmail.com",
                "timestamp": 1537343162157,
                "status": "ACCEPTED
        },
        {
            "id": 11084,
                "firstName": "Jane",
                "lastName": ”Doe",
            "email": "user2@gmail.com",
                "timestamp": 1537341736342,
                "status": "ACCEPTED",
        },
        {
            "id": 11036,
                "firstName": "Canberk",
                "lastName": "Akduygu",
                "email": "user3@gmail.com",
                "timestamp": 1537180322381,
                "status": "ACCEPTED",
        }    
    ],
        "totalCount": 4
    }

This time, we extract only a value from Array Object.

Collection<String> coll = respGet.jsonPath().getList(“records.email”);

Then search for an email that we used to create a user in the response data.

Assert.assertTrue(coll.stream().anyMatch((s -> s.equals(user.getMail()))));

I hope this article will be helpful to you to create better and concise test automation frameworks in your projects.

Thanks.
Onur Baskirt

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.