HEUTE - Hierarchical Extensible Unit Testing Environment for Common LISP

Version 1.1

Introduction

HEUTE is a Common LISP implementation of a UNIT TESTING framework implementing a hierarchical approach to unit testing. The concept allows you to define simple tests, or suites of tests. Each test suite corresponds to one CLOS class, and subclasses form sub-suites. Sub-suites must pass to consider parent-suites as passing.

You may download the latest release from the following URL. heute.tar.gz.

Compatiblity

This release as been tested with:

Contents


License Information

Jim Newton grants you the rights to distribute and use this software as governed by the terms of the Lisp Lesser GNU Public License, known as the LLGPL.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.


Adding Tests

A Quick Example

(use-package 'heute)
;; symbols from the HEUTE package are in ALL-CAPS

;; define a subclass of HEUTE:TESTCASE
(defclass my-top-suite (TESTCASE)
  ((TEST-NAME
    ;; human readable name of this test suite
    ;; all suites should have different names.
    :initform "Top Suite")
   (TEST-FUNS
    ;; list all the test function, each should be a method on this class
    :initform '(test1
                test2))))

(defmethod test1 ((testcase my-top-suite))
  ;; make one or more assetions using the ASSERT... or FAIL-IF-.. APIs.
  (ASSERT-FALSE testcase 
                ;; some form to evaluate in this lexical scope
                (> 1 2)
                ;; you may identify a particular assertion with a uniqe tag
                :tag 100
                ;; a form to eval to get a human readable error message
                ;; in case the assertion fails.
                :text "1 is not greater than 2")
  (ASSERT-TRUE testcase
               (member 3 '( 1 5 3 7))
               :tag 101
               :text "some interesting failure message"))

(defmethod test2 ((testcase my-top-suite))
  (ASSERT-EQUAL testcase
                (- 10 5) (+ 2 3)
               :tag 200
               :text "arithmetic error")
  (ASSERT-NOT-EQUAL testcase
               (/ 10 2) (1+ (/ 10 2))
               :tag 201
               :text "an different arithmetic error"))

;; Setup function.
;; Runs before my-top-suite or any of its subclasses are tested.
(defmethod RUN-SUITE :before ((testcase my-top-suite))
  (format t "hello this is *before* my-top-suite~%"))

;; Mopup function.
;; Runs after  my-top-suite and all its subclasses are tested.
(defmethod RUN-SUITE :after ((testcase my-top-suite))
  (format t "hello this is *after* my-top-suite~%"))

;; Setup and Mopup together in an around method.
;; But don't forget to call call-next-method and return its value.
(defmethod RUN-TEST :around ((testcase my-top-suite) test-fun)
  (format t "[ hello this is *around* each of the test* functions~%")
  (prog1 (call-next-method)
    (format t "done with ~A]~%" test-fun)))

;; Run the test suite, or you can also specify a class name
(RUN-ALL-UNIT-TESTS t)

YOU MUST

To add a new test suite you must make a subclass of TESTCASE. You are required to override at least the one field TEST-NAME in your subclass to be a textual, human-readable, description of the tests the suite will cover. Generally you should create one class per package in your application.

YOU MAY

You may make any number of subclasses of TESTCASE or of any of your derived classes. These classes form a hierarchy of test suites. The hierarchy is dictacted exclusively by the CLOS class hierarchy starting with TESTCASE as the top level suite. Normally if your application has several somewhat independent pieces of functionality, you should build one subclass for each conceptual piece of functionality.

In each sublcass of TESTCASE you may override the slot TEST-FUNS to be a list of (symbols) function names. Each such function name must name a method callable on the class. Of course the method might actually only be defined on a super-class.

Each test-function (whose name is in TEST-FUNS) is expected to make calls to none, one, or many of the following functions (actually macros). Each macro evaluates the given expression expression in the lexical environment of the macro expansion. In each case a different situation is checked to decide if it is a PASS or FAIL condition.

(ASSERT-FALSE testcase expression &key tag text action-on-fail)
(FAIL-IF testcase expression &key tag text action-on-fail)
Fail if the expression evaluates to non-nil.

(ASSERT-TRUE testcase expression &key tag text action-on-fail)
(FAIL-IF-NOT testcase expression &key tag text action-on-fail)
Fail if the expression evaluates to nil.

(ASSERT-NOT-CONDITION testcase condition expression &key tag text action-on-fail)
(FAIL-IF-CONDITION testcase condition expression &key tag text action-on-fail)
Fail if evaluating the expression signals the given (or a compatible) condition.

(ASSERT-CONDITION testcase condition expression &key tag text action-on-fail)
(FAIL-IF-NOT-CONDITION testcase condition expression &key tag text action-on-fail)
Fail if evaluating the expression does not signal the given condition nor any compatible condition.

(ASSERT-NOT-EQUAL testcase lhs rhs &key tag text action-on-fail test)
(FAIL-IF-EQUAL testcase lhs rhs &key tag text action-on-fail test)
Fail if evaluting the two expressions, lhs and rhs are equal under the given test function, defaulting to #'EQL.

(ASSERT-EQUAL testcase lhs rhs &key tag text action-on-fail test)
(FAIL-IF-NOT-EQUAL testcase lhs rhs &key tag text action-on-fail test)
Fail if evaluting the two expressions, lhs and rhs are not equal under the given test function, defaulting to #'EQL.

Other Arguments:

An example some unit tests is provided with HEUTE.


Setup and Mopup Methods

Each stage of the HEUTE engine is actually the envocation of a generic function. You may implement SETUP and MOPUP function on a per-suite or a per-test basis by defining before and after methods on the following generic functions, specializing on your TESTCASE subclass.

(RUN-SUITE testcase)
You may define a setup or mopup function to run once on the suite by defingin a before, after, or around method of this generic function specializing on your subclass of TESTCASE. However, be warned. Because of the way CLOS works these auxilary methods are also applicable to any subclass of your subclass. So if you build a hierarchy of TESTCASE classes for your application, the same RUN-SUITE methods will be run on all of them unless of course you define different methods for each one.

(RUN-TEST testcase test-fun)
You may define setup or mopup functions on each of the TEST-FUNS by defining a before, after, or around method of this generic function specializing on your subclass of TESTCASE. The RUN-TEST generic function will be called once for each function you have listed in the TEST-FUNS slot of your subclass of TESTCASE.

Overview of the Algorithm

The HEUTE algorithm recursively descends the CLOS class tree starting at the TESTCASE class. Each class might have sub-suites defined as identified by the subclasses of the class, and might have TEST-FUNS. The algorithm first runs recursively on each subclass, then on its own TEST-FUNS. A test-suite is considered PASS if all the sub-suites pass and all the TEST-FUNS pass. A test-suite is considered FAIL if any of the sub-suites or any of the TEST-FUNS fail.

Adding A Graphical Front End

To add a graphical frontend onto HEUTE, you must define a class with DEFCLASS register it by name with the function REGISTER-GUI. E.g.,
(use-package 'heute)
(defclass my-gui-class ()
   ())
(register-gui 'my-gui-class)

Doing so desructively modifies the CLOS class hierarchy of the HEUTE framework allowing you to write auxillary methods specializing on your class. There are several generic functions of interest for this purpose.

(RUN-ALL-UNIT-TESTS testcase )
Defining a :BEFORE method on RUN-ALL-UNIT-TESTS specializing on your graphic-ui class allows you to initialize the necessary graphial environment.

(RUN-SUITE testcase )
For example, defining a RUN-SUITE :BEFORE method allows the graphical environemnt to draw a graphical item representing the test suite. Defining a RUN-SUITE :AFTER method allows the the graphical environment to paint the graphical item red or green to indicate PASS or FAIL.

(RUN-TEST testcase test-fun )

(PARENT-SUITE testcase)
Returns nil if this is the top level suite testcase obj, otherwise returns the parent object. The parent object is an instance of a superclass of the given object's class.

(SUITE-LEVEL testcase)
Returns an integer indicating at which level of the test suite tree the given testcase object is located. 0 indicates the top level. 1 indicates direct children of the top level etc.

(STATUS testcase)
Returns some element of the list *test-statuses*. While the test is running this return the value :RUNNING. After the test completes, either :PASS or :FAIL is returned.

(NUMBER-OF-SIBLINGS testcase)
This returns the number of subclasses of the class of the parent object in the test suite hierarchy.

(SIBLING-INDEX testcase)
This returns an integer between 0, inclusive and (NUMBER-OF-SIBLINGS testcase), exclusive, corresponding to the index of the given test.

Two UI front-ends are provided with HEUTE by default:

Here is a picture of what the LTK front end looks like. Red rectangles signify tests that failed. Yellow signifies tests that are running or waiting to run. Green signifies tests that have passed. You can click the mouse on any of the rectangles to re-run that test and all its sub-tests.

Example of LTK Graphical frontend
After tests are complete red and green indicate pass/fail status.

Example of LTK Graphical frontend
While the tests are running, yello indicates that a test is waiting to run or running.

Things to be careful of.

To Do


Please contact Jim Newton (jimka at rdrop dot com) if you have questions or comments or if you'd like to make a contribution to the code. Please let me know if you find this package useful!
$Id: heute.html,v 1.24 2006/01/07 12:50:09 jimka Exp $