Skip to content

Commit 19510ea

Browse files
committed
Merge pull request #14 from apibyexample/carles/non-strict-matching
Support non-strict matching of samples
2 parents 4f3b980 + 50d4e1a commit 19510ea

File tree

7 files changed

+341
-42
lines changed

7 files changed

+341
-42
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ python:
88
install: pip install -r test-requirements.txt
99
script:
1010
- python -m unittest discover || python -m unittest
11-
- flake8 abe
11+
- python -m doctest abe/utils.py
12+
- flake8 abe tests

abe/unittest.py

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22

33
from .mocks import AbeMock
4-
from .utils import to_unicode
4+
from .utils import normalize, subkeys
55

66

77
class AbeTestMixin(object):
@@ -33,53 +33,71 @@ def get_sample_request(self, path, label):
3333
sample_request = sample.examples[label].request
3434
return sample_request.body
3535

36-
def assert_data_equal(self, data1, data2):
36+
def assert_item_matches(self, real, sample):
37+
"""
38+
A primitive value matches the sample.
39+
40+
If the sample represents a parameter, then do simple pattern matching.
41+
42+
"""
43+
real = normalize(real)
44+
sample = normalize(sample)
45+
self.assertEqual(real, sample)
46+
47+
def assert_data_equal(self, real, sample, non_strict=None):
3748
"""
3849
Two elements are recursively equal
50+
51+
:param non_strict:
52+
Names of fields to match non-strictly. In current implementation,
53+
only check for field presence.
3954
"""
55+
non_strict = non_strict or []
4056
try:
41-
if isinstance(data1, list):
42-
self.assertIsInstance(data2, list)
43-
self.assert_data_list_equal(data1, data2)
44-
elif isinstance(data1, dict):
45-
self.assertIsInstance(data2, dict)
46-
self.assert_data_dict_equal(data1, data2)
57+
if isinstance(real, list):
58+
self.assertIsInstance(sample, list)
59+
self.assert_data_list_equal(real, sample, non_strict)
60+
elif isinstance(real, dict):
61+
self.assertIsInstance(sample, dict)
62+
self.assert_data_dict_equal(real, sample, non_strict)
4763
else:
48-
data1 = to_unicode(data1)
49-
data2 = to_unicode(data2)
50-
self.assertIsInstance(data2, data1.__class__)
51-
self.assertEqual(data1, data2)
64+
self.assert_item_matches(real, sample)
5265
except AssertionError as exc:
53-
message = str(exc) + '\n{}\n{}\n\n'.format(data1, data2)
66+
message = str(exc) + '\n{}\n{}\n\n'.format(real, sample)
5467
raise type(exc)(message)
5568

56-
def assert_data_dict_equal(self, data1, data2):
69+
def assert_data_dict_equal(self, real, sample, non_strict=None):
5770
"""
5871
Two dicts are recursively equal without taking order into account
5972
"""
73+
non_strict = non_strict or []
6074
self.assertEqual(
61-
len(data1), len(data2),
75+
len(real), len(sample),
6276
msg='Number of elements mismatch: {} != {}\n'.format(
63-
data1.keys(), data2.keys())
77+
real.keys(), sample.keys())
6478
)
65-
for key in data1:
66-
self.assertIn(key, data2)
67-
self.assert_data_equal(data1[key], data2[key])
79+
for key in sample:
80+
self.assertIn(key, real)
81+
if key not in non_strict:
82+
inner_non_strict = subkeys(non_strict, key)
83+
self.assert_data_equal(
84+
real[key], sample[key], inner_non_strict)
6885

69-
def assert_data_list_equal(self, data1, data2):
86+
def assert_data_list_equal(self, real, sample, non_strict=None):
7087
"""
7188
Two lists are recursively equal, including ordering.
7289
"""
90+
non_strict = non_strict or []
7391
self.assertEqual(
74-
len(data1), len(data2),
92+
len(real), len(sample),
7593
msg='Number of elements mismatch: {} {}'.format(
76-
data1, data2)
94+
real, sample)
7795
)
7896

7997
exceptions = []
80-
for element, element2 in zip(data1, data2):
98+
for real_item, sample_item in zip(real, sample):
8199
try:
82-
self.assert_data_equal(element, element2)
100+
self.assert_data_equal(real_item, sample_item, non_strict)
83101
except AssertionError as exc:
84102
exceptions.append(exc)
85103

@@ -107,32 +125,44 @@ def assert_headers_contain(self, response_data, spec_data):
107125
"header {0}".format(expected_header)
108126
)
109127

110-
def assert_matches_request(self, sample_request, wsgi_request):
128+
def assert_matches_request(self, sample_request, wsgi_request,
129+
non_strict=None):
111130
"""
112131
Check that the sample request and wsgi request match.
113132
"""
114-
self.assertEqual(wsgi_request.META['PATH_INFO'], sample_request['url'])
115-
self.assertEqual(wsgi_request.META['REQUEST_METHOD'],
116-
sample_request['method'])
133+
non_strict = non_strict or []
134+
135+
if 'url' not in non_strict:
136+
self.assertEqual(wsgi_request.META['PATH_INFO'],
137+
sample_request['url'])
138+
if 'method' not in non_strict:
139+
self.assertEqual(wsgi_request.META['REQUEST_METHOD'],
140+
sample_request['method'])
117141

118-
if 'headers' in sample_request:
142+
if 'headers' in sample_request and 'headers' not in non_strict:
119143
self.assert_headers_contain(
120144
wsgi_request.META, sample_request['headers']
121145
)
122146

123-
if 'body' in sample_request:
147+
if 'body' in sample_request and 'body' not in non_strict:
124148
self.assert_data_equal(wsgi_request.POST, sample_request['body'])
125149

126-
def assert_matches_response(self, sample_response, wsgi_response):
150+
def assert_matches_response(self, sample_response, wsgi_response,
151+
non_strict=None):
127152
"""
128153
Check that the sample response and wsgi response match.
129154
"""
155+
non_strict = non_strict or []
130156
self.assertEqual(wsgi_response.status_code, sample_response.status)
131157
if 'body' in sample_response:
132158
response_parsed = wsgi_response.data
133-
self.assert_data_equal(response_parsed, sample_response.body)
159+
self.assert_data_equal(
160+
response_parsed, sample_response.body, non_strict)
134161

135-
def assert_matches_sample(self, path, label, response):
162+
def assert_matches_sample(
163+
self, path, label, response, non_strict_response=None,
164+
non_strict_request=None
165+
):
136166
"""
137167
Check a URL and response against a sample.
138168
@@ -143,10 +173,19 @@ def assert_matches_sample(self, path, label, response):
143173
:param response:
144174
The actual API response we want to match with the sample.
145175
It is assumed to be a Django Rest Framework response object
176+
:param non_strict:
177+
List of fields that will not be checked for strict matching.
178+
You can use this to include server-generated fields whose exact
179+
value you don't care about in your test, like ids, dates, etc.
146180
"""
181+
non_strict_response = non_strict_response or []
182+
non_strict_request = non_strict_request or []
147183
sample = self.load_sample(path)
148184
sample_request = sample.examples[label].request
149185
sample_response = sample.examples[label].response
150186

151-
self.assert_matches_response(sample_response, response)
152-
self.assert_matches_request(sample_request, response.wsgi_request)
187+
self.assert_matches_response(
188+
sample_response, response, non_strict=non_strict_response)
189+
self.assert_matches_request(
190+
sample_request, response.wsgi_request,
191+
non_strict=non_strict_request)

abe/utils.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,31 @@ def datetime_to_string(value):
1616
return representation
1717

1818

19-
def to_unicode(data):
19+
def normalize(data):
2020
"""
2121
Ensure that dates, Decimals and strings become unicode
22+
23+
Integers, on the other hand, are not converted.
2224
"""
2325
if isinstance(data, datetime):
2426
data = datetime_to_string(data)
25-
else:
27+
elif not isinstance(data, int):
2628
data = str(data)
2729

2830
if not _PY3 and isinstance(data, str):
2931
data = unicode(data)
3032
return data
33+
34+
35+
def subkeys(original, key):
36+
"""
37+
Takes a list of dot-hierarchical values and keeps only matching subkeys.
38+
39+
Example:
40+
41+
>>> subkeys(['one.two', 'one.three', 'four'], 'one')
42+
['two', 'three']
43+
"""
44+
new_keys = filter(lambda s: s.startswith(key + '.'), original)
45+
new_keys = list(map(lambda s: s[len(key) + 1:], new_keys))
46+
return new_keys

ep.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ dependencies:
44
version: ">=2.6.0"
55
file: test-requirements.txt
66
run:
7-
- flake8 abe
7+
- flake8 abe tests
88
- coverage erase
99
- coverage run --omit="*/tests/*","*$VIRTUAL_ENV*" -m unittest discover
1010
- coverage html
11+
- python -m doctest abe/utils.py
1112
publish:
1213
- git diff --exit-code
1314
- ep run

test-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22

33
flake8
44
coverage
5+
mock

0 commit comments

Comments
 (0)