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.
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.
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.
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.
References
-
[1] Wikipedia article on Design by Contract, Design by contract
-
[2] valid4j valid4j.org
-
[3] pcond dakusui.github.io/pcond
-
[4] Valid4j, valid4j.org
-
[5] PreconditionsExplained, PreconditionsExplained
-
[6] Hamcrest hamcrest.org
-
[7] Programming With Assertions Programming With Assertions
-
[8] Preconditions, Google Guava Preconditions class
-
[9] Validates, Apache Commons Validate class
-
[10] AssertJ https://assertj.github.io/doc/
-
[11] truth, Google Truth https://github.com/google/truth