Skip to content

Latest commit

 

History

History
179 lines (135 loc) · 6.66 KB

Test-Utils.md

File metadata and controls

179 lines (135 loc) · 6.66 KB

Trapperkeeper Test Utils

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"]]}})

Logging

The logging namespace provides utilities to help capture and validate logging behavior.

with-test-logging

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?

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.

event->map

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"}

with-logged-event-maps

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 ...).

with-logger-event-maps

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.

Testing Services

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:

with-app-with-config

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))))

with-app-with-cli-data

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))))

with-app-with-cli-args

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))))

with-app-with-empty-config

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.