1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
|
# Trapperkeeper Test Utils
Trapperkeeper provides some [utility code](https://github.com/puppetlabs/trapperkeeper/tree/master/test/puppetlabs/trapperkeeper/testutils) 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.
```clojure
(defproject yourproject "1.0.0"
...
:profiles {:dev {:dependencies [[puppetlabs/trapperkeeper "x.y.z" :classifier "test"]]}})
```
## Logging
The
[logging namespace](https://github.com/puppetlabs/trapperkeeper/tree/master/test/puppetlabs/trapperkeeper/testutils/logging.clj)
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:
```clojure
(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:
```clojure
{: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:
```clojure
(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.
```clj
(ns services.test-service-1)
(defprotocol TestService1
(test-fn [this]))
(defservice test-service1
TestService1
[]
(test-fn [this] "foo"))
```
```clj
(ns services.test-service2)
(defservice test-service2
;;...
)
```
```clj
(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:
```clj
(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:
```clj
(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:
```clj
(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.
|