Trapperkeeper provides some utility code for use in tests. The code is available in a separate "test" jar that you may depend on by using a classifier in your project dependencies.
(defproject yourproject "1.0.0"
...
:profiles {:dev {:dependencies [[puppetlabs/trapperkeeper "x.y.z" :classifier "test"]]}})
The logging namespace provides utilities to help capture and validate logging behavior.
This form provides one of the simplest, though least discriminating
ways to examine the log events produced by a body of code. All log
events generated by the "root" logger from within the form (typically
all events) will be available for inspection by the logged?
predicate:
(with-test-logging
(log/info "hello log")
(is (logged? #"^hello log$"))
(is (logged? #"^hello log$" :info)))
Here (log/info "hello log")
generates an info level log event with a
message of "hello log", and then logged?
checks for it, first by
matching the message, and then by matching both the message and the
level.
logged?
must be called from within a with-test-logging
form, and
returns true if any events that match its arguments have been logged
since the beginning of the form.
See the logged?
docstring for a complete description, but as an
example, if the first argument is a regex pattern (typically generated
via Clojure's #"pattern"
), then logged?
will return true if the
pattern matches a single message of anything that has been logged since the
beginning of the enclosing with-test-logging
form. An optional
second parameter restricts the match to log events with the specified
level: :trace
, :debug
, :info
, :warn
, :error
or :fatal
.
Note: by default logged?
returns true only if there is exactly one
log line match. An optional third parameter can be specified to disable
this restriction.
This function converts a LogEvent to a Clojure map of the kind
generated by with-logged-event-maps
and with-logger-event-maps
. A
log event produced by (log/info "hello log")
would be converted to
this:
{:message "hello log"
:level :info
:exception nil
:logger "the.namespace.containing.the.log.info.call"}
This form provides more control than with-test-logging
by appending
an event->map
map to a collection for each log event produced within
its body, and the collection can be accessed though an atom bound to a
caller-specified name. For example, the with-test-logging
based
tests above could be rewritten like this:
(with-logged-event-maps events
(log/info "hello log")
(is (some #(re-matches #"hello log" (:message %)) @events))
(is (some #(and (re-matches #"hello log" (:message %))
(= :info (:message %)))
@events)))
A call to (with-logged-event-maps ...)
is effectively the same as
(with-logger-event-maps root-logger-name ...)
.
This form is identical to with-logged-event-maps
except that it
allows the specification of the logger-id
from which events should
be captured; with-logged-event-maps
always captures events from
root-logger-name
.
For the most part, we recommend that Trapperkeeper service definitions be written as thin wrappers around plain old functions. This means that the vast majority of your tests can be written as unit tests that operate on those functions directly.
However, it can be useful to have a few tests that actually boot up a Trapperkeeper application instance; this allows you to, for example, verify that the services that you have a dependency on get injected correctly.
To this end, the puppetlabs.trapperkeeper.testutils.bootstrap
namespace includes some helper functions and macros for creating a Trapperkeeper application. The macros should be preferred in most cases; they generally start with the prefix with-app-
, and allow you to create a temporary Trapperkeeper app given a list of services. They will take care of some important mechanics for you:
- Making sure that no JVM shutdown hooks are registered during tests, as they would be during a normal Trapperkeeper application boot sequence
- Making sure that the app is shut down properly after the test completes.
Here are some of the most useful ones:
This macro allows you to specify the services you want to launch directly and to pass in a map of configuration data that the app should use. The services specified must include all dependencies and transitive dependencies needed to start each service; that is, what you'd normally put in the bootstrap.cfg.
(ns services.test-service-1)
(defprotocol TestService1
(test-fn [this]))
(defservice test-service1
TestService1
[]
(test-fn [this] "foo"))
(ns services.test-service2)
(defservice test-service2
;;...
)
(ns test.services-test
(:require services.test-service-1 :as t1))
(with-app-with-config app
[test-service1 test-service2]
{:myconfig {:foo "foo"
:bar "bar"}}
(let [test-svc (get-service app :TestService1)]
(is (= "baz" (t1/test-fn test-svc))))
This variant is very similar, but instead of passing a map of config data, you pass a map of parsed command-line arguments, such as the path to a config file on disk that should be processed to build the actual application configuration:
(with-app-with-cli-data app
[test-service1 test-service2]
{:config "./dev-resources/config.conf"}
(let [test-svc (get-service app :TestService1)]
(is (= "baz" (t1/test-fn test-svc))))
This version accepts a vector of command-line arguments:
(with-app-with-cli-args app
[test-service1 test-service2]
["--config" "./dev-resources/config.conf" "--debug"]
(let [test-svc (get-service app :TestService1)]
(is (= "baz" (t1/test-fn test-svc))))
This version is useful when you don't need to pass in any configuration data at all to the services:
(with-app-with-empty-config app
[test-service1 test-service2]
(let [test-svc (get-service app :TestService1)]
(is (= "baz" (t1/test-fn test-svc))))
For each of the above macros, there is generally a bootstrap-services-with-*
function that will behave similarly; however, the bootstrap-*
functions don't handle the cleanup/shutdown behaviors for you, so they should only be used in rare cases.