1. Overview

In this article, we’ll discuss a new Java-based testing framework called Lambda Behave.

As the name suggests, this testing framework is designed to work with Java 8 Lambdas. Further, in this article, we’ll look into the specifications and see an example for each.

The Maven dependency we need to include is:

<dependency>           
    <groupId>com.insightfullogic</groupId>
    <artifactId>lambda-behave</artifactId>
    <version>0.4</version>
</dependency>

The latest version can be found here.

2. Basics

One of the goals of the framework is to achieve great readability. The syntax encourages us to describe test cases using full sentences rather just a few words.

We can leverage parameterized tests and when we don’t want to bound test cases to some predefined values, we can generate random parameters.

3. Lambda Behave Test Implementation

Every specification suite begins with Suite.describe. At this point, we have several built-in methods to declare our specification. So, a Suite is like a JUnit test class, and the specifications are like the methods annotated with @Test in JUnit.

To describe a test, we use should(). Similarly, if we name the expectation lambda parameter as “expect”, we could say what result we expect from the test, by expect.that().

If we want to setup or tear down any data before and after a specification, we can use it.isSetupWith() and it.isConcludedWith(). In the same way, for doing something before and after the Suite, we’ll use it.initiatizesWith() and it.completesWith().

Let’s see an example of a simple test specification for the Calculator class:

public class Calculator {

    public int add() {
        return this.x + this.y;
    }

    public int divide(int a, int b) {
        if (b == 0) {
            throw new ArithmeticException();
        }
        return a / b;
    }
}

We’ll start with Suite.describe and then add the code to initialize the Calculator.

Next, we’ll test the add() method by writing a specification:

{
    Suite.describe("Lambda behave example tests", it -> {
        it.isSetupWith(() -> {
            calculator = new Calculator(1, 2);
        });
 
        it.should("Add the given numbers", expect -> {
            expect.that(calculator.add()).is(3);
        });
}

Here, we named variables “it” and “expect” for better readability. Since those are lambda parameter names, we can replace these with any names of our choice.

The first argument of should() describes using plain English, what this test should check. The second argument is a lambda, that indicates our expectation that the add() method should return 3.

Let’s add another test case for division by 0, and verify if we get an exception:

it.should("Throw an exception if divide by 0", expect -> {
    expect.exception(ArithmeticException.class, () -> {
        calculator.divide(1, 0);
    });
});

In this case, we are expecting an exception, so we state expect.exception() and inside that, we write the code that should throw an exception.

Note, that the text description must be unique for every specification.

4. Data-Driven Specifications

This framework allows test parameterization at the specification level.

To create an example, let’s add a method to our Calculator class:

public int add(int a, int b) {
    return a + b;
}

Let’s write a data-driven test for it:

it.uses(2, 3, 5)
  .and(23, 10, 33)
  .toShow("%d + %d = %d", (expect, a, b, c) -> {
    expect.that(calculator.add(a, b)).is(c);
});

The uses() method is used to specify input data in different numbers of columns. The first two arguments are the add() function parameters and the third one is the expected result. These parameters can also be used in the description as shown in the test.

toShow() is used to describe the test using the parameters – with the following output:

0: 2 + 3 = 5 (seed: 42562700892554)(Lambda behave example tests)
1: 23 + 10 = 33 (seed: 42562700892554)(Lambda behave example tests)

5. Generated Specifications – Property-Based Testing

Usually, when we write a unit test, we want to focus on broader properties that hold true for our system.

For example, when we test a String reversing function, we might check that if we reverse a particular String twice, we’ll end up with the original String.

Property-Based testing focuses on the generic property without hard-coding specific test parameters. We can achieve this by using randomly generated test cases.

This strategy is similar to using Data-Driven specifications, but instead of specifying the table of data, we specify the number of test cases to be generated.

So, our String reversal property-based test would look like this:

it.requires(2)
  .example(Generator.asciiStrings())
  .toShow("Reversing a String twice returns the original String", 
    (expect, str) -> {
        String same = new StringBuilder(str)
          .reverse().reverse().toString();
        expect.that(same).isEqualTo(str);
   });

We have indicated the number of required test cases using the requires() method. We use the example() clause to state what type of objects we need and how.

The output for this specification is:

0: Reversing a String twice returns the original String(ljL+qz2) 
  (seed: 42562700892554)(Lambda behave example tests)
1: Reversing a String twice returns the original String(g) 
  (seed: 42562700892554)(Lambda behave example tests)

5.1. Deterministic Test Case Generation

When we use the auto-generated test cases, it becomes quite difficult to isolate test failures. For example, if our functionality fails once in 1000 times, a specification that auto-generates just 10 cases, will have to be run over and over to observe the error.

So, we need the ability to deterministically re-run tests, including previously failed cases.

Lambda Behave is able to deal with this problem. As shown in the output of previous test case, it prints out the seed that was used to generate the random set of test cases. So, if anything fails, we can use the seed to re-create previously generated test cases.

We can look at the output of the test case and identify the seed: (seed: 42562700892554). Now, to generate the same set of tests again, we can use the SourceGenerator.

The SourceGenerator contains the deterministicNumbers() method that takes just the seed as an argument:

 it.requires(2)
   .withSource(SourceGenerator.deterministicNumbers(42562700892554L))
   .example(Generator.asciiStrings())
   .toShow("Reversing a String twice returns the original String", 
     (expect, str) -> {
       String same = new StringBuilder(str).reverse()
         .reverse()
         .toString();
       expect.that(same).isEqualTo(str);
});

On running this test, we’ll get the same output as we saw previously.

6. Conclusion

In this article, we saw how to write unit tests using the Java 8 lambda expressions, in a new fluent testing framework, called Lambda Behave.

As always the code for these examples can be found over on GitHub.


« 上一篇: Java Weekly, 第191期
» 下一篇: Log4j 2与Lambda表达式