Skip to content

lefou/LambdaTest

Repository files navigation

Lambda Test

Motivation

When constrained to work in a Java-only toolchain, I really miss ScalaTest. I tried some lambda enabled test frameworks, but until now (2014), did not find a suitable solution without compromizing the integration benefits.

Thus, I decided to write a small and generic test library that allows writing of functional test without reinventing the wheel. LambdaTest works on top of JUnit and TestNG, all you need is to add it to the test classpath. No further adaptions to your existing test setup are needed. You will immediately gain the joy of Lambda-enabled functional testing, better assertion messages and nicely colored output.

Features

Most important features are:

  • Write test via API (No longer required to have each test in a separate annotated method)

  • Meaningful names for tests

  • Nicely colored output per test case

  • Easy to write data-centric tests (e.g. generate as much test cases as you need programmatically, e.g. in a loop)

  • Easy to intercept exceptions with intercept

  • Useful assertion message and difference highlighting in expectXXX-methods

  • Opt-in to not fail fast when using expectXXX-methods (see more than the first assertion error)

  • Easy to mark pending tests

  • Contains useful tools to work with temporary files and directories

  • Easy way to create proxies as mock dependencies

Documentation

Beside this document, you can also read the JavadDoc for LambdaTest

Download from Maven Central

LambdaTest is available from Maven central repository.

Maven users can use the following dependency declaration:

<dependencies>
  <dependency>
    <groupId>de.tototec</groupId>
    <artifactId>de.tobiasroeser.lambdatest</artifactId>
    <version>0.8.0</version>
    <scope>test</scope>
  </dependency>
  <!-- If you use LambdaTest with JUnit -->
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
  </dependency>
  <!-- If you use LambdaTest with TestNG -->
  <dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
    <version>6.11</version>
    <scope>test</scope>
  </dependency>
  <!-- If you use LambdaTest with JUnit5 / Jupiter -->
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.9.2</version>
    <scope>test</scope>
  </dependency>
</dependencies>

Choose your favorite Unit-Test Runner: TestNG, JUnit, Junit5 (Jupiter)

With LambdaTest, you need to only know LambdaTests very simple and minimalistic API but can use it to write test for JUnit and TestNG.

To avoid a dependency to both frameworks at the same time, your test class inherits a different base class, but besides that, everything else is the same.

For Junit 5 (Jupiter) you inherit de.tobiasroeser.lambdatest.junit5.FreeSpec.

Note

If you use LambdaTest with mill.testng.TestNGFramework (in Mill or sbt), you may see extra verbose output. You can disable the progress output of mill.testng.TestNGFramework by setting the mill.testng.printProgress property to 0.

build.sc: Make mill.testng.TestNGFramework runner less verbose in Mill
object test extends TestModule.TestNg {
  override def forkArgs = T{ super.forkArgs() ++ Seq("-Dmill.testng.printProgress=0") }
}

Writing tests with Lambda Test

The test cases can be defined in various places.

  • in the class constructor

  • in the protected void initTests() method

  • in a class instance initializer

Here you see a basic test example, which produces a valid TestNG test class. You need to extend from class de.tobiasroeser.lambdatest.testng.FreeSpec.

import static de.tobiasroeser.lambdatest.Expect.expectEquals;
// You can also use JUnit or Junit 5 (Jupiter) based tests with
// import de.tobiasroeser.lambdatest.junit.FreeSpec;
// import de.tobiasroeser.lambdatest.junit5.FreeSpec;
import de.tobiasroeser.lambdatest.testng.FreeSpec;

public class SimpleTest extends FreeSpec {
  public SimpleTest() {

    test("1 + 1 = 2", () -> {
      expectEquals(1 + 1, 2);
    });

    test("a pending test", () -> pending());

    test("divide by zero", () -> {
      int a = 2;
      int b = 0;
      intercept(ArithmeticException.class, () -> {
        int c = a / b;
      });
    });

    section("A String should", () -> {
      final String aString = "A string";

      test("match certain criteria", () -> {
        expectString(aString)
          .contains("string")
          .containsIgnoreCase("String")
          .startsWith("A")
          .endsWith("ng")
          .hasLength(8);
      });

      test("be not longer than 2", () -> {
        expectString(aString).isLongerThan(2);
      });
    });

    test("demo of a fail", () -> {
      "yes".equals("yes and no");
    });

    {
      test("test in initializer", () -> {
	    expectTrue(true);
      });
	}
  }

  // You can also define test here, to avoid
  // their initialization at class construction time
  @Override protected void initTests() {
  	test("should succeed (lazy init)", () -> {
	  expectTrue(true);
  	});
  }
}

The methods test, pending and intercept are provided by FreeSpec whereas the usual expectXXX methods are provided by Expect.

The output of this test suite above would look like this:

Screenshot SimpleTest

Note

You can run the above test directly in the LambdaTest project directory with:

mvn test -Dtest=SimpleTest

You should write your test cases so that they don’t need to be executed in order. LambdaTest is able to run tests in parallel, if you enable it explicitly with FreeSpec.setRunInParallel(true).

By default expectXXX-methods fail fast, which means the first failing assertion will end the whole test. This is also the behaviour you will get with most other test frameworks.

But you can disable fail-fast behaviour for assertions/expectations with FreeSpec.setExpectFailFast(false). Then, the first failing expectXXX-error will not abort the test but the test is optimistically continued. Further failing assertion errors are collected and the test fails at the end, reporting all collected errors.

Writing assertions with Expect

LambdaTest provides many methods in the class de.tobiasroeser.lambdatest.Expect to write assertion. You can use these as an alternative to the assertion methods provides by other unit testing framework to gain the following advantages:

  • Nice output of differences between expected and actual values. Especially for string and various collection types

  • expectXXX-methods provide a feature to collect multiple assertions (non-fail-fast behaviour), such that you can collect as much errors as possible in one test run, instead of giving up at the first error.

Selected static methods of Expect
  • expectNull - Assert that a given value is null

  • expectNotNull - Assert that a given value is not null

  • expectEquals - Assert equality of two given objects or values.

  • expectNotEquals - Assert non-equality of two given objects or values.

  • expectTrue - Assert a value evaluates to true

  • expectFalse - Assert a value evaluates to false

  • expectDouble - Assert that a given double is non-null and return an instance of ExpectDouble with provides further checks on the actual double in a fluent API

  • expectString - Assert that a given string is non-null and return an instance of ExpectString with provides further checks on the actual string in a fluent API

  • expectCollection - Assert that a given collection is non-null and return an instance of ExpectCollection with provides further checks on the actual colletion in a fluent API

  • expectMap - Assert that a given map is non-null and return an instance of ExpectMap with provides further checks on the actual map in a fluent API

  • intercept - Assert that a code block throws an Exception of the given type and optional with an message matching a given regular expression. Returns the thrown exception for further analysis

There are more method in Expect with setup and control it non-fail-fast handling via ThreadLocals. Those are only needed, if you want to use these behaviour outside of FreeSpec.

Note

If you want to use the non-fail-fast behaviour of the Expect class outside of FreeSpec, you have to take care of setup and finalization by yourself.

Fluent API to investigate common types

All fluet API ExpectXXX classes support the fail-late behaviour.

Analyze Strings with ExpectString

To inspect and assert strings, use the class ExpectString, which is also returned, if you use Expect.expectString.

Methods of ExpectString
  • isEqual

  • isNotEqual

  • isEqualIgnoreCase

  • isNotEqualIgnoreCase

  • startsWith

  • StartsWithNot

  • endsWith

  • endsNotWith

  • matches

  • matchesNot

  • hasLength

  • hasLengthNot

  • isLongerThan

  • isShorterThan

  • isTrimmed

  • contains

  • containsNot

  • containsIgnoreCase

  • containsIgnoreCaseNot

Analyze Collections with ExpectCollection

To inspect and assert collections, use the class ExpectCollection, which is also returned, if you use Expect.expectCollection.

Analyze Maps with ExpectMap

To inspect and assert maps, use the class ExpectMap, which is also returned, if you use Expect.expectMap.

Analyze Doubles with ExpectDouble

To inspect and assert doubles, use the class ExpectMap, which is also returned, if you use Expect.expectDouble.

Methods of ExpectDouble
  • isCloseTo

  • isNotCloseTo

  • isBetween

  • isNotBetween

  • isNaN

  • isNotNaN

Testing files and directories with TempFile

LambdaTest comes with a helper class de.tobiasroeser.lambdatest.TempFile which contains useful methods to work with temporary files.

To create a temporary file with a given content and do something with it, you can use TempFile.withTempFile or it procedural version with does not return a value TempFile.withTempFileP. After the method completes, the temporary file will automatically deleted.

To create and work with temporary files, you can use TempFile.withTempDir and TempFile.withTempDirP`. Those will be recursively deleted after completion.

There are more useful methods in class TempFile, e.g. readFile, writeToFile and deleteRecursive. Please inspect the class for more information.

Using TestProxy to create mock objects

The general idea in unit testing is to isolate a class under test from its dependencies.

An naive way to do this is to create dummy implementations, but this can be a very cumbersome, repetitive and booring task. Also it creates a lot of boilerplate code, which is also unnecessary hard to maintain.

On the opposite end, you can find very advanced mocking frameworks which will create mocks that can be trained and replayed, but the resulting code is no longer easy to understand and also adds lots of new dependencies.

Therefore in the middle there is TestProxy to easily create dummy proxy instances. By default, each invoked method on the proxy will throw an UnsupportedOperationException with a meaningful detail message.

You can also provide explicit behaviour to your proxy by providing delegate objects. Whenever a method is invoked on the proxy, the given objects will be checked if they contain a method with a matching signature, and if so, that method will be invoked an behalf of the proxy.

You can either use the more explicit way with TestProxy.proxy(ClassLoader, List<Class<?>>, List<Object>) or the more convenient and compact TestProxy.proxy(Object…​) method.

Example Test using TestProxy
package org.example;

import static de.tobiasroeser.lambdatest.Expect.expectEquals;

import de.tobiasroeser.lambdatest.proxy.TestProxy;
import de.tobiasroeser.lambdatest.testng.FreeSpec;

public class ExampleProxyTest extends FreeSpec {
  interface Dependency {
    String hello();
  }

  class ServiceWithDependency {
    private Dependency dependency;

    public ServiceWithDependency(final Dependency dependency) {
      this.dependency = dependency;
    }

    String usingDependency() {
      return dependency.hello();
    }

    String notUsingDependency() {
      return "Have a nice day!";
    }
  }

  public ExampleProxyTest() {

      test("A proxy without delegates as optional dependencies should be sufficient", () -> {
        final Dependency dep = TestProxy.proxy(Dependency.class);
        final ServiceWithDependency service = new ServiceWithDependency(dep);
        expectEquals(service.notUsingDependency(), "Have a nice day!");
      });

      test("A proxy without delegates as mandatory dependencies should fail", () -> {
        final Dependency dep = TestProxy.proxy(Dependency.class);
        final ServiceWithDependency service = new ServiceWithDependency(dep);
        intercept(UnsupportedOperationException.class, () -> {
          service.usingDependency();
        });
      });

      test("A proxy with delegates as mandatory dependency should succeed", () -> {
        final Dependency dep = TestProxy.proxy(Dependency.class, new Object() {
          @SuppressWarnings("unused")
          public String hello() {
            return "Hello Proxy!";
          }
        });
        final ServiceWithDependency service = new ServiceWithDependency(dep);
        expectEquals(service.usingDependency(), "Hello Proxy!");
      });

  }
}

Working on Java7

Even though writing functional test makes most sense under Java 8+, there are enough reasons to also use them on older Java versions which do not provide nice closures.

LambdaTest versions up to 0.7.1 didn’t use any Java 8 API! You can download pre-compiled binaries of LambdaTest for older Java 7 Runtimes. To use the non-Java8 version with Maven, use a classifier ("java7") to download the version you want. The compatibility packages were produced with the great retrolambda project.

To use the latest Java7 compatible version 0.7.1 in Maven:

<dependencies>
  <dependency>
    <groupId>de.tototec</groupId>
    <artifactId>de.tobiasroeser.lambdatest</artifactId>
    <version>0.7.1</version>
    <classifier>java7</classifier>
    <scope>test</scope>
  </dependency>
  <!-- Also you need one of JUnit or TestNG, see above -->
</dependencies>

Instead of Java 8 Closures, you have to create anonymous classes.

import static de.tobiasroeser.lambdatest.Expect.expectEquals;
import de.tobiasroeser.lambdatest.RunnableWithException;
import de.tobiasroeser.lambdatest.junit.FreeSpec;

class SimpleTest extends FreeSpec {
  public SimpleTest() {

    test("1 + 1 = 2", new RunnableWithException() {
      public void run() throws Exception {
        expectEquals(1 + 1, 2);
      }
    });

    test("divide by zero", new RunnableWithException() {
      public void run() throws Exception {
        int a = 2;
        int b = 0;
        intercept(ArithmeticException.class, new RunnableWithException() {
          public void run() throws Exception {
            int c = a / b;
          }
        });
      }
    });
  }
}

Build LambdaTest from Source

Building with Maven

LambdaTest is build with Apache Maven 3.3.1 and the polyglot-scala extension. Maven 3.5 is recommended.

Build LambdaTest from source
mvn clean install

The built JARs file can be found in the target directory.

Create pom.xml for interoperability, e.g. IDEs

To generate the pom.xml use the gen-pom-xml profile.

Creating pom.xml files
mvn -Pgen-pom-xml initialize
Deleting generated pom.xml files
mvn -Pgen-pom-xml clean

Licence

This project is published under the Apache Licence Version 2.0.

Contribution / Contact

Your feedback is highly appreciated. I also accept pull request.

For questions please use the Gitter chatroom. To report issues or send pull request, use GitHub.

You can also find me on Twitter as @TobiasRoeser.

If you like LambdaTest, please star it on GitHub. This will help me to set my priorities. Thanks!

Changelog

LambdaTest 0.8.0 - 2023-02-28

  • Added support for JUnit 5 aka Jupiter.

  • Dropped released for Java 7

  • No longer use Asciidoclet to generate JavaDoc

LambdaTest 0.7.1 - 2021-06-28

  • Added support for lazily initialized tests (with initTests())

LambdaTest 0.7.0 - 2019-01-10

  • Added Expect.expectDouble and ExpectDouble class to assert properties of double values with a fluent API.

  • Improved error message in TestProxy for missing implemented proxy methods with array parameters

LambdaTest 0.6.2 - 2018-08-01

  • Fixed invalid Manifest entry Import-Package present in older releases.

LambdaTest 0.6.1 - 2018-07-24

  • Dropped support for Java 6. (Technically speeking, Java 6 wasn’t really supported by older versions, as a contructor of java.lang.AssertionError was used, which was only introduced in Java 7.)

  • Proxies created with TestProxy gained better copy’n’paste able error message in case of unimplemented methods were call.

LambdaTest 0.6.0 - 2018-06-22

  • Added Expect.expectCollection and ExpectCollection class to assert properties of collection with a fluent API.

  • TestProxy now properly passes exceptions thrown by delegate objects.

  • Improved detection of test name collisions (reported as suite warning).

  • The default reporter can now be set via FreeSpecBase.setDefaultReporter.

  • Added Expect.expectMap and ExpectMap class to assert properties of maps with a fluent API.

LambdaTest 0.5.0 - 2018-06-11

  • Added TestProxy in package de.tobiasroeser.lambdatest.proxy to easily create mock dependencies / proxies.

LambdaTest 0.4.0 - 2018-03-05

  • Detect logging framework (slf4j or java.util.logging) and log test progress

  • Added internal.Logger and internal.LoggerFactory to wrap either an Slf4j-Logger or a JUL-Logger, both supporting Slf4j parameter placeholders.

  • Added new LoggingWrappingReporter which logs to an logging frameorg and delegates all methods to an underlying Reporter.

  • Changed handling of the "optional" msg-paramter in Assert.assertXXX and Expect.expectXXX methods. If given, the msg-parameter does no longer replace the generic assertion message, instead both messages are shown, first the given message, then the generic message.

  • Better handle arrays with primitive types.

  • Fixed issue, where a failing assert could throw a ClassCastExcpetion for primitive type arrays.

LambdaTest 0.3.1 - 2018-02-13

  • DefaultReporter can now hide the stacktrace

  • Improved expectEquals message for number and arguments of different types

  • Fixed inverted expectNotNull behaviour

  • Extended test suite

LambdaTest 0.3.0 - 2017-10-30

  • Introduced Reporter interface and DefaultReporter class

  • Introduced generic base class FreeSpecBase to hold test framework unspecific logic

  • Added FreeSpec.section to allow more structured tests

  • Reformatted output of tests (handled now by Reporter)

  • Added Assert.assertNull and Assert.assertNotNull

  • Added Expect.expectNull and Expect.expectNotNull

  • Generate proper OSGi manifests for all JARs/bundles

  • Fixed assert message of Intercept.intercept(Class<T>, String, RunnableWithException)

  • Documentation improvements

LambdaTest 0.2.4 - 2016-08-03

  • Added a pending method with a reason parameter.

  • Added more JavaDoc comments.

LambdaTest 0.2.3 - 2016-05-10

  • Fixed fail late logic for Expect

  • Added new class ExpectString and Expect.expectString for fluent string assertions.

LambdaTest 0.2.2 - 2016-05-03

  • Fixed Assert.assertEquals for Strings, especially when expected is empty or shorter than actual.

LambdaTest 0.2.1 - 2016-03-04

  • Added TempFile, an utility class providing support to work with temporary files and directories which will be automatically cleaned up after the test case.

LambdaTest 0.2.0 - 2016-02-12

  • Added JUnit support. You can now use de.tobiasroeser.lambdatest.junit.FreeSpec for JUnit based tests in addition to the already existing de.tobiasroeser.lambdatest.testng.FreeSpec for TestNG based tests.

  • Introduced new Assert and Expect classes. Expect-based asserts also support deferred exceptions. FreeSpec already integrates the setup of Expect.

LambdaTest 0.1.0 - 2014-12-08

  • Also release compatibility packages for older Java runtimes: Java7 and Java6. They are available via the "java7" and "java6" classifier.

  • FreeSpec.intercept now returns the intercepted exception.

  • Added FreeSpec.setRunInParallel to enable option to run tests in parallel.

  • Introduced LambdaTest interface (implemented by testng.FreeSpec) to contain the common API.

Rerelease of LambdaTest 0.0.3 - 2014-11-29

  • Rerelease of 0.0.3 under "de.tototec" groupId. You can now grab it directly from Maven Central without configuring a dedicated bintray repository.

LambdaTest 0.0.3 - 2014-11-16

  • Fixed issue with missapplied close of STDOUT stream

  • Fixed bug preventing from TestNG seeing pending tests as skipped

  • de.tobiasroeser.lambdatest.testng.FreeSpec class no longer inherits org.testng.Assert

LambdaTest 0.0.2 - 2014-10-18

  • Colored output

  • Added support to match exception messages with regex in intecept

LambdaTest 0.0.1 - 2014-10-15

  • First release