By this point, your little web application should be looking pretty fantastic. It’s simple, but powerful and extensible. The refactoring we’ve done, while fairly cursory, has gone a long way to making sure that more work on the system is possible and easy. There’s one section where we’re lacking, however, and that’s a test suite. Most programmers today have gotten onboard the unit testing bus in some capacity or another. It is possible to go overboard – my feeling is that test suites should contain absurdly small amounts of logic, and be comprised of one or two assertions per unit test. Even then, it’s very easy to start testing the wrong behavior, or writing specious tests.

With these thoughts in mind, we’re going to write a small, efffective test suite. We will be doing so using the FiveAM library. FiveAM is attractive for a few reasons:

It encapsulates tests into logical units, called suites , much like functions, macros, and classes are broken up into packages.

, much like functions, macros, and classes are broken up into packages. The assertion operator is simple. is takes a form and if the form evalutates to non-nil, then the assertion passes. If the form evaluates to nil , the assertion fails.

takes a form and if the form evalutates to non-nil, then the assertion passes. If the form evaluates to , the assertion fails. It allows for the use of a wrapping macro called a fixture. While other testing suites use the term ‘fixture’ to describe pre-constructed data, such as records in a database, FiveAM extends the notion to include the setup and teardown of tests.

The first thing you should do is create a place to put the tests. Off of my main directory, I’ve simply touched test/test-game-voter.lisp . test-game-voter will be another package, so declare it:

( defpackage test-game-voter ( :use :cl :game-voter :elephant :fiveam )) ( in-package :test-game-voter )

And we’ll write a test to make sure everything works as expected. Make sure to enter the package.

TEST-GAME-VOTER> (test sanity (is (= 4 (+ 2 2)))) TEST-GAME-VOTER> (run!) . Did 1 check. Pass: 1 (100%) Skip: 0 ( 0%) Fail: 0 ( 0%)

Pretty nice, eh? Extremely simple. The single period in the output after the run! command is the single passed test, similar to what you’d see in xUnit derivatives. Now let’s see what happens when a test fails.

TEST-GAME-VOTER> (test more-sanity (is (= 4 (+ 2 4)))) TEST-GAME-VOTER> (run!) .f Did 2 checks. Pass: 1 (50%) Skip: 0 ( 0%) Fail: 1 (50%) Failure Details: -------------------------------- MORE-SANITY []: (+ 2 4) evaluated to 6, which is not = to 4.. --------------------------------

Again, a huge amount of detail. Now, remove those tests from the suite with (rem-test 'sanity) and (rem-test 'more-sanity) . We have real tests to write.

The first unit tests we’re going to tackle are on the model level – making sure the game class we created in the first tutorial works the way we expect. However, it’s a persistent class, and we want to make sure we’re saving test instances somewhere else other than our public database. So that’s the first step: defining an Elephant configuration and controller to house this new data. The second step is making sure we compartmentalize our model tests into a model suite.

( defparameter *test-database-config* '( :clsql ( :sqlite3 "test-store" )) "The connection information and filename of the database used in the test suite." ) ( defparameter *test-database-controller* (open-store *test-database-config*) "The database controller for the test suite. We open one here so that every test run doesn't open more." ) (def-suite :suite-game-voter-model ) (in-suite :suite-game-voter-model )

The next part is going to be a little tricky. We don’t want every single test adding its own results to the database, and then using that dirtied database in the next test. Elephant exposes database transaction management to us, but that’s not quite good enough. We need to be able to specify that the transaction should be aborted. This is going to involve diving into the internals of what makes Elephant go: the clsql-sys and db-clsql packages.

Without getting too much into it, I will say this. *test-database-controller* is our instance of the data store. Stored on it is the information used to track transactions, including the status of the transaction. This field can be set manually, and if Elephant attempts to close a transaction and finds that it has failed, it rolls it back entirely. How does it know if it’s failed? It checks to see if the transaction status is :aborted .

With that explanation in mind, here’s what we’re looking at in terms of code. This may not be the most elegant way of dealing with it, but it works nicely for our purposes.

(def-fixture db-fixtures () (with-transaction ( :store-controller *test-database-controller*) ( &body ) (setf (clsql-sys::transaction-status (clsql-sys::transaction (db-clsql::controller-db *test-database-controller*))) :aborted )))

def-fixture is a FiveAM macro. It works almost exactly like defmacro , instead placing the described macro into FiveAM’s own repertoire of fixtures. with-transaction is within Elephant’s namespace, and takes an associated list of options. In this case we want to make sure the transaction is occuring on our test controller. The &body tag is implicit with def-fixture , so we don’t need to provide it in the argument list. Finally, we setf the status of the transaction we’re in to :aborted . When with-transaction hits the end of this form, it will see the transaction ‘failed’, and roll back everything we’ve done in the body.

Now let’s begin writing some tests! Off the top of my head, I can think of a couple things we’d like to test: adding a game, voting for a game, getting a game based on its name, and ensuring we can tell if a game is stored by passing its name. We’ll start with adding a game.

(test test-add-game-name (with-fixture db-fixtures () ( let ((game (game-voter::add-game "test game" ))) (is (equal "test game" (game-voter::name game)))))) (test test-add-game-votes (with-fixture db-fixtures () ( let ((game (game-voter::add-game "test game" ))) (is (= 0 (game-voter::votes game))))))

Call run! again and check out the results. Did you get two passing checks? Notice that run! reports the total number of checks, or assertions, and not the number of tests. This is because FiveAM will continue through a test even if a check fails, and report the passing status of all the checks in a test.

Note that we have to use the double-colon notation for our game functions, since we haven’t made them externally visible from our game-voter package.

This is looking good, but we can remove just a sliver of duplication by writing a macro for tests we know will be using the database. To wit:

( defmacro db-test (test-name &body body) `(test ,test-name (with-fixture db-fixtures () ,@body))) (db-test test-add-game-name ( let ((game (game-voter::add-game "test game" ))) (is (equal "test game" (game-voter::name game)))))

Write some more tests. Make sure that game-from-name returns correct values for games that are or aren’t in the system. Make sure adding a game with a duplicate name fails. Then, write something like this:

(db-test test-sanitized-game-name (game-voter::add-game "Foo" ) (game-voter::add-game " Foo " ) (is (= 1 (length (game-voter::games)))))

Well, that fails! We aren’t sanitizing our input! This isn’t even caught on the client side. This is what writing good tests is about – finding small pieces of behavior that fail in unexpected ways.

We can ensure that the server takes care of extra padding around the game name by trimming it. string-trim takes a string called a char bag, and then the string you wish to trim. Since all strings are array of characters, string-trim walks through the char bag and removes instances of each character from both sides of your argument.

We can modify the add-game function as below:

( defun add-game (name) ( let ((sanitized-name (string-trim " " name))) (with-transaction () ( unless (game-stored? sanitized-name) (make-instance 'persistent-game :name sanitized-name)))))

Once the new function is evaluated, running the test suite again should pass.