Skip to content

Commit 0619cad

Browse files
author
Fabien Thouraud
committed
feat(httpApi): add startWith, endWith matchers as well as concise matcher expressions
1 parent 824c435 commit 0619cad

11 files changed

+642
-52
lines changed

.eslintrc.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
parserOptions:
2-
ecmaVersion: 2017
2+
ecmaVersion: 2019
33
sourceType: script
44
env:
55
node: true
66
es6: true
77
jest: true
88
extends: eslint:recommended
99
rules:
10-
no-unused-vars: off
10+
no-unused-vars: off

README.md

+65-12
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,17 @@ Scenario: Fetching some json response from the internets
237237
| address.country | equal | Japan |
238238
```
239239

240+
Checking json response properties start with value:
241+
242+
```gherkin
243+
Scenario: Fetching some json response from the internets
244+
When I GET http://whatever.io/things/1
245+
Then json response should match
246+
| field | matcher | value |
247+
| name | start with | ing |
248+
| address.country | starts with | Jap |
249+
```
250+
240251
Checking json response properties contain value:
241252

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

262+
Checking json response properties end with value:
263+
264+
```gherkin
265+
Scenario: Fetching some json response from the internets
266+
When I GET http://whatever.io/things/1
267+
Then json response should match
268+
| field | matcher | value |
269+
| name | end with | ing |
270+
| address.country | ends with | pan |
271+
```
272+
251273
Checking json response properties match value:
252274

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

287309
Available matchers are:
288310

289-
| matcher | description |
290-
|-------------------------- |---------------------------------------------------|
291-
| `match` | property must match given regexp |
292-
| `matches` | see `match` |
293-
| `contain` | property must contain given value |
294-
| `contains` | see `contain` |
295-
| `defined` | property must not be `undefined` |
296-
| `present` | see `defined` |
297-
| `equal` | property must equal given value |
298-
| `equals` | see `equal` |
299-
| `type` | property must be of the given type |
300-
| `equalRelativeDate` | property must be equal to the computed date |
311+
| matcher | short matcher | description |
312+
|-------------------------- |-------------------------- |---------------------------------------------------|
313+
| `match` | `~=` | property must match given regexp |
314+
| `matches` | see `match` | see `match` |
315+
| `start with` | `^=` | property must start with given value |
316+
| `starts with` | see `start with` | see `startWith` |
317+
| `contain` | `*=` | property must contain given value |
318+
| `contains` | see `contain` | see `contain` |
319+
| `end with` | `$=` | property must end with given value |
320+
| `ends with` | see `end with` | see `endWith` |
321+
| `defined` | `?` | property must not be `undefined` |
322+
| `present` | see `defined` | see `defined` |
323+
| `equal` | `=` | property must equal given value |
324+
| `equals` | see `equal` | see `equal` |
325+
| `type` | `#=` | property must be of the given type |
326+
| `equalRelativeDate` | n/a | property must be equal to the computed date |
301327

302328
**Any** of these matchers can be negated when preceded by these : `!`, `not`, `does not`, `doesn't`, `is not` and `isn't`.
329+
330+
The short version of each matcher is intended to be used that way:
331+
332+
```gherkin
333+
Scenario: Fetching some json response from the internets
334+
When I GET http://whatever.io/things/1
335+
Then json response should fully match
336+
| expression |
337+
| name ~= ^(.+)ing$ |
338+
| address.country = Japan |
339+
| address.city ? |
340+
| address.postalCode #= string |
341+
```
342+
343+
If it eases the reading, you can also pad your expressions:
344+
345+
```gherkin
346+
Scenario: Fetching some json response from the internets
347+
When I GET http://whatever.io/things/1
348+
Then json response should fully match
349+
| expression |
350+
| name ~= ^(.+)ing$ |
351+
| address.country = Japan |
352+
| address.city ? |
353+
| address.postalCode #= string |
354+
```
355+
303356
#### Testing response headers
304357

305358
In order to check response headers, you have the following gherkin expression available:

doc/README.tpl.md

+65-12
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,17 @@ Scenario: Fetching some json response from the internets
241241
| address.country | equal | Japan |
242242
```
243243

244+
Checking json response properties start with value:
245+
246+
```gherkin
247+
Scenario: Fetching some json response from the internets
248+
When I GET http://whatever.io/things/1
249+
Then json response should match
250+
| field | matcher | value |
251+
| name | start with | ing |
252+
| address.country | starts with | Jap |
253+
```
254+
244255
Checking json response properties contain value:
245256

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

266+
Checking json response properties end with value:
267+
268+
```gherkin
269+
Scenario: Fetching some json response from the internets
270+
When I GET http://whatever.io/things/1
271+
Then json response should match
272+
| field | matcher | value |
273+
| name | end with | ing |
274+
| address.country | ends with | pan |
275+
```
276+
255277
Checking json response properties match value:
256278

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

291313
Available matchers are:
292314

293-
| matcher | description |
294-
|-------------------------- |---------------------------------------------------|
295-
| `match` | property must match given regexp |
296-
| `matches` | see `match` |
297-
| `contain` | property must contain given value |
298-
| `contains` | see `contain` |
299-
| `defined` | property must not be `undefined` |
300-
| `present` | see `defined` |
301-
| `equal` | property must equal given value |
302-
| `equals` | see `equal` |
303-
| `type` | property must be of the given type |
304-
| `equalRelativeDate` | property must be equal to the computed date |
315+
| matcher | short matcher | description |
316+
|-------------------------- |-------------------------- |---------------------------------------------------|
317+
| `match` | `~=` | property must match given regexp |
318+
| `matches` | see `match` | see `match` |
319+
| `start with` | `^=` | property must start with given value |
320+
| `starts with` | see `start with` | see `startWith` |
321+
| `contain` | `*=` | property must contain given value |
322+
| `contains` | see `contain` | see `contain` |
323+
| `end with` | `$=` | property must end with given value |
324+
| `ends with` | see `end with` | see `endWith` |
325+
| `defined` | `?` | property must not be `undefined` |
326+
| `present` | see `defined` | see `defined` |
327+
| `equal` | `=` | property must equal given value |
328+
| `equals` | see `equal` | see `equal` |
329+
| `type` | `#=` | property must be of the given type |
330+
| `equalRelativeDate` | n/a | property must be equal to the computed date |
305331

306332
**Any** of these matchers can be negated when preceded by these : `!`, `not`, `does not`, `doesn't`, `is not` and `isn't`.
333+
334+
The short version of each matcher is intended to be used that way:
335+
336+
```gherkin
337+
Scenario: Fetching some json response from the internets
338+
When I GET http://whatever.io/things/1
339+
Then json response should fully match
340+
| expression |
341+
| name ~= ^(.+)ing$ |
342+
| address.country = Japan |
343+
| address.city ? |
344+
| address.postalCode #= string |
345+
```
346+
347+
If it eases the reading, you can also pad your expressions:
348+
349+
```gherkin
350+
Scenario: Fetching some json response from the internets
351+
When I GET http://whatever.io/things/1
352+
Then json response should fully match
353+
| expression |
354+
| name ~= ^(.+)ing$ |
355+
| address.country = Japan |
356+
| address.city ? |
357+
| address.postalCode #= string |
358+
```
359+
307360
#### Testing response headers
308361

309362
In order to check response headers, you have the following gherkin expression available:

src/core/assertions.js

+51-6
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,29 @@
55
*/
66

77
const _ = require('lodash')
8-
const { expect } = require('chai')
8+
const { expect, use } = require('chai')
99
const moment = require('moment-timezone')
1010
const Cast = require('./cast')
11+
const { registerChaiAssertion } = require('./custom_chai_assertions')
12+
13+
use(registerChaiAssertion)
1114

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

2126
const RuleName = Object.freeze({
2227
Match: Symbol('match'),
2328
Contain: Symbol('contain'),
29+
StartWith: Symbol('startWith'),
30+
EndWith: Symbol('endWith'),
2431
Present: Symbol('present'),
2532
Equal: Symbol('equal'),
2633
Type: Symbol('type'),
@@ -141,6 +148,34 @@ exports.assertObjectMatchSpec = (object, spec, exact = false) => {
141148
}
142149
break
143150
}
151+
case RuleName.StartWith: {
152+
const baseExpect = expect(
153+
currentValue,
154+
`Property '${field}' (${currentValue}) ${
155+
rule.isNegated ? 'starts with' : 'does not start with'
156+
} '${expectedValue}'`
157+
)
158+
if (rule.isNegated) {
159+
baseExpect.to.not.startWith(expectedValue)
160+
} else {
161+
baseExpect.to.startWith(expectedValue)
162+
}
163+
break
164+
}
165+
case RuleName.EndWith: {
166+
const baseExpect = expect(
167+
currentValue,
168+
`Property '${field}' (${currentValue}) ${
169+
rule.isNegated ? 'ends with' : 'does not end with'
170+
} '${expectedValue}'`
171+
)
172+
if (rule.isNegated) {
173+
baseExpect.to.not.endWith(expectedValue)
174+
} else {
175+
baseExpect.to.endWith(expectedValue)
176+
}
177+
break
178+
}
144179
case RuleName.Present: {
145180
const baseExpect = expect(
146181
currentValue,
@@ -244,6 +279,16 @@ exports.getMatchingRule = (matcher) => {
244279
return { name: RuleName.Contain, isNegated: !!containGroups[1] }
245280
}
246281

282+
const startWithGroups = startWithRegex.exec(matcher)
283+
if (startWithGroups) {
284+
return { name: RuleName.StartWith, isNegated: !!startWithGroups[1] }
285+
}
286+
287+
const endWithGroups = endWithRegex.exec(matcher)
288+
if (endWithGroups) {
289+
return { name: RuleName.EndWith, isNegated: !!endWithGroups[1] }
290+
}
291+
247292
const presentGroups = presentRegex.exec(matcher)
248293
if (presentGroups) {
249294
return { name: RuleName.Present, isNegated: !!presentGroups[1] }

src/core/custom_chai_assertions.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
exports.registerChaiAssertion = (chai, utils) => {
2+
chai.Assertion.addMethod('startWith', function (expected) {
3+
return this.assert(
4+
typeof this._obj === 'string' && this._obj.startsWith(expected),
5+
`expected #{this} to start with #{exp}`,
6+
`expected #{this} not to start with #{exp}`,
7+
expected
8+
)
9+
})
10+
chai.Assertion.addMethod('endWith', function (expected) {
11+
return this.assert(
12+
typeof this._obj === 'string' && this._obj.endsWith(expected),
13+
`expected #{this} to end with #{exp}`,
14+
`expected #{this} not to end with #{exp}`,
15+
expected
16+
)
17+
})
18+
}

src/extensions/http_api/definitions.js

+9-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const Cast = require('../../core/cast')
99
const { assertObjectMatchSpec } = require('../../core/assertions')
1010

1111
const { STATUS_CODES } = require('http')
12+
const { parseMatchExpression } = require('./utils')
1213
const STATUS_MESSAGES = _.values(STATUS_CODES).map(_.lowerCase)
1314

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

304305
// First we populate spec values if it contains some placeholder
305-
const spec = table.hashes().map((fieldSpec) =>
306-
_.assign({}, fieldSpec, {
307-
value: this.state.populate(fieldSpec.value),
306+
const specifications = table.hashes().map((fieldSpec) => {
307+
const spec = fieldSpec.expression
308+
? parseMatchExpression(fieldSpec.expression)
309+
: fieldSpec
310+
return _.assign({}, spec, {
311+
value: this.state.populate(spec.value),
308312
})
309-
)
313+
})
310314

311-
assertObjectMatchSpec(body, spec, !!fully)
315+
assertObjectMatchSpec(body, specifications, !!fully)
312316
})
313317

314318
/**

src/extensions/http_api/utils.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const expressionRegex = /(?<field>[^\s]+)\s+(?<matcher>!?(?:[#~*^$]?=|\?))(?:\s+(?<value>.+))?/
2+
3+
exports.parseMatchExpression = (expression) => {
4+
const results = expressionRegex.exec(expression)
5+
if (results) return results.groups
6+
throw new TypeError(`'${expression}' is not a valid expression`)
7+
}

0 commit comments

Comments
 (0)