Typed Assertions Tell You What Hurts

One thing clojure.test did reasonably well was tell you why an assertion failed. Currently, Lazytest fails in this regard.

The problem with requiring test functions to return true/false to indicate pass/fail is that they can’t attach any additional information to a failure to explain why it failed.

I realized that function return values are insufficient for describing failure conditions. Fortunately, we’ve long had another means for functions to signal failure: typed exceptions.

Typed exceptions seem to be out of favor at the moment. Clojure itself only uses a handful of generic exception types, and defines none of its own.

It’s slightly awkward to define new exceptions in Clojure because the JVM requires any thrown exception to be derived from the concrete base class java.lang.Throwable.

Sure, you could use gen-class, but generating a stub class that maps to a Clojure namespace seems like overkill for such a simple task. All I need is something that I can throw with an arbitrary payload attached.

So I did that thing that will make everyone cringe: I wrote it in Java. All nine lines of it:

package lazytest;

public class ExpectationFailed extends Error {
    public final Object reason;

    public ExpectationFailed(Object reason) {
	this.reason = reason;
    }
}

Now I can define any number of typed objects representing different failure conditions, and I still only have to worry about catching one exception type.

Next I can write functions that test for different conditions and throw ExpectationFailed when they are not met, attaching the appropriate failure object. I can even write a macro, expect, that transforms an ordinary predicate expression into an “expectation expression” by reflecting on the code.

The expect macro fills the same role in Lazytest as the is macro in clojure.test.

Now I just need to figure out how to merge all this back in to the master branch.

7 Replies to “Typed Assertions Tell You What Hurts”

  1. Meh. Typed exceptions are not the only way. Java needs them because they can’t easily represent different shapes of exception data without different types, and they built their exception ‘switch’ mechanism around types. It’s left people thinking exceptions are some strange sort of thing, each value of which needs its own type. That’s not true of most other things. Why aren’t the reasons (exception reports) just (variously shaped) map values? You could then use distinguished fields for more sophisticated handling, like by :severity, :category (or, yes, :class) etc. If you use types you need to force these ideas into a hierarchy.

    Every exception gets its own type needs to stop. Now. Please. It completely bloats the system with junk types, and engenders switch-on-type logic.

    Thanks,

    Rich

  2. How about the java.lang.AssertionError? You could either throw that or extend it, and achieve “eventual compatibility” with java test/runners.
    I imagine, eventually i would be able to run the clojure tests together with java tests in an IDE (e.g. eclipse) and see the same style of error reporting for both (the green bar with stack and error info).

  3. Phil- No, didn’t know about that. Will investigate.

    Rich- I’m only defining one Exception type, so that I can add a public Object field. The actual “reasons” are just defrecords, which is convenient because I can extend reporting methods on them.

    tod- java.lang.AssertionError only has a String field where I want an Object. I could extend it, however.

  4. Rich- you’re right, defining record types for every predicate is too limiting. I’ve reverted to using simple maps, as you suggested.

Comments are closed.