Skip to content

Commit

Permalink
feat(httpApi): add startWith, endWith matchers as well as concise mat…
Browse files Browse the repository at this point in the history
…cher expressions
  • Loading branch information
fthouraud committed Aug 6, 2021
1 parent 824c435 commit 0619cad
Show file tree
Hide file tree
Showing 11 changed files with 642 additions and 52 deletions.
4 changes: 2 additions & 2 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
parserOptions:
ecmaVersion: 2017
ecmaVersion: 2019
sourceType: script
env:
node: true
es6: true
jest: true
extends: eslint:recommended
rules:
no-unused-vars: off
no-unused-vars: off
77 changes: 65 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,17 @@ Scenario: Fetching some json response from the internets
| address.country | equal | Japan |
```

Checking json response properties start with value:

```gherkin
Scenario: Fetching some json response from the internets
When I GET http://whatever.io/things/1
Then json response should match
| field | matcher | value |
| name | start with | ing |
| address.country | starts with | Jap |
```

Checking json response properties contain value:

```gherkin
Expand All @@ -248,6 +259,17 @@ Scenario: Fetching some json response from the internets
| address.country | contain | Jap |
```

Checking json response properties end with value:

```gherkin
Scenario: Fetching some json response from the internets
When I GET http://whatever.io/things/1
Then json response should match
| field | matcher | value |
| name | end with | ing |
| address.country | ends with | pan |
```

Checking json response properties match value:

```gherkin
Expand Down Expand Up @@ -286,20 +308,51 @@ Now if the json contains extra properties, the test will fail.

Available matchers are:

| matcher | description |
|-------------------------- |---------------------------------------------------|
| `match` | property must match given regexp |
| `matches` | see `match` |
| `contain` | property must contain given value |
| `contains` | see `contain` |
| `defined` | property must not be `undefined` |
| `present` | see `defined` |
| `equal` | property must equal given value |
| `equals` | see `equal` |
| `type` | property must be of the given type |
| `equalRelativeDate` | property must be equal to the computed date |
| matcher | short matcher | description |
|-------------------------- |-------------------------- |---------------------------------------------------|
| `match` | `~=` | property must match given regexp |
| `matches` | see `match` | see `match` |
| `start with` | `^=` | property must start with given value |
| `starts with` | see `start with` | see `startWith` |
| `contain` | `*=` | property must contain given value |
| `contains` | see `contain` | see `contain` |
| `end with` | `$=` | property must end with given value |
| `ends with` | see `end with` | see `endWith` |
| `defined` | `?` | property must not be `undefined` |
| `present` | see `defined` | see `defined` |
| `equal` | `=` | property must equal given value |
| `equals` | see `equal` | see `equal` |
| `type` | `#=` | property must be of the given type |
| `equalRelativeDate` | n/a | property must be equal to the computed date |

**Any** of these matchers can be negated when preceded by these : `!`, `not`, `does not`, `doesn't`, `is not` and `isn't`.

The short version of each matcher is intended to be used that way:

```gherkin
Scenario: Fetching some json response from the internets
When I GET http://whatever.io/things/1
Then json response should fully match
| expression |
| name ~= ^(.+)ing$ |
| address.country = Japan |
| address.city ? |
| address.postalCode #= string |
```

If it eases the reading, you can also pad your expressions:

```gherkin
Scenario: Fetching some json response from the internets
When I GET http://whatever.io/things/1
Then json response should fully match
| expression |
| name ~= ^(.+)ing$ |
| address.country = Japan |
| address.city ? |
| address.postalCode #= string |
```

#### Testing response headers

In order to check response headers, you have the following gherkin expression available:
Expand Down
77 changes: 65 additions & 12 deletions doc/README.tpl.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,17 @@ Scenario: Fetching some json response from the internets
| address.country | equal | Japan |
```

Checking json response properties start with value:

```gherkin
Scenario: Fetching some json response from the internets
When I GET http://whatever.io/things/1
Then json response should match
| field | matcher | value |
| name | start with | ing |
| address.country | starts with | Jap |
```

Checking json response properties contain value:

```gherkin
Expand All @@ -252,6 +263,17 @@ Scenario: Fetching some json response from the internets
| address.country | contain | Jap |
```

Checking json response properties end with value:

```gherkin
Scenario: Fetching some json response from the internets
When I GET http://whatever.io/things/1
Then json response should match
| field | matcher | value |
| name | end with | ing |
| address.country | ends with | pan |
```

Checking json response properties match value:

```gherkin
Expand Down Expand Up @@ -290,20 +312,51 @@ Now if the json contains extra properties, the test will fail.

Available matchers are:

| matcher | description |
|-------------------------- |---------------------------------------------------|
| `match` | property must match given regexp |
| `matches` | see `match` |
| `contain` | property must contain given value |
| `contains` | see `contain` |
| `defined` | property must not be `undefined` |
| `present` | see `defined` |
| `equal` | property must equal given value |
| `equals` | see `equal` |
| `type` | property must be of the given type |
| `equalRelativeDate` | property must be equal to the computed date |
| matcher | short matcher | description |
|-------------------------- |-------------------------- |---------------------------------------------------|
| `match` | `~=` | property must match given regexp |
| `matches` | see `match` | see `match` |
| `start with` | `^=` | property must start with given value |
| `starts with` | see `start with` | see `startWith` |
| `contain` | `*=` | property must contain given value |
| `contains` | see `contain` | see `contain` |
| `end with` | `$=` | property must end with given value |
| `ends with` | see `end with` | see `endWith` |
| `defined` | `?` | property must not be `undefined` |
| `present` | see `defined` | see `defined` |
| `equal` | `=` | property must equal given value |
| `equals` | see `equal` | see `equal` |
| `type` | `#=` | property must be of the given type |
| `equalRelativeDate` | n/a | property must be equal to the computed date |

**Any** of these matchers can be negated when preceded by these : `!`, `not`, `does not`, `doesn't`, `is not` and `isn't`.

The short version of each matcher is intended to be used that way:

```gherkin
Scenario: Fetching some json response from the internets
When I GET http://whatever.io/things/1
Then json response should fully match
| expression |
| name ~= ^(.+)ing$ |
| address.country = Japan |
| address.city ? |
| address.postalCode #= string |
```

If it eases the reading, you can also pad your expressions:

```gherkin
Scenario: Fetching some json response from the internets
When I GET http://whatever.io/things/1
Then json response should fully match
| expression |
| name ~= ^(.+)ing$ |
| address.country = Japan |
| address.city ? |
| address.postalCode #= string |
```

#### Testing response headers

In order to check response headers, you have the following gherkin expression available:
Expand Down
57 changes: 51 additions & 6 deletions src/core/assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,29 @@
*/

const _ = require('lodash')
const { expect } = require('chai')
const { expect, use } = require('chai')
const moment = require('moment-timezone')
const Cast = require('./cast')
const { registerChaiAssertion } = require('./custom_chai_assertions')

use(registerChaiAssertion)

const negationRegex = `!|! |not |does not |doesn't |is not |isn't `
const matchRegex = new RegExp(`^(${negationRegex})?(match|matches)$`)
const containRegex = new RegExp(`^(${negationRegex})?(contain|contains)$`)
const presentRegex = new RegExp(`^(${negationRegex})?(defined|present)$`)
const equalRegex = new RegExp(`^(${negationRegex})?(equal|equals)$`)
const typeRegex = new RegExp(`^(${negationRegex})?(type)$`)
const matchRegex = new RegExp(`^(${negationRegex})?(match|matches|~=)$`)
const containRegex = new RegExp(`^(${negationRegex})?(contains?|\\*=)$`)
const startWithRegex = new RegExp(`^(${negationRegex})?(starts? with|\\^=)$`)
const endWithRegex = new RegExp(`^(${negationRegex})?(ends? with|\\$=)$`)
const presentRegex = new RegExp(`^(${negationRegex})?(defined|present|\\?)$`)
const equalRegex = new RegExp(`^(${negationRegex})?(equals?|=)$`)
const typeRegex = new RegExp(`^(${negationRegex})?(type|#=)$`)
const relativeDateRegex = new RegExp(`^(${negationRegex})?(equalRelativeDate)$`)
const relativeDateValueRegex = /^(\+?\d|-?\d),([A-Za-z]+),([A-Za-z-]{2,5}),(.+)$/

const RuleName = Object.freeze({
Match: Symbol('match'),
Contain: Symbol('contain'),
StartWith: Symbol('startWith'),
EndWith: Symbol('endWith'),
Present: Symbol('present'),
Equal: Symbol('equal'),
Type: Symbol('type'),
Expand Down Expand Up @@ -141,6 +148,34 @@ exports.assertObjectMatchSpec = (object, spec, exact = false) => {
}
break
}
case RuleName.StartWith: {
const baseExpect = expect(
currentValue,
`Property '${field}' (${currentValue}) ${
rule.isNegated ? 'starts with' : 'does not start with'
} '${expectedValue}'`
)
if (rule.isNegated) {
baseExpect.to.not.startWith(expectedValue)
} else {
baseExpect.to.startWith(expectedValue)
}
break
}
case RuleName.EndWith: {
const baseExpect = expect(
currentValue,
`Property '${field}' (${currentValue}) ${
rule.isNegated ? 'ends with' : 'does not end with'
} '${expectedValue}'`
)
if (rule.isNegated) {
baseExpect.to.not.endWith(expectedValue)
} else {
baseExpect.to.endWith(expectedValue)
}
break
}
case RuleName.Present: {
const baseExpect = expect(
currentValue,
Expand Down Expand Up @@ -244,6 +279,16 @@ exports.getMatchingRule = (matcher) => {
return { name: RuleName.Contain, isNegated: !!containGroups[1] }
}

const startWithGroups = startWithRegex.exec(matcher)
if (startWithGroups) {
return { name: RuleName.StartWith, isNegated: !!startWithGroups[1] }
}

const endWithGroups = endWithRegex.exec(matcher)
if (endWithGroups) {
return { name: RuleName.EndWith, isNegated: !!endWithGroups[1] }
}

const presentGroups = presentRegex.exec(matcher)
if (presentGroups) {
return { name: RuleName.Present, isNegated: !!presentGroups[1] }
Expand Down
18 changes: 18 additions & 0 deletions src/core/custom_chai_assertions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
exports.registerChaiAssertion = (chai, utils) => {
chai.Assertion.addMethod('startWith', function (expected) {
return this.assert(
typeof this._obj === 'string' && this._obj.startsWith(expected),
`expected #{this} to start with #{exp}`,
`expected #{this} not to start with #{exp}`,
expected
)
})
chai.Assertion.addMethod('endWith', function (expected) {
return this.assert(
typeof this._obj === 'string' && this._obj.endsWith(expected),
`expected #{this} to end with #{exp}`,
`expected #{this} not to end with #{exp}`,
expected
)
})
}
14 changes: 9 additions & 5 deletions src/extensions/http_api/definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const Cast = require('../../core/cast')
const { assertObjectMatchSpec } = require('../../core/assertions')

const { STATUS_CODES } = require('http')
const { parseMatchExpression } = require('./utils')
const STATUS_MESSAGES = _.values(STATUS_CODES).map(_.lowerCase)

/**
Expand Down Expand Up @@ -302,13 +303,16 @@ exports.install = ({ baseUrl = '' } = {}) => {
expect(response.headers['content-type']).to.contain('application/json')

// First we populate spec values if it contains some placeholder
const spec = table.hashes().map((fieldSpec) =>
_.assign({}, fieldSpec, {
value: this.state.populate(fieldSpec.value),
const specifications = table.hashes().map((fieldSpec) => {
const spec = fieldSpec.expression
? parseMatchExpression(fieldSpec.expression)
: fieldSpec
return _.assign({}, spec, {
value: this.state.populate(spec.value),
})
)
})

assertObjectMatchSpec(body, spec, !!fully)
assertObjectMatchSpec(body, specifications, !!fully)
})

/**
Expand Down
7 changes: 7 additions & 0 deletions src/extensions/http_api/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const expressionRegex = /(?<field>[^\s]+)\s+(?<matcher>!?(?:[#~*^$]?=|\?))(?:\s+(?<value>.+))?/

exports.parseMatchExpression = (expression) => {
const results = expressionRegex.exec(expression)
if (results) return results.groups
throw new TypeError(`'${expression}' is not a valid expression`)
}

0 comments on commit 0619cad

Please sign in to comment.