Karate: Idea: bridge native cucumber examples to Karate JSON

Created on 2 Apr 2019  路  12Comments  路  Source: intuit/karate

Would appreciate some feedback on this. Triggered by answering a couple of Stack Overflow questions recently - see this one: https://stackoverflow.com/a/55475101/143475

In other words - suppose you want to create a JSON from an Examples row - you have to go through that step of using the angle-brackets like this:

* def payload = { foo: '<foo>', bar: '<bar>' }
Examples:
| foo | bar |
| x   | y   |

The problem here is that the * def payload line above cannot be moved into a re-usable JSON file. Because it is not the Karate-native embedded expression style. The angle-brackets have to be in-line in the feature, this is a Cucumber thing. This is painful when the payload gets complex.

Proposed Solution: introduce a new keyword to auto-populate a JSON variable with the current row of the Examples table being processed:

* example data
* def payload = { foo: '#(data.foo)', bar: '#(data.bar)' }
Examples:
| foo | bar |
| x   | y   |

But most of the time the data is un-necessary noise so:

* example
* def payload = { foo: '#(foo)', bar: '#(bar)' }
Examples:
| foo | bar |
| x   | y   |

Which means now you can do this:

* example
* def payload = read('reusable.json')
Examples:
| foo | bar |
| x   | y   |

Where reusable.json is in the good old embedded-expression format:

{ "foo": "#(foo)", "bar": "#(bar)" }

The * example on its own is a little weird. Any other ideas ? I am not in favor of auto converting the example-row to variables in scope - because of impact to existing tests.

enhancement fixed

Most helpful comment

this is a very interesting one ! finally decided to go with karate.set() and __row and __num for the magic variables. also I decided on a very simple strategy for "type hints" which is to append a ! after the column name.

these three examples should make things clear. all of this should work for dynamic scenario outlines also, but needs testing

Scenario Outline: name is <name> and age is <age>
  * def name = '<name>'
  * match name == __row.name
  * def expected = __num == 0 ? 'name is Bob and age is 5' : 'name is Nyan and age is 6'
  * match expected == karate.info.scenarioName

  Examples:
    | name | age |
    | Bob  | 5   |
    | Nyan | 6   |

Scenario Outline: magic variables with type hints
  * def expected = __num == 0 ? { name: 'Bob', age: 5 } : { name: 'Nyan', age: 6 }
  * match __row == expected

  Examples:
    | name | age! |
    | Bob  | 5    |
    | Nyan | 6    |

Scenario Outline: magic variables with embedded expressions
  * def expected = __num == 0 ? { name: 'Bob', alive: false } : { name: 'Nyan', alive: true }
  * match expected == { name: '#(__row.name)', alive: '#(__row.alive)' }
  * eval karate.set(__row)
  * match expected == { name: '#(name)', alive: '#(alive)' }

  Examples:
    | name | alive! |
    | Bob  | false  |
    | Nyan | true   |

All 12 comments

And something like : * def payload = example ?
This mapping isn't really usefull { "foo": "#(foo)", "bar": "#(bar)" }
Example could be directly mapped into json ?

@LeJeanbono * def payload = example implies that there is a magic variable called example which is not right - yes we have response and friends - but for good reason. and I don't like __example either. I think the mapping to variables in scope is useful - that's what you typically intend when you use Examples IMO

Having this as a option in configure will look more of the karate way. * configure examplesToJSON = true. User can configure it on different scope based on the need.

@babusekaran yeah that's a good suggestion ! thanks

@LeJeanbono I think I now understand what you meant and agree. I'm thinking that the "directly mapped JSON" would be available from the built-in variable __arg which is consistent with how call works:

* match __arg == { foo: 'x', bar: 'y' }
* def temp = { foo: '#(foo)', bar: '#(bar)' }
* match temp == __arg
Examples:
| foo | bar |
| x   | y   |

just realized - one huge headache here is data-types in Examples: today they all default to string :|

Something like this for data-types ?

Examples:
| foo: string | bar: integer | foobar: date |
| foo         | 1            | 2012-07-14   |

@LeJeanbono yeah :) but I'm inclined to leave it the way it is. if follks need complex data type handling - it is always possible using the old <foo> placeholder mechanism combined with * def = { foo: '<foo>' }

BTW this was one of the biggest breaking changes in Cucumber 3 / 4 - and I want to avoid that mess

to avoid breaking existing tests, here's another proposal - to use __eg and __row as the magic variables within Scenario Outline-s

* match __eg == { foo: 'x', bar: 'y' }
* match __row == 0
Examples:
| foo | bar |
| x   | y   |

Since you may need the convenience of injecting the current row as variable names, we can add a general-purpose JS API karate.def() that will take a given JSON and spray all the keys as variables into the current scope, similar to how "shared scope" works for call:

* def data = { foo: 'x', bar: 'y' }
* eval karate.def(data)
* match foo == 'x'
* match bar == 'y'

so now if you have a JSON file { "foo": "#(foo)", "bar": "#(bar)" }, you can do this:

* eval karate.def(__eg)
* def payload = karate.read('reusable.json')
* match payload == { foo: 'x', bar: 'y' }
Examples:
| foo | bar |
| x   | y   |

the final problem we need to solve is data-types. maybe the suggestion from @LeJeanbono should be considered after all:

* match __eg == { foo: 'x', bar: 1 }
Examples:
| foo | bar: number |
| x   | 1   |

I would stick with the limited JS data types sufficient for JSON marshalling: string (default), number and boolean. If any complex data-type massaging is needed, just do it manually.

This is one of those changes that may tilt things towards feeling "weird" - so would like some feedback.

this is a very interesting one ! finally decided to go with karate.set() and __row and __num for the magic variables. also I decided on a very simple strategy for "type hints" which is to append a ! after the column name.

these three examples should make things clear. all of this should work for dynamic scenario outlines also, but needs testing

Scenario Outline: name is <name> and age is <age>
  * def name = '<name>'
  * match name == __row.name
  * def expected = __num == 0 ? 'name is Bob and age is 5' : 'name is Nyan and age is 6'
  * match expected == karate.info.scenarioName

  Examples:
    | name | age |
    | Bob  | 5   |
    | Nyan | 6   |

Scenario Outline: magic variables with type hints
  * def expected = __num == 0 ? { name: 'Bob', age: 5 } : { name: 'Nyan', age: 6 }
  * match __row == expected

  Examples:
    | name | age! |
    | Bob  | 5    |
    | Nyan | 6    |

Scenario Outline: magic variables with embedded expressions
  * def expected = __num == 0 ? { name: 'Bob', alive: false } : { name: 'Nyan', alive: true }
  * match expected == { name: '#(__row.name)', alive: '#(__row.alive)' }
  * eval karate.set(__row)
  * match expected == { name: '#(name)', alive: '#(alive)' }

  Examples:
    | name | alive! |
    | Bob  | false  |
    | Nyan | true   |

an update the extra * eval karate.set(__row) is no longer necessary, this is done automatically by default, but can be switched off via a configure setting. after writing a few data-driven java unit-tests using karate (yes you read that right, unit-tests :) I felt this was the right thing to do, see example: https://github.com/intuit/karate/blob/develop/karate-demo/src/test/java/demo/unit/cat.feature

released 0.9.3

Was this page helpful?
0 / 5 - 0 ratings