pcond
: The Printable {pre,post}-conditionsSee: Description
Package | Description |
---|---|
com.github.dakusui.pcond.core |
A package to provide the core part of the
pcond 's functionalities. |
com.github.dakusui.pcond.core.fluent | |
com.github.dakusui.pcond.core.fluent.builtins | |
com.github.dakusui.pcond.core.identifieable |
A package that provides a mechanism to make "identifieable" functions and predicates with at least one parameter for their creations.
|
com.github.dakusui.pcond.core.printable |
A package that provides a mechanism to make functions and predicates (lambdas) printable.
|
com.github.dakusui.pcond.core.refl |
Offers a mechanism to invoke a method dynamically.
|
com.github.dakusui.pcond.experimentals.currying |
The
currying package provides a mechanism to "curry" a multi-parameters function. |
com.github.dakusui.pcond.experimentals.currying.context |
A package to provide the "Context" mechanism of the
pcond library. |
com.github.dakusui.pcond.experimentals.currying.multi |
A package to handle "multi-parameters" function.
|
com.github.dakusui.pcond.experimentals.cursor | |
com.github.dakusui.pcond.fluent |
A package to provide entry-point classes for the
Fluent style. |
com.github.dakusui.pcond.forms |
This package stores entry-point classes to
static import for your conveniences. |
com.github.dakusui.pcond.internals |
A package to place utility classes internally used by the
pcond library. |
com.github.dakusui.pcond.validator |
Table of Contents
|
com.github.dakusui.pcond.validator.exceptions |
A package to store exception classes thrown when a condition passed to assertion/validation method of
pcond is not satisfied. |
pcond
: The Printable {pre,post}-conditionsThe pcond
is a multi-purposes predicate library for "Design by Contract" style programming and test assertions.
You can replace Guava’s Preconditions
class, Apache Common’s Validate
class, and test assertion libraries with this.
Following is a list of goals that the pcond
library tries to achieve.
Unified Support for Value Check, DbC, Test Assertion programming: Input/status value checking, user input validation, Checks in Design by Contract, and test assertions have similar concerns, however, completely different solutions are provided [1].
The pcond
tries to offer a unified solution to those use cases.
More Programmer-Friendly Readability and Writability: Test assertion libraries have more and more focused on writability of code from readability of code and output.
The pcond
makes one step further by addressing remaining pain points.
Easy to handle custom types: Test assertion libraries require users to implement custom value verifier (Matcher
in Hamcrest, Assert
in AssertJ, Subject
in Google Truth) classes.
This is a significant work by itself, and it may also impede refactorings of the software under test.
Remove repeating the same fact twice in the failure message and the predicate: More or less similar to Google Guava’s Preconditions
class, a human needs to describe the condition to be checked twice.
One is in a natural language to print what condition is violated.
And the other is in Java programming language to define what check should be done for a given value.
This is a practice that violates "D-R-Y" (Don’t repeat yourself) principle.
This is more important for programmers than printing a nice message on a failure.
Remove fail→fix→run loop: In the earliest age of JUnit, there used not to be a good way to verify multiple conditions in one test method.
Programmers needed to call assert
, assertEquals
, etc. multiple times from inside one test method.
This was causing a situation, where they need to repeat fail→fix→run→fail→fix→run… loop, once an assert method call fails.
Ensuring a good way to write a test method with only a single assertion method call is important.
Not having to go back-and-force between source code and output: When a check fails, making a full description of the failure available is important. Otherwise, you will need to go back-and-forth between the code and the log (or test report).
Human-analyzable output: A readable message is essential to understand what happened when a check fails.
However, the pcond
interprets it in a slightly different way from other existing assertion libraries.
It focuses more on making a failure report self-sufficient and structured.
Rather than making it look "natural" as if it were written by a human, it focuses on make it readable for programmers.
Not for general English-speaking human.
Make custom verifiers composable: Instead of let a user define a custom verifier for every type, pcond
offers a way to compose a verifier for a user type from already provided functions and predicates.
Configurable: There are customization points for a general purpose library like pcond
.
For instance a failure report format, an exception type for a combination of a certain context and a certain use case, A message template for a failure, etc.
Providing a good way to customize these configurations at runtime is important.
To achieve the goals, the pcond
library equips the following mechanisms.
Predicates and function for value checking: Instead of custom verifier classes like Matcher
, Assert
, or Subject
, use conventional`Function` and Predicate
so that it can be used not only for test assertions but also for DbC, general value checking, etc.
Transform and Check mechanism: Decompose a verification process into two steps: "Transform" function and "Check" predicate.
Printable
mechanism: Make functions and predicates "printable".
Identifiable
mechanism: It’s preferable to be able to "identify" two predicates are equal, if they are meant to be the same.
Context
and Currying
mechanism: Sometimes we need to handle functions and predicates that have more than one parameter.
The Context
and Currying
are designed for such functions.
"Fluent" style support mechanism: Nowadays, "fluent" style is more and more preferred in test assertions.
pcond
also supports the style.
Configurable ValueChecker
mechanism: To use the library with various testing frameworks, pcond
has a configurable mechanism called ValueChecker
.
You can configure it for your own testing framework.
It supports JUnit4 and JUnit5 out-of-box.
The most fundamental idea of the pcond
library is to use Predicate
s, not special classes like Matcher
s (Hamcrest), AbstractAssert
(AssertJ), or Subject
(Google Truth).
A basis of the design of the pcond
library is an observation that we almost always can convert a given value into a type, which is convenient for verification, and then we can do the final verification.
For instance, if we have a custom typed value, we can convert it to a string, for which we can think of a lot of ways to verify if the value matches our expectation and to report how it didn’t match the expectation.
In the step one, we transform a value into a type for which we have a method to verify it. Then we will check it with the intended method.
"Convenient" types for verification pcond
chose are Object
, String
, List
, and numbers (int
, long
, double
, …), as of now.
This entire step \$q\$ can be considered one Predicate
for a value of type T
.
\$p\$ is a predicate for verification of a convenient type value.
\$f\$ is a function to transform a given value to a convenient type.
For \$p\$, we only need to support a limited number of predicates because, programmers will always convert a given value into some handy types.
So, we don’t need to come up with a nicely readable messages for every method you define in your custom matchers.
Instead, the pcond
library composes it from the transformation function \$f\$ and \$p\$ 's string representations.
Following is a small example code, which examines if a given value yourName
satisfies required conditions.
public class ExampleDbC {
public static void main(String[] args) {
System.out.println(hello("JohnDoe"));
}
public static String hello(String yourName) {
// (2)
requireArgument(yourName, and(isNotNull(),
transform(length()).check(gt(0)),
containsString(" ")));
String ret = String.format("Hello, %s", NameUtils.firstNameOf(yourName));
// (3)
return ensureNonNull(ret);
}
}
This prints the following output.
Exception in thread "main" java.lang.IllegalArgumentException: value:<"JohnDoe"> violated precondition:value (isNotNull&&length >[0]&&containsString[" "]) "JohnDoe"->&& ->false isNotNull ->true transform length ->7 7 -> check >[0] ->true "JohnDoe"-> containsString[" "]->false
It might not be 100% natural English text, but still very easily understandable for programmers. The author of the library believes it is more useful and reliable for developers.
Printable
mechanismTo implement library like above, it’s necessary to format a predicate into a human-understandable format.
Unfortunately, it is not sufficient and not straight forward to override the toString
method.
Because:
In the "transform and check" style requires Function
s, not only Predicate
s.
It is not possible to override toString
method in an interface.
Predicate
and Function
interfaces have a few methods that return a newly created Predicate
and Function
(Predicate#and
, Function#compose
, for instances ).
These returned objects also need to have overridden version of toString
.
Overriding toString
is a cumbersome manual task.
The pcond
library provides a solution to these problems by offering its own base classes for Predicate
and Function
.
The implementation of this feature is provided by the com.github.dakusui.pcond.core.printable
package.
Identifiable
mechanismThe pcond
has a mechanism to create a "parameterized" predicate, such as Predicates.containsString(String)
.
If you call this method twice, two different predicate objects are returned.
However, should those return make Objects.equals(Object,Object)
return false
?
class Example{
public static void main(String... args) {
Predicate<String> p1 = Predicates.containsString("hello");
Predicate<String> p2 = Predicates.containsString("hello");
System.out.println(p1.equals(p2));
}
}
pcond
is designed to return true
for this check.
The implementation of this feature is provided by the com.github.dakusui.pcond.core.identifieable
package.
The package com.github.dakusui.pcond
holds entry point classes of the pcond
library.
The other usage of the pcond
library is an assertion library.
It supports two styles.
One is traditional "Hamcrest" like style and the other is more recently fashioned "fluent" style like AssertJ or Google Truth.
Hamcrest [2] is the first popular assertion library.
The style JUnit itself presented at the time Hamcrest was published is to call assertEquals
, assertTrue
, assertFalse
methods.
Those methods fail if the given value do not satisfy the desired condition.
Also, they print human-readable message about what happened. That is, what the given value was and what was expected.
This approach leads to an explosion of the number of assertXyz
methods because we need to verify values with a lot of different expectations and, for each of them, this approach requires one assertXyz
method.
Hamcrest separated an assertion into two parts, one of which controls a value checking flow and the other is the part that defines a condition to be satisfied.
Following is an example found in Hamcrest’s tutorial[3]:
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
public class BiscuitTest {
@Test
public void testEquals() {
Biscuit theBiscuit = new Biscuit("Ginger");
Biscuit myBiscuit = new Biscuit("Ginger");
assertThat(theBiscuit, equalTo(myBiscuit)); // The Line
}
}
The object returned by a static method Matchers.equalTo
is a Matcher
object as other static methods in the class do.
The example verifies if theBuiscuit
is equalTo
myBiscuit
as it says.
Suppose if myBiscuit
is Sugar
and this test fails, the following message will be printed:
java.lang.AssertionError: Expected: <Sugar> but: was <Ginger> Expected :<Sugar> Actual :<Ginger>
If we want to test a different expectation, for instance, suppose we want to check if the value is not equal when a different object is given to be compared. We can modify the test as follows at The Line:
assertThat(theBiscuit, not(equalTo(myBiscuit))); // The Line
Thus, with Hamcrest, you can construct various conditions from (relatively) limited number of Matcher
classes.
Now you can write a human-readable test which prints a human-readable failure report.
However, there are still two remaining pain points:
To test your own class, you will need to implement a custom matcher class for better readability. This is not a straight forward task.
Hamcrest was designed and published at the age where Java 8 did not exist, which introduced lambda and Predicate
.
Neither using a matcher as a predicate nor the other way around is not straight forward, although it will be convenient if it is possible.
The approach pcond
took is as follows.
Introduce the "Transform-and-check" concept to uniform the check. This will allow us to support our own class just by writing a printable function to convert the object to already fixed types.
Use Java’s out-of-box Predicate
and Function
for that.
Following is the simplest example of pcond
style test.
public class UTExample {
@Test
public void shouldPass_testFirstNameOf() {
String firstName = NameUtils.firstNameOf("Yoshihiko Naito");
assertThat(firstName, allOf(not(containsString(" ")), startsWith("Y")));
}
}
and
, not
, containsString
, and startsWith
are just predicates of Java.
If you want to do a custom check, you can write your own predicate, as usual programming.
If you watn to check your custom class, you can write your own function, which converts your custom value to well-known types such as String
, Number
, Boolean
, List
of them, etc., as usual programming.
If the NameUtils.firstNameOf
returns an empty string, it will print the following error message.
org.junit.ComparisonFailure: Value:"" violated: (!containsString[" "]&&startsWith["R"]) ""->&& ->true |""->&& ->false ! ->true | ! ->true containsString[" "]->false| containsString[" "]->false X startsWith["Y"] ->true | startsWith["Y"] ->false
For an equivalent test, what Hamcrest prints as an error report is:
java.lang.AssertionError: Expected: (not a string containing " " and a string starting with "R") but: a string starting with "R" was ""
As you see, pcond
gives more informative report.
It shows each predicate’s expected actual predicate one by one and with a modern IDE, those will be shown side-by-side.
You will notice the only last predicate startsWith["Y"]
was not satisfied by the value ""
and that’s why the test failed.
While you will need to analyze which part of the Expected
was not satisfied by the input value ""
and how by yourself from the Hamcrest’s report.
The next challenge assertion libraries faced was the explosion of the static methods to be imported. There is a bunch of static methods to be imported and classes to which they belong. Hamcrest itself has twenty-four matcher classes, each of for which entry point class is necessary. On top of that, there is a bunch of third party libraries.
What the author of AssertJ or Google Truth thought is to let programmers create a builder object first by a static method and then from the object, let programmers choose the next method to call using "fluent" style.
Following is the example for the usage of AssertJ based testing code:
class AssertJExample {
public void assertJexample() {
// AsssertJ example from:
// - https://assertj.github.io/doc/#overview-what-is-assertj
// in the examples below fellowshipOfTheRing is a List<TolkienCharacter>
assertThat(fellowshipOfTheRing).hasSize(9)
.contains(frodo, sam)
.doesNotContain(sauron);
}
}
Major drawback of this approach is.:
No clean way to verify multiple values.
Still users need to write their own assertion builder class (Assert
in AssertJ, Subject
in Google Truth)
Each builder class will need to have a number of methods. This is because a builder just can "add" a simple check by one method. No way to create a new one from existing ones.
Adding an explanation only to the first point as the other two are more or less obvious.
When you need to do assertions for multiple values in AssertJ
, a normal way to achieve it is following:
public static class AssertJMultiValueExample {
public void assertjMultiValueExample() {
// https://stackoverflow.com/questions/47397525/multiply-conditions-set-in-assertj-assertions
SoftAssertions phoneBundle = new SoftAssertions();
phoneBundle.assertThat("a").as("Phone 1").isEqualTo("a");
phoneBundle.assertThat("b").as("Service bundle").endsWith("c");
phoneBundle.assertAll();
}
}
This is a bit verbose, and it will silently PASS, if you forget calling assertAll
in the end.
An example for the pcond
's fluent style support looks like the following:
public class MoreFluentExample {
@Test
public void checkTwoValues() {
String s = "HI";
List<String> strings = asList("HELLO", "WORLD");
assertAll(
valueOf(s).asString()
.exercise(TestUtils.stringToLowerCase())
.then()
.isEqualTo("HI"),
valueOf(strings).asListOf((String)value())
.then()
.findElementsInOrder("hello", "world"));
}
}
This leads to the following report.:
"HI" ->WHEN:treatAsString ->"HI" stringToLowerCase ->"hi" X"hi" ->THEN:isEqualTo["HI"] ->false ["HELLO","WORLD"]->WHEN:treatAsList ->["HELLO","WORLD"] : : [] -> (end) ->true
Thus, we can keep both the code and report human-readable.
The pcond
can be used as a library for Design by Contract style programming in Java.
There is a couple of ways for this usage.
assert
statementWith this style, you can benefit the good old feature of Java: assert
.
public class Example {
public void example(String arg) {
assert precondition(arg, isNotNull().and(not(isEmpty())));
System.out.println("Hello, " + arg + "!");
}
}
You will see a human-readable and analyzable output, when an assertion fails.
At the same time, once the assertion is disabled by the VM option -da
, you will see no performance penalty for the feature.
Aside from the precondition
method, that
method is prepared for checking an invariant condition and postcondition
method is prepared for what the name suggests.
requireXyz
and ensureXyz
methodsThat said, you may want to force your code to conduct a check for input value.
public class Example {
public static String hello(String yourName) {
// (2)
requireArgument(yourName, and(isNotNull(), transform(length()).check(gt(0)), containsString(" ")));
String ret = String.format("Hello, %s", NameUtils.firstNameOf(yourName));
// (3)
return ensureNonNull(ret);
}
}
Valid4J valid4j: 2015
Hamcrest, Matchers that can be combined to create flexible expressions of intent Hamcrest: 2019
Hamcrest, Hamcrest Tutorial Hamcrest Tutorial: 2019
AssertJ, Fluent assertions for java AssertJ: 2022
Truth - Fluent assertions for Java and Android Truth Truth: 2022
Enjoy.
pcond
library. It offers a style that unifies test assertions and DbC programming based on the Hamcrest library.
Copyright © 2024. All rights reserved.