(defpackage #:lisp-test (:use #:common-lisp) (:export #:run-suite #:test-suite #:testcase #:run-all-suites #:clear-all-suites #:test-suites)) (in-package #:lisp-test) (defvar *unbound-slot* (copy-seq #(unbound))) (defun init () (defparameter *test-suites* (make-hash-table :test #'eq))) (defclass test-suite () ((tests :initform '()))) (defclass test () ((name :initarg :name :reader test-name) (form :initarg :form) (returns :initarg :returns :initform *unbound-slot*) (raises :initarg :raises :initform *unbound-slot*) (test :initarg :test :initform #'equal))) (defmethod run-test ((test-case test)) "True if TEST-CASES passes. False otherwise." (with-slots (form returns raises test) test-case (handler-case (funcall test (eval form) returns) (error (e) (if (eq raises *unbound-slot*) nil (typep e raises)))))) (defmethod add-test ((suite test-suite) (test test)) "Add TEST to SUITE." (with-slots (tests) suite (push test tests))) (defmethod run-suite ((suite test-suite)) "Run all tests in SUITE. Returns two lists: tests that passed, tests that failed" (with-slots (tests) suite (let ((passes '()) (fails '())) (dolist (test tests) (if (run-test test) (push (test-name test) passes) (push (test-name test) fails))) (values passes fails)))) (defmethod run-suite ((suite-name symbol)) "Run all tests in the suite named SUITE-NAME" (run-suite (gethash suite-name *test-suites*))) (defmacro make-test (name form &rest keys) "Create a new test object" `(make-instance 'test :name ',name :form ',form ,@keys)) (defmacro testcase (name form &rest keys) "Run a testcase." `(run-test (make-test ,name ,form ,@keys))) (defmacro test-suite (name &body test-cases) "Define a new test suite." `(progn (setf (gethash ,name *test-suites*) (make-instance 'test-suite)) (macrolet ((testcase (name form &rest keys) `(add-test (gethash ,',name *test-suites*) (make-test ,name ,form ,@keys)))) ,@test-cases))) (defun run-all-suites (&optional (stream *standard-output*)) "Run all test suites." (let ((passed 0) (failed 0)) (maphash #'(lambda (suite-name suite) (multiple-value-bind (passes fails) (run-suite suite) (when fails (format stream "~A:~%~{ ~A: failed~%~}~%" suite-name fails)) (incf passed (length passes)) (incf failed (length fails)))) *test-suites*) (list :passed passed :failed failed))) (defun test-suites () (let ((keys '())) (maphash #'(lambda (k v) (push k keys)) *test-suites*) keys)) (defun clear-all-suites () (init)) (init)