The Test Machine
Rationale
The test machine is what we came up with in an attempt to make it easier to write more reliable integration tests. By handling reads and writes in a background thread and making them available in a Clojure ref, the test author is relieved of the requirement to manually send!
and recv
messages from kafka. The initial aim was to simply ‘fire’ events at the system and then observe the outputs that are (or are not) collected by the reader. In general, the aims are to allow the creation of blackbox integration tests which are:
- Generative, to avoid manual creation and alteration of test cases (or we will not write them, and will eventually abandon them)
- Reliable, so we trust the results (or we will ignore them)
- Fast, so we get results quickly (or we will not run them)
The test machine also aims to be agnostic to the system it is testing, other than that it should get input/output primarily from Kafka.
Construction
The examples below, demonstrate how to create a test machine for executing a sequence of “commands”. The “local-machine” will execute the commands against a local kafka cluster with all services running on their default ports. The “remote-machine” in contrast executes commands over HTTP using the configured rest-proxy. This can be useful in scenarios where you don’t have direct access to these services in a shared environment like uat/staging.
(ns my.app-test
(:require
[my.app :as app]
[jackdaw.test :as j.t]))
(def local-kafka-config
{"bootstrap.servers" "localhost:9092"
"group.id" "my-app"})
(def topic-config
{:foo {:topic-name "foo"
:key-serde (string-serde)
:value-serde (json-serde)}
:bar {:topic-name "foo"
:key-serde (string-serde)
:value-serde (edn-serde)}})
(defn local-machine []
(let [t (trns/transport {:type :kafka
:config local-kafka-config
:topics topic-config})]
(test-machine {:transport t})))
(def remote-machine []
(let [t (trns/transport {:type :confluent-rest-proxy
:config remote-kafka-config
:topics topic-config})]
(test-machine {:transport t})))
Serialization/Deserialization
The topic-config
referenced in the snippet above is a mapping from topic-ids to serialization/deserialization configurations. This means that tests can read/write using the same serializers/deserializers used by your applications. So for example, on encountering a command like
[:write! :foo {:id 1, :msg "hello"}]
the test-machine will lookup :foo
in the topic-config to get the :key-serde
and :value-serde
which it will then use when writing the message. Similarly, all topics listed in the topic-config are read using the corresponding deserializer.
Lifecycle
The test-machine implements the Closeable
protocol so be sure to use it in conjunction with with-open
to ensure that associated resources are shut down cleanly when you are finished with a machine.
(deftest test-my-app
(with-open [machine (local-machine))]
(let [result (run-test machine test-commands)]
(is (all-ok? result))))
Test Commands
Each test-command is a vector with the first item being a keyword representing the type of operation this command represents, and subsequent items being command specific arguments. Currently the following commands are the supported.
:write! [topic-id msg opts] Writes to the
:watch [f opts] Blocks until `(f @journal)` returns truthy
:stop [] Stops processing commands. All subsequent commands
are ignored
:sleep [sleep-ms] Sleeps for `sleep-ms` milliseconds
:println [args] Prints the supplied args to stdout
:pprint [args] Pretty prints the supplied args to stdout
:do [f] Execute arbitrary function
Test Results
The return value from run-test
is a map with just two keys
:results A sequence of execution results. One for each command attempted
:journal A snapshot of all kafka output read by the test consumer
The journal contains all output written to the topics configured when creating the test-machine (including any input messages). Each key in the journal represents one topic. The value is a vector of messages observed on the topic in the order that they were observed.
Fixtures
A selection of fixtures are provided to help setting up required topics and to start the applications, and external systems under test. For more details see the functions in the jackdaw.test.fixtures namespace.
Wrapping up
You make find it helpful to write a function to tie it all together and invoke your test function f
with a machine after performing any setup required. Since this typically involves some knowledge of the system under test, it’s likely better that you write this macro yourself so that you can tailor it to your own requirements.
(defn with-test-machine [f {:keys [transport}]}]
(fix/with-fixtures [(fix/topic-fixture kafka-config input-topics)
(fix/topic-fixture kafka-config output-topics)
(fix/service-ready {:http-url "http://localhost:8082"
:http-timeout 5000})]
(with-open [machine (test-machine {:transport transport})]
(f machine))))