github.com/Jeffail/benthos/v3@v3.65.0/website/docs/configuration/unit_testing.md (about) 1 --- 2 title: Unit Testing 3 --- 4 5 The Benthos service offers a command `benthos test` for running unit tests on sections of a configuration file. This makes it easy to protect your config files from regressions over time. 6 7 ## Contents 8 9 1. [Writing a Test](#writing-a-test) 10 2. [Output Conditions](#output-conditions) 11 3. [Running Tests](#running-tests) 12 4. [Mocking Processors](#mocking-processors) 13 14 ## Writing a Test 15 16 Let's imagine we have a configuration file `foo.yaml` containing some processors: 17 18 ```yaml 19 input: 20 kafka: 21 addresses: [ TODO ] 22 topics: [ foo, bar ] 23 consumer_group: foogroup 24 25 pipeline: 26 processors: 27 - bloblang: '"%vend".format(content().uppercase().string())' 28 29 output: 30 aws_s3: 31 bucket: TODO 32 path: '${! meta("kafka_topic") }/${! json("message.id") }.json' 33 ``` 34 35 One way to write our unit tests for this config is to accompany it with a file of the same name and extension but suffixed with `_benthos_test`, which in this case would be `foo_benthos_test.yaml`. We can generate an example definition for this config with `benthos test --generate ./foo.yaml` which gives: 36 37 ```yml 38 tests: 39 - name: example test 40 target_processors: '/pipeline/processors' 41 environment: {} 42 input_batch: 43 - content: 'example content' 44 metadata: 45 example_key: example metadata value 46 output_batches: 47 - 48 - content_equals: example content 49 metadata_equals: 50 example_key: example metadata value 51 ``` 52 53 Under `tests` we have a list of any number of unit tests to execute for the config file. Each test is run in complete isolation, including any resources defined by the config file. Tests should be allocated a unique `name` that identifies the feature being tested. 54 55 The field `target_processors` is a [JSON Pointer][json-pointer] that identifies the specific processors within the file which should be executed by the test. This allows you to target a specific processor (`/pipeline/processors/0`), or processors within a different section on your config (`/input/broker/inputs/0/processors`) if required. 56 57 The field `environment` allows you to define an object of key/value pairs that set environment variables to be evaluated during the parsing of the target config file. These are unique to each test, allowing you to test different environment variable interpolation combinations. 58 59 The field `input_batch` lists one or more messages to be fed into the targeted processors as a batch. Each message of the batch may have its raw content defined as well as metadata key/value pairs. 60 61 For the common case where the messages are in JSON format, you can use `json_content` instead of `content` to specify the message structurally rather than verbatim. 62 63 The field `output_batches` lists any number of batches of messages which are expected to result from the target processors. Each batch lists any number of messages, each one defining [`conditions`](#output-conditions) to describe the expected contents of the message. 64 65 If the number of batches defined does not match the resulting number of batches the test will fail. If the number of messages defined in each batch does not match the number in the resulting batches the test will fail. If any condition of a message fails then the test fails. 66 67 ### Inline Tests 68 69 Sometimes it's more convenient to define your tests within the config being tested. This is fine, simply add the `tests` field to the end of the config being tested. 70 71 ### Bloblang Tests 72 73 Sometimes when working with large [Bloblang mappings][bloblang] it's preferred to have the full mapping in a separate file to your Benthos configuration. In this case it's possible to write unit tests that target and execute the mapping directly with the field `target_mapping`, which when specified is interpreted as either an absolute path or a path relative to the test definition file that points to a file containing only a Bloblang mapping. 74 75 For example, if we were to have a file `cities.blobl` containing a mapping: 76 77 ```coffee 78 root.Cities = this.locations. 79 filter(loc -> loc.state == "WA"). 80 map_each(loc -> loc.name). 81 sort().join(", ") 82 ``` 83 84 We can accompany it with a test file `cities_test.yaml` containing a regular test definition: 85 86 ```yml 87 tests: 88 - name: test cities mapping 89 target_mapping: './cities.blobl' 90 environment: {} 91 input_batch: 92 - content: | 93 { 94 "locations": [ 95 {"name": "Seattle", "state": "WA"}, 96 {"name": "New York", "state": "NY"}, 97 {"name": "Bellevue", "state": "WA"}, 98 {"name": "Olympia", "state": "WA"} 99 ] 100 } 101 output_batches: 102 - 103 - json_equals: {"Cities": "Bellevue, Olympia, Seattle"} 104 ``` 105 106 And execute this test the same way we execute other Benthos tests (`benthos test ./dir/cities_test.yaml`, `benthos test ./dir/...`, etc). 107 108 ### Fragmented Tests 109 110 Sometimes the number of tests you need to define in order to cover a config file is so vast that it's necessary to split them across multiple test definition files. This is possible but Benthos still requires a way to detect the configuration file being targeted by these fragmented test definition files. In order to do this we must prefix our `target_processors` field with the path of the target relative to the definition file. 111 112 The syntax of `target_processors` in this case is a full [JSON Pointer][json-pointer] that should look something like `target.yaml#/pipeline/processors`. For example, if we saved our test definition above in an arbitrary location like `./tests/first.yaml` and wanted to target our original `foo.yaml` config file, we could do that with the following: 113 114 ```yml 115 tests: 116 - name: example test 117 target_processors: '../foo.yaml#/pipeline/processors' 118 environment: {} 119 input_batch: 120 - content: 'example content' 121 metadata: 122 example_key: example metadata value 123 output_batches: 124 - 125 - content_equals: example content 126 metadata_equals: 127 example_key: example metadata value 128 ``` 129 130 ## Input Definitions 131 132 ### `content` 133 134 Sets the raw content of the message. 135 136 ### `json_content` 137 138 ```yml 139 json_content: 140 foo: foo value 141 bar: [ element1, 10 ] 142 ``` 143 144 Sets the raw content of the message to a JSON document matching the structure of the value. 145 146 ### `file_content` 147 148 ```yml 149 file_content: ./foo/bar.txt 150 ``` 151 152 Sets the raw content of the message by reading a file. The path of the file should be relative to the path of the test file. 153 154 ### `metadata` 155 156 A map of key/value pairs that sets the metadata values of the message. 157 158 ## Output Conditions 159 160 ### `bloblang` 161 162 ```yml 163 bloblang: 'this.age > 10 && meta("foo").length() > 0' 164 ``` 165 166 Executes a [Bloblang expression][bloblang] on a message, if the result is anything other than a boolean equalling `true` the test fails. 167 168 ### `content_equals` 169 170 ```yml 171 content_equals: example content 172 ``` 173 174 Checks the full raw contents of a message against a value. 175 176 ### `content_matches` 177 178 ```yml 179 content_matches: "^foo [a-z]+ bar$" 180 ``` 181 182 Checks whether the full raw contents of a message matches a regular expression (re2). 183 184 ### `metadata_equals` 185 186 ```yml 187 metadata_equals: 188 example_key: example metadata value 189 ``` 190 191 Checks a map of metadata keys to values against the metadata stored in the message. If there is a value mismatch between a key of the condition versus the message metadata this condition will fail. 192 193 ### `file_equals` 194 195 ```yml 196 file_equals: ./foo/bar.txt 197 ``` 198 199 Checks that the contents of a message matches the contents of a file. The path of the file should be relative to the path of the test file. 200 201 ### `json_equals` 202 203 ```yml 204 json_equals: { "key": "value" } 205 ``` 206 207 Checks that both the message and the condition are valid JSON documents, and that they are structurally equivalent. Will ignore formatting and ordering differences. 208 209 You can also structure the condition content as YAML and it will be converted to the equivalent JSON document for testing: 210 211 ```yml 212 json_equals: 213 key: value 214 ``` 215 216 ### `json_contains` 217 218 ```yml 219 json_contains: { "key": "value" } 220 ``` 221 222 Checks that both the message and the condition are valid JSON documents, and that the message is a superset of the condition. 223 224 ## Running Tests 225 226 Executing tests for a specific config can be done by pointing the subcommand `test` at either the config to be tested or its test definition, e.g. `benthos test ./config.yaml` and `benthos test ./config_benthos_test.yaml` are equivalent. 227 228 In order to execute all tests of a directory simply point `test` to that directory, e.g. `benthos test ./foo` will execute all tests found in the directory `foo`. In order to walk a directory tree and execute all tests found you can use the shortcut `./...`, e.g. `benthos test ./...` will execute all tests found in the current directory, any child directories, and so on. 229 230 ## Mocking Processors 231 232 BETA: This feature is currently in a BETA phase, which means breaking changes could be made if a fundamental issue with the feature is found. 233 234 Sometimes you'll want to write tests for a series of processors, where one or more of them are networked (or otherwise stateful). Rather than creating and managing mocked services you can define mock versions of those processors in the test definition. For example, if we have a config with the following processors: 235 236 ```yaml 237 pipeline: 238 processors: 239 - bloblang: 'root = "simon says: " + content()' 240 - label: get_foobar_api 241 http: 242 url: http://example.com/foobar 243 verb: GET 244 - bloblang: 'root = content().uppercase()' 245 ``` 246 247 Rather than create a fake service for the `http` processor to interact with we can define a mock in our test definition that replaces it with a `bloblang` processor. Mocks are configured as a map of labels that identify a processor to replace and the config to replace it with: 248 249 ```yaml 250 tests: 251 - name: mocks the http proc 252 target_processors: '/pipeline/processors' 253 mocks: 254 get_foobar_api: 255 bloblang: 'root = content().string() + " this is some mock content"' 256 input_batch: 257 - content: "hello world" 258 output_batches: 259 - - content_equals: "SIMON SAYS: HELLO WORLD THIS IS SOME MOCK CONTENT" 260 ``` 261 262 With the above test definition the `http` processor will be swapped out for `bloblang: 'root = content().string() + " this is some mock content"'`. For the purposes of mocking it is recommended that you use a `bloblang` processor that simply mutates the message in a way that you would expect the mocked processor to. 263 264 > Note: It's not currently possible to mock components that are imported as separate resource files (using `--resource`/`-r`). It is recommended that you mock these by maintaining separate definitions for test purposes (`-r "./test/*.yaml"`). 265 266 ### More granular mocking 267 268 It is also possible to target specific fields within the test config by [JSON pointers][json-pointer] as an alternative to labels. The following test definition would create the same mock as the previous: 269 270 ```yaml 271 tests: 272 - name: mocks the http proc 273 target_processors: '/pipeline/processors' 274 mocks: 275 /pipeline/processors/1: 276 bloblang: 'root = content().string() + " this is some mock content"' 277 input_batch: 278 - content: "hello world" 279 output_batches: 280 - - content_equals: "SIMON SAYS: HELLO WORLD THIS IS SOME MOCK CONTENT" 281 ``` 282 283 [json-pointer]: https://tools.ietf.org/html/rfc6901 284 [bloblang]: /docs/guides/bloblang/about