Skip to content

Simplify implementation of self-contained dynamic tests #3261

@odrotbohm

Description

@odrotbohm

@TestFactorys and DynamicTests provide API to implement multiple test probes that will become individual tests. They're centered around the idea that one would create test fixtures as instances of types and provide API to then define the naming of the test and the actual assertion:

class MyTests

  @TestFactory
  Stream<DynamicTest> foo() {

    var fixture = Stream.of(new Foo(1, 1, 2), new Foo(2, 3, 5));

    return DynamicTest.stream(fixture, it -> assertThat(it.left() + it.right()).isEqualTo(it.result()));
  }

  record Foo(int left, int right, int result) implements Named<Foo> {

    @Override
    public String getName() {
      return left + " + " + right + " = " + result;
    }

    @Override
    public Foo getPayload() {
      return this;
    }
  }
}

There are a few things to note here:

  • We need to implement getPayload() to return the instance itself to satisfy Named although that method doesn't play into the signature of DynamicTest.stream(…) as the elements of the stream are handed into the callable.
  • It would be nice if we were able to colocate the assertion callback with the fixture type so that the implementation of the test factory would only have to return a stream of fixtures.

Assuming an extension of Named looking like this existed:

interface NamedExecutable<T extends NamedExecutable<T>> extends Named<T>, Executable {

  @Override
  public default T getPayload() {
    return (T) this;
  }
}

a bit of syntactical sugar on DynamicTest:

@SafeVarargs
public static <T extends NamedExecutable<T>> Stream<DynamicTest> stream(
    NamedExecutable<T>... inputGenerator) {
  return stream(Arrays.stream(inputGenerator));
}

public static <T extends NamedExecutable<T>> Stream<DynamicTest> stream(
    Stream<? extends NamedExecutable<T>> inputGenerator) {
  Preconditions.notNull(inputGenerator, "inputGenerator must not be null");

  return DynamicTest.stream(inputGenerator, it -> it.execute());
}

Would allow us to reduce the very first code sample to:

public class DummyTest {

  @TestFactory
  Stream<DynamicTest> foo() {
    return DynamicTest.stream(new AdderTest(1, 1, 2), new AdderTest(2, 3, 5));
  }

  record AdderTest(int left, int right, int result) implements NamedExecutable<AdderTest> {

    @Override
    public String getName() {
      return left + " + " + right + " = " + result;
    }

    @Override
    public void execute() throws Throwable {
      assertThat(left + right).isEqualTo(result);
    }
  }
}

This allows to define parameterize test fixtures nicely and colocate the assertions within a type and the test factory method's responsibility is reduced to set up the individual probes.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions