Implementation of Hamcrest for Common Lisp

HAMCREST ASDF System Details

Installation

You can install this library from Quicklisp, but you want to receive updates quickly, then install it from Ultralisp.org:

(ql-dist:install-dist "http://dist.ultralisp.org/"
                      :prompt nil)
(ql:quickload :hamcrest)

Introduction

This is an implementation of Hamcrest for Common Lisp.

It simplifes unittests and make them more readable. Hamcrest uses idea of pattern-matching, to construct matchers from different pieces and to apply them to the data.

Here is a simple example

(assert-that
  log-item
  (has-plist-entries :|@message| "Some"
                     :|@timestamp| _)
  (hasnt-plist-keys :|@fields|))

Why not pattern-matching library?

You may ask: "Why dont use a pattern-matching library, like Optima?"

Here is another example from another library log4cl-json, where I want to check that some fields in plist have special values and other key is not present. Here is the data:

(defvar log-item '(:|@message| "Some"
                   :|@timestamp| 122434342
                   ;; this field is wrong and
                   ;; shouldn't be here
                   :|@fields| nil))

With Optima I could write this code to match the data:

(ok (ematch
      log-item
    ((and (guard (property :|@message| m)
                 (equal m "Some"))
          (property :|@timestamp| _)
          (not (property :|@fields| _)))
     t))
  "Log entry has message, timestamp, but not fields")

But error message will be quite cumbersome:

× Aborted due to an error in subtest "Simple match"
  Raised an error Can't match ((:|@fields| NIL :|@timestamp|
                                "2017-01-03T16:42:00.991444Z" :|@message|
                                "Some")) with ((COMMON-LISP:AND
                                                (GUARD
                                                 (PROPERTY :|@message| M)
                                                 (EQUAL M "Some"))
                                                (PROPERTY :|@timestamp|
                                                 _)
                                                (NOT
                                                 (PROPERTY :|@fields|
                                                  _)))). (expected: :NON-ERROR)

CL-HAMCREST is more concise and clear

With cl-hamcrest test becomes more readable:

(assert-that
      log-item
      (has-plist-entries :|@message| "Some"
                         :|@timestamp| _)
      (hasnt-plist-keys :|@fields|))

As well, as output about the failure:

× Key :|@fields| is present in object, but shouldn't

That is because cl-hamcrest tracks the context and works together with testing framework, to output all information to let you understand where the problem is.

Why not just use Prove's assertions?

To draw a full picture, here is test, written in plain Prove's assertions:

(ok (member :|@message| log-item))
(is (getf log-item :|@message|)
    "Some")
(ok (member :|@timestamp| log-item))
(ok (not (member :|@fields| log-item)))

And it's output:

✓ (:|@message| "Some") is expected to be T 
✓ "Some" is expected to be "Some" 
✓ (:|@timestamp| "2017-01-03T16:57:17.988810Z" :|@message| "Some") is expected to be T 
× NIL is expected to be T 

is not as clear, if you'll try to figure out what does NIL is expected to be T mean.

Description of all supported matchers, you can find in the Matchers library section.

Matchers library

Here you will find all matchers, supported by CL-HAMCREST, grouped by their purpose.

Object matchers

This kind of matchers checks some sort of properties on an object. In this case objects are not only the CLOS objects but also, hashmaps, alists and property lists.

Sequence matchers

Boolean matchers

Utility functions

Integration with Prove

CL-HAMCREST has integration with Prove.

TEST> (ql:quickload :hamcrest-prove)
TEST> (use-package :hamcrest.prove)
TEST> (let ((obj (make-hash-table)))
        (assert-that
         obj
         (has-hash-entries :foo :bar)))
  × Key :FOO is missing
T
TEST> (let ((obj (make-hash-table)))
        (setf (gethash :foo obj) :bar)

        (assert-that
         obj
         (has-hash-entries :foo :bar)))
  ✓ Has hash entries:
      :FOO = :BAR
T
TEST> (let ((obj (make-hash-table)))
        (setf (gethash :foo obj) :bar)

        (assert-that
         obj
         (has-hash-entries :foo :some-value)))
  × Key :FOO has :BAR value, but :SOME-VALUE was expected
T

This is the simple case, but nested objects can be checked too.

All available matchers are described in the hamcrest-docs/matchers:@matchers section.

Roadmap