thincrest-pcond is a test assertion library named after hamcrest[4] and it is at the same time a pun for "think the rest". It uses pcond, instead of Matcher used by hamcrest, for a mechanism to compose human-readable messages. The benefit of pcond over hamcrest is its compatibility and extensibility because they are not relying on custom classes like Matcher but on Java’s plain Predicate. It is why it is "thinner" than hamcrest. It also provides useful preset predicates from which you can build your own for more complex types and checks. So, you can reuse the predicates you created across different use cases such as value-checking[3], DbC contracts[1], or test assertions.

Getting Started

Have a following maven dependency in your pom.xml.

<dependency>
  <groupId>com.github.dakusui</groupId>
  <artifactId>thincrest-pcond</artifactId>
  <version>{thincrest-pcond-version}</version>
</dependency>

Visit oss.sonatype.org to figure out the most recent version of thincrest-pcond.

Programming with pcond twins

thincrest and valid8j are twins of pcond and both libraries for value checking, but intended to be used in respectively different contexts. thincrest is desinged to be used in test assertions, while valid8j should be used for Design by Contract style programming and general value checking in production code. In these contexts, how to build a condition to check a value and compose an appropriate message for its failure are common concern. However, there is no uniformed style that can be applied to all those. pcond and its twins are designed to address this challenge.

First Example

Following is the first example of the thincrest-pcond library.

ThincrestExample.java
public class ThincrestExample {
  @Test
  public void testString() {
    assertThat(
        "hello World",
        transform(function("toUpperCase", o -> Objects.toString(o).toUpperCase())) (1) (2)
            .check(containsString("HELLO"))); (3)
  }
}
1 Predicates.transform(Function<,>)
2 Printables.function(String, Function<,>)
3 Predicates.containsString(String)

For readability’s sake, it’s recommended to extract a function, where possible.

ThincrestExample.java
public class ThincrestExample {
  @Test
  public void testString() {
    assertThat(
        "hello World",
        transform(toUpperCase()).check(containsString("HELLO")));
  }

  private static Function<String, String> toUpperCase() {
    return function("toUpperCase", o -> Objects.toString(o).toUpperCase());
  }
}

The programming style of thincrest-pcond (or pcond in more general) is to first transform a given value into better known type or form, which is more suitable for examining its validity.

In this example, a given string is first transformed into all upper cases so that it is equal to "HELLO" if we ignore cases.

This results in a following output.

Table 1. Output of Example.java
Expectation Actual
    "Howdy, World"->transform:toUpperCase       ->"HOWDY, WORLD"
[0] "HOWDY, WORLD"->check:containsString[HELLO] ->true

.Detail of failure [0]
---
containsString[hello]
---
    "Howdy, World"->transform:toUpperCase       ->"HOWDY, WORLD"
[0] "HOWDY, WORLD"->check:containsString[HELLO] ->false

.Detail of failure [0]
---
HOWDY, WORLD
---

You can see the violated condition and how it is violated in the stacktrace.

A philosophy behind thincrest-pcond 's (and also valid8j-pcond 's) approach is:

  • A human in the end checks the value on a screen as a text.

  • Either way, test can only report a presence of a bug, cannot ensure absence of it. If so, rather than trying to build "matcher", "subject", or "assertion" objects that can do a check that detects all the bugs for a given class, it will be more productive to decompose the check into various checks, each of which is concise and understandable.

The style of thincrest, where you pass a value and a predicate that checks it to a library entry-point method, is the same for valid8j.

"Printable" Predicate

To verify a value with thincrest or valid8j, you can call a value checking method of them with your value and a predicate to be checked.

valueCheckingMethod(aValue, predicate);

Value checking methods are defined as public static methods in classes such as TestAssertions(thincrest-pcond) or Requires(valid8j-pcond). It is recommended to import them statically at the beginning of your class.

You can give your own lambda as a predicate to the method, however it will result in non-user friendly string such as following.

Exception in thread "main" java.lang.IllegalArgumentException: value:"JohnDoe" violated precondition:value java.util.function.Predicate$$Lambda$78/2047329716@46f7f36a

This is still better than nothing, however, probably it will be much better if you can see what a condition the value "JohnDoe" violated and how it did without visiting the source code. In order to make thincrest and valid8j print a better message on a failure, you need to give a "printable" predicate.

pcond, which powers thincrest and valid8j, offers a set of method to create a printable predicate in Predicates class. It also offers a way to construct a conjunction, disjunction, and negation of given printable predicates.

To create a non-printable function and predicate, you can use static methods in Printables class.

  • Printables.function(Supplier<String> formatter, Function<T, R> function)

  • Printables.function(String name, Function<T, R> function)

  • Printables.predicate(Supplier<String> formatter, Predicate<T> predicate)

  • Printables.predicate(String name, Predicate<T> predicate)

In the first example above, we already used one of them.:

public class PrintableFunctionsExample {
    public static void example() {
        Printables.function("toUpperCase", String::upperCase);
        Printables.predicate("longerThan10", s -> s.length() > 10);
    }
}

However, if we create a large number of printable functions and predicates one by one, the message you set for them will be not manageable and error-prone. Instead, we should think of a way to keep them manageable by reusing them. A way pcond offers is to construct a more complex predicate and functions from simpler ones.

Following is an example.

and(isNotNull(), transform(length()).check(gt(0)), containsString(" "))

// Don't try: v -> isNotNull().test(v) && transform(...).test(v) && containsString(" ").test(v)
// It will result in the cryptography-like lambda's toString() text.

isNotNull is a method that returns a predicate which returns true if a value given to test method is not null. And its toString method is overridden so that it returns a string isNotNull.

and is another method that returns a printable predicate. The method accepts predicates and constructs a new printable predicate that is a conjunction of them.

There is another method allOf. The difference is and aborts its evaluation at the first fail, while allOf keeps going until the end. For test assertions, where you need an entire picture of the execution, allOf will be more convenient. The relationship between or and anyOf is the same.
Predicate#and, Predicate#or, and Predicate#negate methods are overridden in printable functions appropriately, so you can use them either. But it may sometimes be cumbersome to give generic parameter types explicitly.

transform and check structure needs a bit more explanation than others.

transform(length()).check(gt(0))

This line constructs a printable predicate which returns true if a length of a given string is greater than (gt) 0.

This is a little artificial example to illustrate the usage of the transform and check structure. You can achieve the same thing with Predicates.isEmptyString() and it will be simpler and faster.

First the method transfom accepts, preferably a printable, function. In this case a function returned by length method, which is defined in Functions class, is passed to the method. Now it returns a factory for a printable predicate. The check method of the factory returns a new predicate that checks a transformed value by the first function. With this transform and check approach, you can build various printable predicates from preset ones.

Examining a Value: Value Checking Methods

pcond offers a couple of styles to build conditions, one is conventional, which we already used in the examples in the earlier sections. The other is "fluent" style, which will be covered in this section.

For each of them, thincrest and valid8j have different set of classes and methods.

Conventional Value Checking Methods of thincrest-pcond

In this category, there are only a couple of static methods. For both of them, the first parameter is a value to be checked and the second is a (printable) predicate used for the check.

assertThat (TestAssertions)

Use this for normal test assertions.

assumeThat (TestAssertions)

Use this for checking an assumption to execute a test is satisfied or not.

To add an explanation to assumeThat, suppose that you have a test only valid on Microsoft Windows platform. What should happen if it is run on a Linux-based platform? If it fails, it means your product doesn’t compile on Linux. So, you want it to be ignored. In this situation, you can write assumeThat(isRunningOnWindowsPlatorm()) and AssumptionViolatedException, which make JUnit4 mark the test ignored, will be thrown.

Fluent Value Checking Methods of thincrest-pcond

Methods end with Statement accepts only one Statement, while ones end with All accepts multiple statements. Basically, they are doing the same thing and just defined as "syntax-sugara" for readability’s sake.

assertStatement (TestAssertions)

Use this for normal test assertions.

assertAll (TestAssertions)

Use this for normal test assertions, where you need to check multiple values at once.

assumeStatement (TestAssertions)

Use this for checking an assumption to execute a test is satisfied or not.

assumeAll (TestAssertions)

Use this for checking an assumption to execute a test is satisfied or not in a situation, where you need to check multiple conditions at once.

For the detail of how to create a statement object, check Writing your Code Fluently.

Conventional Value Checking Methods of valid8j-pcond

Depending on the context, where you are going to examine a given value, an appropriate message and exception to be thrown on a failure can be different. pcond provides methods that offer such messages and exceptions. You can simply call methods with the value and the condition you are going to examine. Followings are methods for it and classes in which they are defined.

requireXyz (Preconditions)

Methods for checking "preconditions", which a user of your product needs to satisfy. On a failure, a RuntimeException such as NullPointerException etc. will be thrown depending on the actual method. Xyz can be NonNull, Argument, or State.

ensureXyz (Postconditions)

Methods for checking "postconditions" , which your product needs to satisfy for its user. These methods also throw RuntimeException s depending on an actual prefix Xyz . Xyz can be NonNull or State.

validate (Validations)

This is also used for checking "preconditions". However, unlike requireXyz methods, it throws an ApplicationException, which is a checked exception (not a RuntimeException). This method should be used in a situation where a "recoverable" failure happens.

assert xyz (Assertions)

This should be used for checking "internal" error, where you want to disable the check in production code. xyz can be precondition, postcondition, invariant and that. The usege of the first three is self-explanatory. that is used for the other purposes. In case you want to use assert for any other purposes than them, use that.

To check your user’s error, use requireXyz or validate. When there is a simple and easy way to check the condition before calling your function, use requireXyz. Otherwise, use validate to allow your user to handle the error. To check your own fault, use ensureXyz or assert xyz. If the check should be done even in the production, i.e. it can be broken by your user, use ensureXyz. Otherwise, use assert xyz. Because the check can only be broken by your own fault (bug), which should not exist in your production code.

Among all of those, assert xyz has a quite outstanding characteristic, where it can be completely disabled by -da option to your JVM and does not have any performance overhead at all if it is disabled.

Design by Contract Example
public class ExampleDbC {
    public void publicWithdraw(int amount) {
        requireArgument(amount, greaterThan(0));                         (1)
        privateWithdraw(amount);
        this.balance = updateDatabase(this.balance + amount);
        ensureState(this.balance, greaterThanOrEqualTo(amount));         (2)
    }

    private void privateWithdraw(int amount) {
        assert precondition(amount, isGreaterThanOrEqualTo(0));          (3)
        balance += amount;
        assert postcondition(this.balance, isGreaterThanOrEqualTo(0));   (4)
    }
}
1 Check incoming argument value. Since externally exposed method should protect itself by rejecting invalid value, use requireArgument(s).
2 If the result of updateDatabase is not consistent with any of known constraints, we should consider that something went wrong inside the method, and it should be reported to caller side. In general, inconsistency that can happen at runtime in production because of
3 It is a design of this class that publicWithdraw makes sure only valid value comes into inside. If so, the value for amount will not be 0 or less. This is a "belief" that can be checked by assert statement.
4 If this.balance becomes less than 0, we should think we are detecting an internal error. In production, this check is useless and just a performance over-head, because such bugs should be fixed before releasing. Such a check should be done with assert so that it can be turned of in production.

Fluent Value Checking Methods of valid8j-pcond

The matrix below shows "Fluent" versions of value checking methods of valid8j-pcond. They are defined in ValidationFluents class.

There are two axes to think of valid8j 's use cases. One is targets, for which a check is made. The other is context, what the check means.

There are four possible targets, which are arguments, states, general conditions, and general conditions in assert. About contexts, there are three possible items, which are precondition, invariant, and postcondition.

For each of these combinations, a couple of methods are defined, singular and plural, for the code readability’s sake.

Following is a matrix that describes the methods for fluent style in valid8j.

Argument State General Condition General Condition (assert)

precondition

  • requireArgument

  • requireArguments

  • requireState

  • requireStates

  • requireStatement

  • requireAll

  • precondition

  • preconditions

invariant

N/A

N/A

N/A

  • that

  • all

postcondition

N/A

  • ensureState

  • ensureStates

  • ensureStatement

  • ensureAll

  • postcondition

  • postconditions

The first three targets are intended to be used mainly in public methods to defend your program from invalid input values given externally. The last one (assert) is intended to be used mainly in private methods to detect bugs you created inside your product.

Reading the Output

Look at the first line of the output you see in a failure. There’s a message:

value:"JohnDoe" violated precondition:value ((isNotNull&&!isEmpty)&&containsString[" "])

This is describing the value that was examined and the condition violated by it.

The next several lines explain how a given value violated a condition to be satisfied.

&&                               -> false
  isNotNull("JohnDoe")           -> true
  transformAndCheck              -> true
    length("JohnDoe")            -> 7
    >[0](7)                      -> true
  containsString[" "]("JohnDoe") -> false

See the last line, this means the given string JohnDoe made a condition containsString[" "] false, and it made the entire check fail. pcond is designed to shortcut an evaluation as soon as a value of a disjunctive or conjunctive predicate becomes fixed. That is, if you have an or condition, and the first predicate under it becomes true, the rest will not be evaluated at all. So the last line in the message always shows the direct reason why the check failed.

Thus, you can read the output above as "The check failed because the value \"JohnDoe\" containsString[" "] was false.

Writing your Code Fluently

Following is an example that illustrates how to use "Fluent" API of thincrest-pcond to build a test.

public class FluentExample {
  public void assertSalute() {
    Salute salute = new Salute();
    assertStatement(                  (1)
        objectValue(salute)           (2)
            .invoke("inJapanese")     (3)
            .asString()               (4)
            .length()                 (5)
            .then()                   (6)
            .greaterThan(2));         (7)
  }
}
1 TestFluents.assertStatement(…​). This method takes one Statement as a parameter.
2 Statement.objectValue(…​) ObjectTransformer
3 invoke(String methodName, Object…​ args) is a method to invoke a method on an object given to objectValue. Usually you can specify a method that you want to test. This method returns ObjectTransformer since you cannot make any assumption on the type at compile-time.
4 You need to let the compiler know you want to use StringTransformer for the returned value, instead of ObjectTransformer.
5 This method calls String#length() method on the current object and returns IntegerTransformer.
6 Let the current transformer know that now you want to check the transformed value. This method returns a corresponding checker, in this case, it will be IntegerChecker.
7 Add a check if the current value is greater than 2 to the current checker. Since Checker is extending Statement, this compiles without calling a builder method to make it a Statement instance.

You can build a more complex assertion for your test using transform(…​) method.

public class FluentExample {
  public void assertAllSalutes() {
    Salute salute = new Salute();
    assertAll(                                  (1)
        objectValue(salute)
            .invoke("inJapanese")               (2)
            .asString()
            .length()
            .then()
            .greaterThan(2),
        objectValue(salute)
            .invoke("inEnglish")
            .asString()
            .transform(                         (3)
                v -> allOf(v.length().then()    (4)
                            .greaterThan(10)
                            .toPredicate(),     (5)
                           v.then()
                            .contains("Hello")
                            .toPredicate())));
  }
}
1 To assert multiple statements at once, use TestAssertions.assertAll(Statement…​ statements).
2 ObjectTransformer#invoke(String,Object…​):Object is a method that invokes a method of a given object.
3 transform(Function<XyzTransfomer<Xya>, Predicate<Xyz> clause)
4 Predicates.allOf(Predicate<> …​) can be used here to branch the checking procedure. In this case, we want to check if a salute in English is longer than ten characters, and it contains a word "Hello".
5 A checker has a method toPredicate(), which builds a printable predicate from the conditions that have been added to it.

In case of a failure, the test above will print a message like following:

Expected Actual
    ThincrestExample$Salute@6e3c1e69->WHEN:transform              ->true
                                    ->    <>.inJapanese()         ->"Kon-nichi-ha"
    "Kon-nichi-ha"                  ->    castTo[String]          ->"Kon-nichi-ha"
                                    ->    length                  ->12
    5                               ->  THEN:>[2]                 ->true
    ThincrestExample$Salute@1888ff2c->WHEN:transform              ->true
                                    ->    <>.inEnglish()          ->""
    ""                              ->    castTo[String]          ->""
                                    ->  THEN:allOf                ->true
                                    ->    transform:length        ->0
[0] 0                               ->    THEN:>[10]              ->true
[1] ""                              ->      containsString[Hello] ->true

.Detail of failure [0]
---
>[2]
---

.Detail of failure [1]
---
containsString[Hello]
---
    ThincrestExample$Salute@6e3c1e69->WHEN:transform              ->true
                                    ->    <>.inJapanese()         ->"Kon-nichi-ha"
    "Kon-nichi-ha"                  ->    castTo[String]          ->"Kon-nichi-ha"
                                    ->    length                  ->12
    5                               ->  THEN:>[2]                 ->true
    ThincrestExample$Salute@1888ff2c->WHEN:transform              ->false
                                    ->    <>.inEnglish()          ->""
    ""                              ->    castTo[String]          ->""
                                    ->  THEN:allOf                ->false
                                    ->    transform:length        ->0
[0] 0                               ->    THEN:>[10]              ->false  (1)
[1] ""                              ->      containsString[Hello] ->false  (2)

.Detail of failure [0]
---
0
---

.Detail of failure [1]
---

---
1 The detail is shown in "Detail of failure[0]"
2 The detail is shown in "Detail of failure[1]"

In this example, it seems Salute#inEnglish method contains a bug, where its return value becomes a string without length.

Using Together with thincrest and valid8j

When you use both thincrest-pcond and valid8j-pcond in one project, please be careful at package names of classes you are going to use. They both use pcond to render messages from predicates. But the classes from pcond are stored under com.github.dakusui.thincrest_pcond in thincrest-pcond. valid8j does the same. Classes from pcond are stored under com.github.dakusui.valid8j_pcond in valid8j-pcond. If you create a predicate using com.github.dakusui.valid8j_pcond.forms.Predicates, for instance, the thincrest 's mechanism to compose error messages from predicates cannot do its work. Because the predicates created by valid8j-pcond 's classes will look just normal Predicate, not ones that have mechanism to print it, the message will look much poorer than it can be.

The other way around will not happen (fortunately) because thincrest-pcond will be a dependency in test scope, and it will not be visible from the production scope, which valid8j-pcond is usually used.

Building the thincrest-pcond Library

How to Build the Project

Compile and Test

mvn clean compile test

Docs

mvn clean compile test site, you will see generated docs under target/site directory.

Build dependencies

  • Java SDK8 (openjdk-8-jdk)

  • mvn (maven)

  • gem (ruby)

  • xmllint (libxml2-utils)

  • git (git)

References