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