Skip to content

Commit 436e9f7

Browse files
authored
Merge pull request #179 from radish-bdd/feature/error-case-matching
Implement error-case sentence testing
2 parents 62f2db5 + 4fff6fb commit 436e9f7

File tree

4 files changed

+170
-35
lines changed

4 files changed

+170
-35
lines changed

docs/tutorial.rst

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,7 @@ sentences defined in a YAML file. We call those files **match configs**. A *matc
10361036
10371037
- sentence: <SOME STEP SENTENCE>
10381038
should_match: <THE STEP FUNCTION NAME IT SHOULD MATCH>
1039+
should_not_match: <THE STEP FUNCTION NAME IT SHOULD NOT MATCH>
10391040
with_arguments:
10401041
# argument check if implicit type
10411042
- <ARGUMENT 1 NAME>: <ARGUMENT 1 VALUE>
@@ -1055,10 +1056,13 @@ sentences defined in a YAML file. We call those files **match configs**. A *matc
10551056
It's an example of a sentence which should match
10561057
a certain Step pattern.
10571058
:should_match:
1058-
**Required**. This is the name of the python Step implementation
1059+
**Required if should_not_match omitted**. This is the name of the Python Step implementation
10591060
function which you expect the sentence will match with.
1061+
:should_not_match:
1062+
**Required if should_match omitted**. This is the name of a Python Step implementation
1063+
function which you expect the sentence will **not** match with.
10601064
:with_arguments:
1061-
**Optional**. This is a list of arguments which you expect
1065+
**Optional for should_match**. This is a list of arguments which you expect
10621066
will be passed in the python Step implementation function.
10631067
The arguments can be specified as key-value pairs or as an object
10641068
with a *type* and *value* and a boolean value *cast*.
@@ -1104,6 +1108,13 @@ And a ``step-matches.yml`` file like this:
11041108
- sentence: When I sum them
11051109
should_match: sum_numbers
11061110
1111+
- sentence: When I divide them
1112+
should_not_match: sum_numbers
1113+
1114+
- sentence: When I do some weird stuff
1115+
# if no step is given it shouldn't match any at all
1116+
should_not_match:
1117+
11071118
- sentence: Then I expect the result to be 8
11081119
should_match: expect_result
11091120
with_arguments:
@@ -1126,9 +1137,10 @@ For the ``radish-test`` call above we would get the following output:
11261137
Testing sentences from tests/step-matches.yml:
11271138
>> STEP "Given I have the number 5" SHOULD MATCH have_number ✔
11281139
>> STEP "When I sum them" SHOULD MATCH sum_numbers ✔
1140+
>> STEP "When I divide them" SHOULD NOT MATCH sum_numbers ✔
11291141
>> STEP "Then I expect the result to be 8" SHOULD MATCH expect_result ✔
11301142
1131-
3 sentences (3 passed)
1143+
4 sentences (4 passed)
11321144
Covered 3 of 3 step implementations
11331145
11341146
In case of success we get the exit code **0** and in case of failure we'd get an exit code which is greater than **0**.

radish/testing/matches.py

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def test_step_matches_configs(match_config_files, basedirs, cover_min_percentage
5858
failed += failed_sentences
5959
passed += passed_senteces
6060

61-
covered_steps = covered_steps.union(x['should_match'] for x in match_config)
61+
covered_steps = covered_steps.union(x['should_match'] for x in match_config if 'should_match' in x)
6262

6363
# newline
6464
sys.stdout.write('\n')
@@ -120,42 +120,66 @@ def test_step_matches(match_config, steps):
120120
validate_config_item(item)
121121

122122
sentence = item['sentence']
123-
expected_step = item['should_match']
124123

125-
sys.stdout.write('{0} STEP "{1}" SHOULD MATCH {2} '.format(
126-
colorful.yellow('>>'), colorful.cyan(sentence), colorful.cyan(expected_step)))
124+
if 'should_match' in item:
125+
has_passed = test_step_match(sentence, item['should_match'],
126+
item.get('with_arguments', None), steps)
127+
else:
128+
has_passed = test_step_not_match(sentence, item['should_not_match'], steps)
127129

128-
result = match_step(item['sentence'], steps)
129-
if not result:
130-
output_failure(None, ['Expected sentence didn\'t match any step implementation'])
130+
if has_passed:
131+
passed += 1
132+
else:
131133
failed += 1
132-
continue
133134

134-
if expected_step != result.func.__name__:
135-
output_failure(result.func, ['Expected sentence matched {0} instead of {1}'.format(result.func.__name__, expected_step)])
136-
failed += 1
137-
continue
135+
return failed, passed
138136

139137

140-
expected_arguments = item.get('with_arguments')
138+
def test_step_match(sentence, expected_step, expected_arguments, steps):
139+
sys.stdout.write('{0} STEP "{1}" SHOULD MATCH {2} '.format(
140+
colorful.yellow('>>'), colorful.cyan(sentence), colorful.cyan(expected_step)))
141141

142-
if expected_arguments:
143-
arguments = merge_step_args(result)
144-
expected_arguments = {k: v for expected_arguments in expected_arguments for k, v in expected_arguments.items()}
145-
argument_errors = check_step_arguments(expected_arguments, arguments)
146-
if argument_errors:
147-
output_failure(result.func, argument_errors)
148-
failed += 1
149-
continue
142+
result = match_step(sentence, steps)
143+
if not result:
144+
output_failure(None, ['Expected sentence didn\'t match any step implementation'])
145+
return False
150146

151-
# check if arguments match
152-
print(u(colorful.bold_green(u'✔')))
153-
passed += 1
147+
if expected_step != result.func.__name__:
148+
output_failure(result.func,
149+
['Expected sentence matched {0} instead of {1}'.format(
150+
result.func.__name__, expected_step)])
151+
return False
154152

155-
return failed, passed
153+
if expected_arguments:
154+
arguments = merge_step_args(result)
155+
expected_arguments = {k: v for expected_arguments in expected_arguments
156+
for k, v in expected_arguments.items()}
157+
argument_errors = check_step_arguments(expected_arguments, arguments)
158+
if argument_errors:
159+
output_failure(result.func, argument_errors)
160+
return False
161+
162+
print(u(colorful.bold_green(u'✔')))
163+
return True
164+
165+
166+
def test_step_not_match(sentence, expected_not_matching_step, steps):
167+
step_to_print = colorful.cyan(expected_not_matching_step) if expected_not_matching_step else 'ANY'
168+
sys.stdout.write('{0} STEP "{1}" SHOULD NOT MATCH {2} '.format(
169+
colorful.yellow('>>'), colorful.cyan(sentence), step_to_print))
170+
171+
result = match_step(sentence, steps)
172+
if result:
173+
if not expected_not_matching_step or result.func.__name__ == expected_not_matching_step:
174+
output_failure(None, ['Expected sentence did match {0} but it shouldn\'t'.format(
175+
expected_not_matching_step)])
176+
return False
177+
178+
print(u(colorful.bold_green(u'✔')))
179+
return True
156180

157181

158-
VALID_CONFIG_ITEMS = {'sentence', 'should_match', 'with_arguments'}
182+
VALID_CONFIG_ITEMS = {'sentence', 'should_match', 'should_not_match', 'with_arguments'}
159183

160184

161185
def validate_config_item(config):
@@ -167,8 +191,8 @@ def validate_config_item(config):
167191
raise ValueError('The config attributes {0} are invalid. Use only {1}'.format(
168192
', '.join(sorted(given_attributes.difference(VALID_CONFIG_ITEMS))), ', '.join(sorted(VALID_CONFIG_ITEMS))))
169193

170-
if 'sentence' not in config or 'should_match' not in config:
171-
raise ValueError('You have to provide a sentence and the function name which should be matched (should_match)')
194+
if 'sentence' not in config or ('should_match' not in config and 'should_not_match' not in config):
195+
raise ValueError('You have to provide a sentence and the function name which should (not) be matched (should_match, should_not_match)')
172196

173197

174198
def output_failure(step_func, errors):

tests/exploratory/basic/tests/radish-matches.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99
- sentence: When I sum them
1010
should_match: sum_numbers
1111

12+
- sentence: When I sum them
13+
should_not_match: have_number
14+
15+
- sentence: When I divide them
16+
should_not_match:
17+
18+
- sentence: When I divide them
19+
should_not_match: sum_numbers
20+
1221
- sentence: Then I expect the result to be 8
1322
should_match: expect_result
1423
with_arguments:

tests/unit/testing/test_matches.py

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,13 @@ def test_empty_matches_config(mocker, capsys):
8989

9090
@pytest.mark.parametrize('given_invalid_config, expected_error_msg', [
9191
([{'sentence': None}],
92-
'You have to provide a sentence and the function name which should be matched (should_match)'),
92+
'You have to provide a sentence and the function name which should (not) be matched (should_match, should_not_match)'),
9393
([{'should_match': None}],
94-
'You have to provide a sentence and the function name which should be matched (should_match)'),
94+
'You have to provide a sentence and the function name which should (not) be matched (should_match, should_not_match)'),
9595
([{}],
96-
'You have to provide a sentence and the function name which should be matched (should_match)'),
96+
'You have to provide a sentence and the function name which should (not) be matched (should_match, should_not_match)'),
9797
([{'foo': None}],
98-
'The config attributes foo are invalid. Use only sentence, should_match, with_arguments')
98+
'The config attributes foo are invalid. Use only sentence, should_match, should_not_match, with_arguments')
9999
], ids=[
100100
'test config with missing should_match function attribute',
101101
'test config with missing sentence attribute',
@@ -183,6 +183,96 @@ def foo(step, foo, bar): pass
183183
assert actual_returncode == expected_returncode
184184

185185

186+
def test_sentence_not_match(capsys):
187+
"""
188+
Test if sentence does not match if that's expected
189+
"""
190+
# given
191+
def foo(step, foo, bar): pass
192+
193+
steps = {re.compile(r'What (.*?) can (.*)'): foo}
194+
config = [{
195+
'sentence': 'foo', 'should_not_match': 'foo'
196+
}]
197+
expected_returncode = (0, 1)
198+
199+
# when
200+
actual_returncode = matches.test_step_matches(config, steps)
201+
out, _ = capsys.readouterr()
202+
203+
# then
204+
assert actual_returncode == expected_returncode
205+
206+
207+
def test_sentence_not_match_anything(capsys):
208+
"""
209+
Test if sentence does not match any steps
210+
"""
211+
# given
212+
def foo(step, foo, bar): pass
213+
214+
steps = {re.compile(r'What (.*?) can (.*)'): foo}
215+
config = [{
216+
'sentence': 'foo', 'should_not_match': ''
217+
}]
218+
expected_returncode = (0, 1)
219+
220+
# when
221+
actual_returncode = matches.test_step_matches(config, steps)
222+
out, _ = capsys.readouterr()
223+
224+
# then
225+
assert actual_returncode == expected_returncode
226+
227+
228+
def test_sentence_not_match_specific_step(capsys):
229+
"""
230+
Test if sentence does not match specific step
231+
"""
232+
# given
233+
def foo(step): pass
234+
235+
def bar(step): pass
236+
237+
steps = {
238+
re.compile(r'foo'): foo,
239+
re.compile(r'bar'): bar,
240+
}
241+
config = [{
242+
'sentence': 'foo', 'should_not_match': 'bar'
243+
}]
244+
expected_returncode = (0, 1)
245+
246+
# when
247+
actual_returncode = matches.test_step_matches(config, steps)
248+
out, _ = capsys.readouterr()
249+
250+
# then
251+
assert actual_returncode == expected_returncode
252+
253+
254+
def test_sentence_not_match_but_does(capsys):
255+
"""
256+
Test if sentence matched step but shouldn't
257+
"""
258+
# given
259+
def foo(): pass
260+
261+
steps = {'foo': foo}
262+
config = [{
263+
'sentence': 'foo', 'should_not_match': 'foo'
264+
}]
265+
expected_returncode = (1, 0)
266+
267+
# when
268+
actual_returncode = matches.test_step_matches(config, steps)
269+
out, _ = capsys.readouterr()
270+
271+
# then
272+
assert 'Expected sentence did match foo but it shouldn\'t' in out
273+
assert actual_returncode == expected_returncode
274+
275+
186276
@pytest.mark.parametrize('given_actual_arguments, expected_messages', [
187277
(
188278
{'FOO': None},

0 commit comments

Comments
 (0)