Skip to content

Instantly share code, notes, and snippets.

@denkspuren
Last active February 20, 2023 20:49
Show Gist options
  • Save denkspuren/c379cd6d4512144e595d1dab98bba5ff to your computer and use it in GitHub Desktop.
Save denkspuren/c379cd6d4512144e595d1dab98bba5ff to your computer and use it in GitHub Desktop.
A minimalistic testing framework for use with the assert statement
/**
A minimalistic testing framework for use with the assert statement
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Author: https://github.com/denkspuren/, Version 1.7, CC BY-NC-SA
The following methods improve the way to formulate tests with assert statements.
This very tiny framework is primarily intended for programmers who use Java's
JShell for smaller self-contained programs and would like to include and write
tests in the simplest possible way using assert statements. The tests included
also serve as a demonstration of that kind of coding and testing practice.
The idea of testing with assert statements is elaborated on and demonstrated in
the other file that comes with the gist this file is in:
https://gist.github.com/denkspuren/c379cd6d4512144e595d1dab98bba5ff
If you prefer a more concise format, use assertTrue/assertFalse for testing.
How to use this framework?
- Download this code. Name the file `testingFramework.java`
- Use `/open testingFramework.java` in your JShell file
To automatically load this framework, you might run it as a startup script,
see https://docs.oracle.com/en/java/javase/19/jshell/scripts.html
*/
// Issue a warning if assertions are not enabled
AssertionError ae;
try { assert false; } catch (AssertionError exception) { ae = exception; }
if (ae == null) System.out.println("WARNING: Turn assertions on for testing puposes, run \"jshell -R-ea\"");
// beTrue: Value assignments and calls to void methods always return true
boolean beTrue(Runnable... methods) { for (Runnable method : methods) method.run(); return true; }
boolean beTrue(Object... values) { return true; }
// isCaught: If expected exception is caught return true, false otherwise.
boolean isCaught(Runnable method, Class... exceptionClasses) {
try {
beTrue(method);
return false;
} catch (Throwable e) {
for (Class exceptionClass : exceptionClasses)
if (exceptionClass.isInstance(e)) return true;
return false;
}
}
// sameRepr: Does given string equal to all object representations in rest?
boolean sameRepr(String first, Object... rest) {
String firstString = String.valueOf(first);
return Arrays.stream(rest).allMatch(o -> firstString.equals(String.valueOf(o)));
}
// matchRegex: Does given regex match to all object representations in rest?
boolean matchRegex(String regex, Object... rest) {
return Arrays.stream(rest).allMatch(o -> Pattern.matches(regex, String.valueOf(o)));
}
// assertTrue and assertFalse make your tests even more convenient, so use them
void assertBool(Boolean criteria, String intent, Object... values) {
for (Object value : values)
if (value instanceof Boolean && !value.equals(criteria))
throw new AssertionError(intent);
}
void assertTrue(String intent, Object... values) { assertBool(Boolean.TRUE, intent, values); }
void assertTrue(Object... values) { assertTrue("thrown by assertTrue", values); }
void assertFalse(String intent, Object... values) { assertBool(Boolean.FALSE, intent, values); }
void assertFalse(Object... values) { assertFalse("thrown by assertFalse", values); }
// Framework tests
{ // create own scope in order to not pollute scope of framework user
class Test {
int i, j;
void method() { }
void method(int i) { }
public String toString() { return "Test"; }
}
Test t = new Test();
String intent;
intent = "Test different use cases for beTrue";
assert beTrue() && beTrue(t.i = 3) && beTrue(t.i = 3, t.j = 4) &&
beTrue(t::method) && beTrue(t::method, t::method) &&
beTrue(() -> t.method(3)) : intent;
intent = "Test different use cases for isCaught";
assert !isCaught(() -> { assert true; }, AssertionError.class) &&
isCaught(() -> { assert false; }, AssertionError.class) &&
!isCaught(() -> { throw new UnsupportedOperationException(); }, AssertionError.class) &&
!isCaught(() -> { throw new AssertionError(); }, UnsupportedOperationException.class) &&
isCaught(() -> { assert false; }, UnsupportedOperationException.class, AssertionError.class) &&
isCaught(() -> { assert false; }, Error.class) : intent;
intent = "Test different use cases for sameRepr";
assert sameRepr("Test", new Test()) && sameRepr("Test", new Test(), new Test()) &&
sameRepr("null", (Test)null) && sameRepr(null, (Object)null) &&
sameRepr("7", 4 + 3) && sameRepr("7", 7, 0b111, 07, 0x7) : intent;
intent = "Test different use cases for matchRegex";
assert matchRegex("Test", new Test()) && matchRegex("[A-Z][a-z]{3}", new Test()) &&
matchRegex(Pattern.quote("Test"), new Test(), new Test()) &&
matchRegex("null", (Test)null) && matchRegex("7", 7, 4 + 3) : intent;
assertTrue("Test assertTrue",
true, 7 == 4 + 3, "Hello".equals("Hello"),
beTrue(() -> assertTrue(true)), beTrue(() -> assertTrue(true, true)),
isCaught(() -> { assertTrue(false); }, AssertionError.class),
isCaught(() -> { assertTrue(true, false); }, AssertionError.class),
isCaught(() -> { assertTrue("Test", false); }, AssertionError.class),
isCaught(() -> { assertTrue("Test", true, false); }, AssertionError.class));
assertFalse("Test assertFalse",
false, 7 == 4 + 4, "Hello".equals("Hallo"),
!beTrue(() -> assertFalse(false)), !beTrue(() -> assertFalse(false, false)),
!isCaught(() -> { assertFalse(true); }, AssertionError.class),
!isCaught(() -> { assertFalse(false, true); }, AssertionError.class),
!isCaught(() -> { assertFalse("Test", true); }, AssertionError.class),
!isCaught(() -> { assertFalse("Test", false, true); }, AssertionError.class));
}
/**
EFFECTIVELY TEST WITH ASSERT STATEMENTS
=======================================
Author: https://github.com/denkspuren/, Version 1.7, CC BY-NC-SA
This demonstration is primarily intended for programmers who use Java's
JShell for smaller self-contained programs and would like to include and write
tests in the simplest possible way using assert statements. This file
serves as a demonstration of that kind of coding and testing practice using
the helper methods delivered with `testingFramework.java`.
*/
/open testingFramework.java
/*
Example Code to Test
~~~~~~~~~~~~~~~~~~~~
Let's introduce an example in order to demonstrate testing with the framework.
Class `Countdown` counts from an initial value down to zero, tick by tick.
*/
class Countdown {
private int counter, init;
Countdown(int init) { assert init != 0; reset(init); }
void tick() { if (counter != 0) counter += counter > 0 ? -1 : +1; }
int getCount() { return counter; }
boolean isZero() { return counter == 0; }
void reset() { reset(init); }
void reset(int init) { counter = (this.init = init); }
@Override public String toString() { return super.toString() + "(" + counter + ")"; }
}
/*
The Problem
~~~~~~~~~~~
Testing code the traditional way with assert statements is a bit annoying.
You run a szenario and test state changes with asserts in between statements.
You might annotate the intent of the scenario with a comment,
but if anything goes wrong, you don't know the scenario that failed the test.
Here are two scenarios testing class `Countdown`. The testing code requires
quite some space.
*/
// Intent: Controlled countdown from 1 to 0; trying to go beyond 0
Countdown c = new Countdown(1);
assert c.getCount() == 1;
assert !c.isZero();
c.tick();
assert c.isZero();
c.tick();
assert c.isZero();
// Intent: Controlled countdown from -2 to 0; trying to go beyond 0
c = new Countdown(-2);
assert c.getCount() == -2;
assert !c.isZero();
c.tick();
c.tick();
assert c.isZero();
c.tick();
assert c.isZero();
/*
The Idea
~~~~~~~~
The idea of the testing framework is to chain method calls and checks in a
boolean expression. This way, an assert statement embraces a whole scenario
instead of a just checking a single step in a sequence of statements as seen
above.
Chaining calls and checks requires to handle a side effect in such a way that
a `true` value is produced. This is the purpose of the framework's `beTrue` method
and of the `isCaught` method for exceptions.
*/
// Demonstration of scenario testing with asserts, `beTrue` and `isCaught`
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Countdown c;
String intent;
intent = "Controlled countdown from 1 to 0; trying to go beyond 0";
assert beTrue(c = new Countdown(1)) && c.getCount() == 1 && !c.isZero() &&
beTrue(c::tick) && c.isZero() && beTrue(c::tick) && c.isZero() : intent;
intent = "Controlled countdown from -2 to 0; trying to go beyond 0";
assert beTrue(c = new Countdown(-2)) && c.getCount() == -2 && !c.isZero() &&
beTrue(c::tick, c::tick) && c.isZero() && beTrue(c::tick) && c.isZero() : intent;
intent = "Countdown reset after single tick";
assert beTrue(c = new Countdown(2)) &&
beTrue(c::tick, c::reset) && c.getCount() == 2 : intent;
intent = "Countdown reset to 7 after single tick";
assert beTrue(c = new Countdown(2)) &&
beTrue(c::tick, () -> c.reset(7)) && c.getCount() == 7 : intent;
intent = "Countdown init with zero violates contract";
assert isCaught(() -> new Countdown(0), AssertionError.class) : intent;
intent = "Check that the representation of Countdown is correct";
assert beTrue(c = new Countdown(7)) && matchRegex(".*Countdown@\\p{XDigit}+\\(7\\)", c) &&
beTrue(c::tick) && matchRegex(".*Countdown@\\p{XDigit}+\\(6\\)", c) : intent;
// That's quite neat, isn't it? If you prefer to be even more concise, use assertTrue!
// Here are the test cases reformulated with assertTrue:
assertTrue("Controlled countdown from 1 to 0; trying to go beyond 0",
c = new Countdown(1), c.getCount() == 1, !c.isZero(),
beTrue(c::tick), c.isZero(), beTrue(c::tick), c.isZero());
assertTrue("Controlled countdown from -2 to 0; trying to go beyond 0",
c = new Countdown(-2), c.getCount() == -2, !c.isZero(),
beTrue(c::tick, c::tick), c.isZero(), beTrue(c::tick), c.isZero());
assertTrue("Countdown reset after single tick",
c = new Countdown(2), beTrue(c::tick, c::reset), c.getCount() == 2);
assertTrue("Countdown reset to 7 after single tick",
c = new Countdown(2),
beTrue(c::tick, () -> c.reset(7)), c.getCount() == 7);
assertTrue("Countdown init with zero violates contract",
isCaught(() -> new Countdown(0), AssertionError.class));
assertTrue("Check that the representation of Countdown is correct",
c = new Countdown(7), matchRegex(".*Countdown@\\p{XDigit}+\\(7\\)", c),
beTrue(c::tick), matchRegex(".*Countdown@\\p{XDigit}+\\(6\\)", c));
/*
Remark regarding Countdown's toString() method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The `toString` method in Countdown reuses via `super.toString()` the
default representation of a class instance. By intention, the JShell
doesn't show you the full length of a returned default representation.
Here is a simple example to demonstrate the behavior:
jshell> class Example {}
| Erstellt Klasse Example
jshell> new Example()
$16 ==> Example@439f5b3d
jshell> System.out.println($16)
REPL.$JShell$25$Example@439f5b3d
If you use `matchRepr` you most likely prefer to ignore the prefix:
jshell> matchRegex(".*Example@\\p{XDigit}*", $16)
$18 ==> true
jshell> matchRegex("Example@\\p{XDigit}*", $16)
$19 ==> false
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment