2

What are the best practices / recommendations around building complex functionality in karate-netty?

We have a requirement to write a relatively complex mock service to avoid hitting our integration partner in lower regions. We have about 8 endpoints comprising search, lookup, and additional information for later stages of our checkout funnel. We need to do things like for id = 7, return this payload, but for id = 8, return that payload. karate-netty supports all of this very nicely! However, an additional requirement we have is to closely mirror error behavior - eg missing auth header, wrong shape of payload, etc. With this sort of conditional/exception handling, we struggled to find a paradigm within karate-netty that worked for us.

To explain what I mean, imagine an endpoint which requires an auth header, and valid request.foo, and valid request.bar. You might write something like this:

Scenario: pathMatches('ourEndpoint') && methodIs('post') && !headerContains('Auth', 'secret')
  * call read('403.feature')

Scenario: pathMatches('ourEndpoint') && methodIs('post') && !request.foo
  * call read('422.feature') { missing: 'foo' }

Scenario: pathMatches('ourEndpoint') && methodIs('post') && !request.bar
  * call read('422.feature') { missing: 'bar' }

Scenario: pathMatches('ourEndpoint') && methodIs('post') && headerContains('Auth', 'secret') && request.foo && request.bar
  * call read('200.feature')

This is fine for small examples like this, but the problem with this approach for complicated endpoints doing a lot of validation is that you have to specify all of your conditions twice - once in the error case and once in the 200 case. This is not so optimal when you have 10 different scenarios for an endpoint - your 200 condition in particular becomes extremely unwieldy. It would be nice if there was something like karate.abort() that instead of aborting a Scenario instead aborted an entire feature - karate.abortFeature() if you will. Then you would only have to specify your error conditions once and simplify the condition for the 200 case to simply handle every request to that endpoint that hasn't errored out before, eg:

Scenario: pathMatches('ourEndpoint') && methodIs('post') && !headerContains('Auth', 'secret')
  * call read('403.feature')
  * karate.abortFeature()

Scenario: pathMatches('ourEndpoint') && methodIs('post') && !request.foo
  * call read('422.feature') { missing: 'foo' }
  * karate.abortFeature()

Scenario: pathMatches('ourEndpoint') && methodIs('post') && !request.bar
  * call read('422.feature') { missing: 'bar' }
  * karate.abortFeature()

Scenario: pathMatches('ourEndpoint') && methodIs('post')
  * call read('200.feature')

It would also be nice if there were some way to support filtering at multiple levels; then we could have per-endpoint feature files like so:

Feature: pathMatches('ourEndpoint') && methodIs('post')
  Scenario: !headerContains('Auth', 'secret')
    * call read('403.feature')
    * karate.abortFeature()

  Scenario: !request.foo
    * call read('422.feature') { missing: 'foo' }
    * karate.abortFeature()

  Scenario: !request.bar
    * call read('422.feature') { missing: 'bar' }
    * karate.abortFeature()

  Scenario:
    * call read('200.feature')

Lacking the above, we went with pulling all the conditionals and exception flows in to a single scenario, something like this:

* def abortWithResponse = 
"""
  function(responseStatus, response) { 
    karate.set('response', response);
    karate.set('responseStatus', responseStatus);
    karate.abort();
  }
"""

Scenario: pathMatches('ourEndpoint') && methodIs('post')
  * if (!headerContains('Auth', 'secret')) abortWithResponse(403, read('403response.json'));
  * if (!request.foo) abortWithResponse(422, build422Response({missing: ['foo']));
  * if (!request.bar) abortWithResponse(422, build422Response({missing: ['bar']));
  * call read('200.feature')

This works, but feels rather inelegant and hard to read. Is there a different pattern that anyone is using to accomplish these ends? If not, would adding karate.abortFeature() be a useful addition?

Thanks in advance!

skibrianski
  • 131
  • 3

2 Answers2

1

I actually thought your last example (single scenario) was pretty elegant :)

Fwiw, what my team does is typically the opposite -- some of our mocks are quite lengthy, as we don't mind including a million scenarios, each with one little twist of their own (the only thing wrong with this request is this particular header, and therefore here is our very particular response for that issue), mostly because it's so easy to write server-side scenarios in Karate.

One thing we do tend to leverage, however, is a Background function or three to take care of ALL the validation a request must pass in order to get a 200. For instance, we might have 5 or 10 things we need to see in the payload. In that case, we'll probably define some "payloadValidator" function as such:

    * def payloadValidator =
      """
        function() {
          return karate.match("request contains{ id: '123', localTime: '045940', localDate: '1216', ref: 'A0A0' }").pass
        }
      """

...then, we'd simply include "payloadValidator()" in the expression for our 200 response scenario. Everything underneath can then focus on one particular issue with the payload at a time (either within one more scenario by leveraging things like IF statements, as you exampled, or spread across multiple scenarios...your choice).

Staffier
  • 118
  • 3
  • I like this. you can even choose to return a JSON instead of a boolean that gives you custom error messages if needed. – Peter Thomas Feb 25 '21 at 04:37
0

As the other answer says - I think your last example looks good and you can use the approach suggested there - which is to write a single, generic, re-usable validateRequest function - that will abort and / or return error messages.

Karate's approach is interesting as you can choose to break out your server-side edge cases into Scenario-s or lean towards "real" server-side patterns where you handle a GET /some/resource and then use (or re-use) logic to handle edge or error cases.

Glad you already figured that you can do karate.set('response', blah) any time - giving a lot of flexibility to conditionally return error-messages or error codes.

That said, since I personally felt that for extreme scenarios, the ability to use "proper" JavaScript would be nice, the Karate 1.0 series adds an alternate way to write API mocks. You can find an example here. Unfortunately there is no documentation yet, but I'm including an image I'm working on for now:

enter image description here

To see this work, clone the develop branch and run demo.ServerRunner as a JUnit test.

After seeing this, 2 things may happen:

  • in comparison, you may prefer the existing "low code" way of Karate mocks - and it may feel easier to understand and maintain
  • you see value in using "pure JS" to express mocks and would be interested in exploring this more and suggesting changes or enhancements.
Peter Thomas
  • 54,465
  • 21
  • 84
  • 248