The design goals of valid8j are:

  • Make the test code human-readable

  • Make the error messages human-readable

  • Remove repetitive work from human

    • Minimize the necessity to repeat what code does as a string message.

    • Remove the fail → fix → run (→ fail → fix → run)* loop.

  • Make it easy to write value checks in DbC’s {pre,invariant,post} condition checks, value checks in product code, assertions in test code.

  • Provide programmers with uniformed experience for these use cases.

In this section, concepts that back these goals will be discussed.

"Fluent" programming model: Statements, Transformers, and Checkers

Like AssertJ[10] and Truth[11], valid8j supports a "Fluent" API. You can start writing your checks from Expectations class, then let your IDE help you.

Before static import
public class DbC {
  public void aMethod(int a) {
    assert Expectations.precondition(Expectations.that(a).satisfies()
                                                         .greaterThan(0)
                                                         .lessThan(100));
  }
}

Then, do static import to improve readability.

After static import
public class DbC {
  public void aMethod(int a) {
    assert precondition(that(a).satisfies()
                               .greaterThan(0)
                               .lessThan(100));
  }
}

The methods for evaluating condition (precondition) and the methods to create the condition are separated. This us to use the consistent syntax for different contexts such as DbC assertions, test assertions, argument checking, etc.

For instance, to build a test assertion, you use Expectations.assertStatement method, if you have only one variable to be checked.

single statement
public class TestExample {
  @Test
  public void testMethod() {
    assertStatement(that(a).satisfies()
                           .greaterThan(0)
                           .lessThan(100));
  }
}
In valid8j, performing a check and building a statement to be checked are separate activities as discussed above. Both start from the Expectations class.

"Transform-and-Check" Model

Either way, we human cannot understand complex predicate without learning cost. AssertJ[10] has a huge set of checking methods, but what they do cannot be understood without reading documentation. For instance, its support for RangeSet of Google Guava has:

  • …​

  • doesNotEncloseAnyRangesOf(RangeSet<T> rangeSet)

  • …​

  • hasSize(int size)

  • intersects(Range<T>…​ ranges)

  • intersectsAll(RangeSet<T>…​ rangeSet)

  • intersectsAll(Iterable<Range<T>> ranges)

  • intersectsAnyOf(Range<T>…​ ranges)

  • …​

This may make users think of "What does hasSize(int) check? The number of ranges in the target RangeSet?", but actually it checks if the range set has a range whose size is size. For each verb "contain", "intersect" and "enclose", it has variations of All, AnyOf, and doesNot.

The approach valid8j takes to address these challenges is:

  • Provide an easy way to support user type for value transformation of into another.

  • Provide rich supports in value checking for limited number of basic types.

Note that The value transformation can be repeated multiple times, and it will be eventually converted into basic types, such as string, numbers, boolean, and their arrays.

Easy user type support for transformation

If you have a class Book as follows:

public static class Book {
  private final String abstractText;
  private final String title;

  public Book(String title, String abstractText) {
    this.abstractText = abstractText;
    this.title = title;
  }

  String title() {
    return title;
  }

  String abstractText() {
    return abstractText;
  }
}

In order to support it in valid8j 's framework, you just need to write a class: BookTransformer, which is straight-forward.

public class BookTransformer extends Expectations.CustomTransformer<BookTransformer, Book> {
  public BookTransformer(Book rootValue) {
    super(rootValue);
  }

  public StringTransformer<Book> title() {
    return toString(Printables.function("title", Book::title));
  }

  public StringTransformer<Book> abstractText() {
    return toString(Printables.function("abstractText", Book::abstractText));
  }
}

This can be used in your test code like this:

public class BookExample {
  @Test
  public void givenBook_whenCheckTitleAndAbstract_thenTheyAreNotNullAndAppropriateLength() {
    Book book = new Book(
        "De Bello Gallico",
        "Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, "
            + "aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur.");
    assertAll(
      new BookTransformer(book)
          .title()
          .parseInt()
          .satisfies()
          .greaterThanOrEqualTo(10)
          .lessThan(40));
  }
}

Limited number of types are supported for checking

The valid8j 's philosophy of "transform-and-check" is to divide an assertion into two. One is to transform a given value to a value which we can easily check and understand. The other is to check the transformed value.

What values are easy to check and understand if the check fails? Even if we use complex types and test them, we don’t actually test them and understand their failures directly. In reality, we test and understand their components, which may be strings, numbers, a collection of theirs.

The types valid8j offers for checking are:

  • String

  • int (Integer)

  • long (Long)

  • short (Short)

  • float (Float)

  • double (Double)

  • Stream<T>

  • List<T>

  • Throwable

Each of them has its own transformer and checker out-of-box. Depending on the type to be supported, individual transformers and checkers have transforming and checking methods. For instances, StringTransformer has length method, which returns IntegerTransformer, IntegerChecker has greaterThan method to check if the current value satisfies the conditions built by the methods users called.

As the example shows, if you call satisfies method, the current transformer returns a checker for the type is returned. You can also use toBe method or then method depending on context to maximize the readability.

Author decided not to support primitives directly but trust and rely on Java’s wrapper classes.
Currently BigDecimal is not supported.

Throwing an Exception, instead of returning false

When false is given to an assert statement, JVM throws an AssertionError with a message given to the statement, if any.

class AClass {
  void aMethod() {
    assert aCondition() : "aMessage";
  }
}

But in valid8j, you don’t need to give your own message by this syntax. You just need to give a condition to be checked and an appropriate message will be composed automatically on a failure.

class AClass {
  void aMethod() {
    assert precondition(that(value).satisfies().predicate(isFruit()));
  }
}

How can this be done? Actually, the precondition method returns true if and only if the value isFruit(), but it does never return false. Instead, if the given Statement doesn’t satisfy the predicate, it will compose an informative message and create a new exception by itself and throw it before assert statement throws an AssertionError.

Make Predicates Printable and Composable.

In Java’s ecosystem, there is no reliable way to make lambdas human-readable. The approach valid8j took is to override toString method of Predicate and Function so that it returns a string that a user gives to a lambda.

However, it is not sufficient. Because Predicate has and, or, and negate methods to create a new predicate from existing ones. For Function, there are compose and andThen methods. We need a mechanism, where appropriate implementation are provided for those composed predicates and functions automatically and behind the scenes.

Check com.github.dakusui.valid8j.pcond.core.printable package for more detail.

Java 8 for Development

valid8j is intended to be a common library, which can be used by any projects. It is built and tested using Java 8, which is the most compatible Java as of April in 2024.

Index

References