Skip to content

Commit

Permalink
read data from disk improvement (#251)
Browse files Browse the repository at this point in the history
This commit adds a new feature that allows a user to specify
a subset of a JSON file by using the existing JSONPath functionality
by expanding on the read from disk operator '<@'

closes #250

* add small warning about readability when using JSONPath from disk
* Adds both unit and gabbit-based tests

NOTE:
  * switching from str to six.string_types is necessary to handle
    encoded  values.
  • Loading branch information
trevormccasland authored and cdent committed Jun 29, 2018
1 parent 913cd94 commit 72256b0
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 8 deletions.
40 changes: 40 additions & 0 deletions docs/source/jsonpath.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,46 @@ or::

Examples like this can be found in one of gabbi's `own tests`_.

When reading from disk you can apply the same JSONPath by adding a ':' to the
end of your file name. This allows you to store multiple API responses into
a JSON file to reduce file management when constructing your tests.

.. highlight:: json

Given JSON data as follows::

{
"values": [{
"pets": [{
"type": "cat",
"sound": "meow"
}, {
"type": "dog",
"sound": "woof"
}]
}, {
"people": [{
"name": "chris",
"id": 1
}, {
"name": "justin",
"id": 2
}]
}]
}

.. highlight:: yaml

You can write your tests like the following::

response_json_paths:
$.pets: <@pets.json
$.pets[?type = "cat"].sound: <@values.json:$.values[0].pets[?type = "cat"].sound

Although placing more than one API response into a single JSON file may seem
convenient, keep in mind there is a tradeoff in readability that should not
be overlooked before implementing this technique.

There are more JSONPath examples in :doc:`example` and in the
`jsonpath_rw`_ and `jsonpath_rw_ext`_ documentation.

Expand Down
32 changes: 24 additions & 8 deletions gabbi/handlers/jsonhandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,29 +76,45 @@ def extract_json_path_value(data, path):
def action(self, test, path, value=None):
"""Test json_paths against json data."""
# Do template expansion in the left hand side.
path = test.replace_template(path)
lhs_path = test.replace_template(path)
rhs_path = rhs_match = None
try:
match = self.extract_json_path_value(
test.response_data, path)
lhs_match = self.extract_json_path_value(
test.response_data, lhs_path)
except AttributeError:
raise AssertionError('unable to extract JSON from test results')
except ValueError:
raise AssertionError('json path %s cannot match %s' %
(path, test.response_data))
raise AssertionError('left hand side json path %s cannot match '
'%s' % (path, test.response_data))

# read data from disk if the value starts with '<@'
if isinstance(value, str) and value.startswith('<@'):
if isinstance(value, six.string_types) and value.startswith('<@'):
# Do template expansion in the rhs if rhs_path is provided.
if ':' in value:
value, rhs_path = value.split(':$', 1)
rhs_path = test.replace_template('$' + rhs_path)
info = test.load_data_file(value.replace('<@', '', 1))
info = six.text_type(info, 'UTF-8')
value = self.loads(info)
if rhs_path:
try:
rhs_match = self.extract_json_path_value(value, rhs_path)
except AttributeError:
raise AssertionError('unable to extract JSON from data on '
'disk')
except ValueError:
raise AssertionError('right hand side json path %s cannot '
'match %s' % (rhs_path, info))

# If expected is a string, check to see if it is a regex.
is_regex = (isinstance(value, six.string_types) and
value.startswith('/') and
value.endswith('/') and
len(value) > 1)
expected = test.replace_template(value, escape_regex=is_regex)
if is_regex:
expected = (rhs_match or
test.replace_template(value, escape_regex=is_regex))
match = lhs_match
if is_regex and not rhs_match:
expected = expected[1:-1]
# match may be a number so stringify
match = six.text_type(match)
Expand Down
19 changes: 19 additions & 0 deletions gabbi/tests/gabbits_handlers/values.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"values": [{
"pets": [{
"type": "cat",
"sound": "meow"
}, {
"type": "dog",
"sound": "woof"
}]
}, {
"people": [{
"name": "chris",
"id": 1
}, {
"name": "justin",
"id": 2
}]
}]
}
25 changes: 25 additions & 0 deletions gabbi/tests/gabbits_intercept/data-right-side.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Test loading expected data from file on disk with JSONPath
#
defaults:
request_headers:
content-type: application/json
verbose: True

tests:
- name: json encoded value from disk
POST: /
data: <@data.json
response_json_paths:
$.foo['bár']: <@data.json:$.foo['bár']

- name: json parital from disk
POST: /
data: <@cat.json
response_json_paths:
$: <@pets.json:$[?type = "cat"]

- name: json partial both sides
POST: /
data: <@pets.json
response_json_paths:
$[?type = "cat"].sound: <@values.json:$.values[0].pets[?type = "cat"].sound
19 changes: 19 additions & 0 deletions gabbi/tests/gabbits_intercept/values.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"values": [{
"pets": [{
"type": "cat",
"sound": "meow"
}, {
"type": "dog",
"sound": "woof"
}]
}, {
"people": [{
"name": "chris",
"id": 1
}, {
"name": "justin",
"id": 2
}]
}]
}
34 changes: 34 additions & 0 deletions gabbi/tests/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""

import json
import os
import unittest

from gabbi import case
Expand Down Expand Up @@ -268,6 +269,39 @@ def test_response_json_paths_dict_type(self):
self.assertIn("response_json_paths in 'omega test'",
str(exc))

def test_response_json_paths_from_disk_json_path(self):
handler = jsonhandler.JSONHandler()
lhs = '$.pets[?type = "cat"].sound'
rhs = '$.values[0].pets[?type = "cat"].sound'
self.test.test_directory = os.path.dirname(__file__)
self.test.test_data = {'response_json_paths': {
lhs: '<@gabbits_handlers/values.json:' + rhs,
}}
self.test.response_data = {
"pets": [
{"type": "cat", "sound": "meow"},
{"type": "dog", "sound": "woof"}
]
}
self._assert_handler(handler)

def test_response_json_paths_from_disk_json_path_fail(self):
handler = jsonhandler.JSONHandler()
lhs = '$.pets[?type = "cat"].sound'
rhs = '$.values[0].pets[?type = "bad"].sound'
self.test.test_directory = os.path.dirname(__file__)
self.test.test_data = {'response_json_paths': {
lhs: '<@gabbits_handlers/values.json:' + rhs,
}}
self.test.response_data = {
"pets": [
{"type": "cat", "sound": "meow"},
{"type": "dog", "sound": "woof"}
]
}
with self.assertRaises(AssertionError):
self._assert_handler(handler)

def test_response_headers(self):
handler = core.HeadersResponseHandler()
self.test.response = {'content-type': 'text/plain'}
Expand Down

0 comments on commit 72256b0

Please sign in to comment.