forked from pytest-dev/pytest-bdd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtest_common.py
340 lines (276 loc) · 10.9 KB
/
test_common.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
import textwrap
from typing import Any, Callable
from unittest import mock
import pytest
from pytest_bdd import given, parser, parsers, then, when
from pytest_bdd.utils import collect_dumped_objects
@pytest.mark.parametrize("step_fn, step_type", [(given, "given"), (when, "when"), (then, "then")])
def test_given_when_then_delegate_to_step(step_fn: Callable[..., Any], step_type: str) -> None:
"""Test that @given, @when, @then just delegate the work to @step(...).
This way we don't have to repeat integration tests for each step decorator.
"""
# Simple usage, just the step name
with mock.patch("pytest_bdd.steps.step", autospec=True) as step_mock:
step_fn("foo")
step_mock.assert_called_once_with("foo", type_=step_type, converters=None, target_fixture=None, stacklevel=1)
# Advanced usage: step parser, converters, target_fixture, ...
with mock.patch("pytest_bdd.steps.step", autospec=True) as step_mock:
parser = parsers.re(r"foo (?P<n>\d+)")
step_fn(parser, converters={"n": int}, target_fixture="foo_n", stacklevel=3)
step_mock.assert_called_once_with(
name=parser, type_=step_type, converters={"n": int}, target_fixture="foo_n", stacklevel=3
)
def test_step_function_multiple_target_fixtures(pytester):
pytester.makefile(
".feature",
target_fixture=textwrap.dedent(
"""\
Feature: Multiple target fixtures for step function
Scenario: A step can be decorated multiple times with different target fixtures
Given there is a foo with value "test foo"
And there is a bar with value "test bar"
Then foo should be "test foo"
And bar should be "test bar"
"""
),
)
pytester.makepyfile(
textwrap.dedent(
"""\
import pytest
from pytest_bdd import given, when, then, scenarios, parsers
from pytest_bdd.utils import dump_obj
scenarios("target_fixture.feature")
@given(parsers.parse('there is a foo with value "{value}"'), target_fixture="foo")
@given(parsers.parse('there is a bar with value "{value}"'), target_fixture="bar")
def _(value):
return value
@then(parsers.parse('foo should be "{expected_value}"'))
def _(foo, expected_value):
dump_obj(foo)
assert foo == expected_value
@then(parsers.parse('bar should be "{expected_value}"'))
def _(bar, expected_value):
dump_obj(bar)
assert bar == expected_value
"""
)
)
result = pytester.runpytest("-s")
result.assert_outcomes(passed=1)
[foo, bar] = collect_dumped_objects(result)
assert foo == "test foo"
assert bar == "test bar"
def test_step_functions_same_parser(pytester):
pytester.makefile(
".feature",
target_fixture=textwrap.dedent(
"""\
Feature: A feature
Scenario: A scenario
Given there is a foo with value "(?P<value>\\w+)"
And there is a foo with value "testfoo"
When pass
Then pass
"""
),
)
pytester.makepyfile(
textwrap.dedent(
"""\
import pytest
from pytest_bdd import given, when, then, scenarios, parsers
from pytest_bdd.utils import dump_obj
scenarios("target_fixture.feature")
STEP = r'there is a foo with value "(?P<value>\\w+)"'
@given(STEP)
def _():
dump_obj(('str',))
@given(parsers.re(STEP))
def _(value):
dump_obj(('re', value))
@when("pass")
@then("pass")
def _():
pass
"""
)
)
result = pytester.runpytest("-s")
result.assert_outcomes(passed=1)
[first_given, second_given] = collect_dumped_objects(result)
assert first_given == ("str",)
assert second_given == ("re", "testfoo")
def test_user_implements_a_step_generator(pytester):
"""Test advanced use cases, like the implementation of custom step generators."""
pytester.makefile(
".feature",
user_step_generator=textwrap.dedent(
"""\
Feature: A feature
Scenario: A scenario
Given I have 10 EUR
And the wallet is verified
And I have a wallet
When I pay 1 EUR
Then I should have 9 EUR in my wallet
"""
),
)
pytester.makepyfile(
textwrap.dedent(
"""\
import re
from dataclasses import dataclass, fields
import pytest
from pytest_bdd import given, when, then, scenarios, parsers
from pytest_bdd.utils import dump_obj
@dataclass
class Wallet:
verified: bool
amount_eur: int
amount_usd: int
amount_gbp: int
amount_jpy: int
def pay(self, amount: int, currency: str) -> None:
if not self.verified:
raise ValueError("Wallet account is not verified")
currency = currency.lower()
field = f"amount_{currency}"
setattr(self, field, getattr(self, field) - amount)
@pytest.fixture
def wallet__verified():
return False
@pytest.fixture
def wallet__amount_eur():
return 0
@pytest.fixture
def wallet__amount_usd():
return 0
@pytest.fixture
def wallet__amount_gbp():
return 0
@pytest.fixture
def wallet__amount_jpy():
return 0
@pytest.fixture()
def wallet(
wallet__verified,
wallet__amount_eur,
wallet__amount_usd,
wallet__amount_gbp,
wallet__amount_jpy,
):
return Wallet(
verified=wallet__verified,
amount_eur=wallet__amount_eur,
amount_usd=wallet__amount_usd,
amount_gbp=wallet__amount_gbp,
amount_jpy=wallet__amount_jpy,
)
def generate_wallet_steps(model_name="wallet", stacklevel=1):
stacklevel += 1
@given("I have a wallet", target_fixture=model_name, stacklevel=stacklevel)
def _(wallet):
return wallet
@given(
parsers.re(r"the wallet is (?P<negation>not)?verified"),
target_fixture=f"{model_name}__verified",
stacklevel=2,
)
def _(negation: str):
if negation:
return False
return True
# Generate steps for currency fields:
for field in fields(Wallet):
match = re.fullmatch(r"amount_(?P<currency>[a-z]{3})", field.name)
if not match:
continue
currency = match["currency"]
@given(
parsers.parse(f"I have {{value:d}} {currency.upper()}"),
target_fixture=f"{model_name}__amount_{currency}",
stacklevel=2,
)
def _(value: int, _currency=currency) -> int:
dump_obj(f"given {value} {_currency.upper()}")
return value
@when(
parsers.parse(f"I pay {{value:d}} {currency.upper()}"),
stacklevel=2,
)
def _(wallet: Wallet, value: int, _currency=currency) -> None:
dump_obj(f"pay {value} {_currency.upper()}")
wallet.pay(value, _currency)
@then(
parsers.parse(f"I should have {{value:d}} {currency.upper()} in my wallet"),
stacklevel=2,
)
def _(wallet: Wallet, value: int, _currency=currency) -> None:
dump_obj(f"assert {value} {_currency.upper()}")
assert getattr(wallet, f"amount_{_currency}") == value
generate_wallet_steps()
scenarios("user_step_generator.feature")
"""
)
)
result = pytester.runpytest("-s")
result.assert_outcomes(passed=1)
[given, pay, assert_] = collect_dumped_objects(result)
assert given == "given 10 EUR"
assert pay == "pay 1 EUR"
assert assert_ == "assert 9 EUR"
def test_step_catches_all(pytester):
"""Test that the @step(...) decorator works for all kind of steps."""
pytester.makefile(
".feature",
step_catches_all=textwrap.dedent(
"""\
Feature: A feature
Scenario: A scenario
Given foo
And foo parametrized 1
When foo
And foo parametrized 2
Then foo
And foo parametrized 3
"""
),
)
pytester.makepyfile(
textwrap.dedent(
"""\
import pytest
from pytest_bdd import step, scenarios, parsers
from pytest_bdd.utils import dump_obj
scenarios("step_catches_all.feature")
@step("foo")
def _():
dump_obj("foo")
@step(parsers.parse("foo parametrized {n:d}"))
def _(n):
dump_obj(("foo parametrized", n))
"""
)
)
result = pytester.runpytest("-s")
result.assert_outcomes(passed=1)
objects = collect_dumped_objects(result)
assert objects == ["foo", ("foo parametrized", 1), "foo", ("foo parametrized", 2), "foo", ("foo parametrized", 3)]
def test_step_name_is_cached():
"""Test that the step name is cached and not re-computed eache time."""
step = parser.Step(name="step name", type="given", indent=8, line_number=3, keyword="Given")
assert step.name == "step name"
# manipulate the step name directly and validate the cache value is still returned
step._name = "incorrect step name"
assert step.name == "step name"
# change the step name using the property and validate the cache has been invalidated
step.name = "new step name"
assert step.name == "new step name"
# manipulate the step lines and validate the cache value is still returned
step.lines.append("step line 1")
assert step.name == "new step name"
# add a step line and validate the cache has been invalidated
step.add_line("step line 2")
assert step.name == "new step name\nstep line 1\nstep line 2"