/
helper_containers.feature
341 lines (293 loc) · 11.2 KB
/
helper_containers.feature
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
341
Feature: Per-suite helper containers
In order to share state and behaviour between contexts
developers need to have a way to create or register shared service container
Rules:
- A single optional container is allowed per suite
- Having container enables you to use its services as context arguments via `@name` syntax
- Container is rebuilt and is isolated between scenarios
- Container is configured via suite's `services` option
- Container is a class implementing `Interop\Container\ContainerInterface` or `Behat\Behat\HelperContainer\ServiceContainer`
- There is a built-in container if you need a very simple service-sharing, configurable through the same `services` setting
- There is an extension point that allows Behat extensions provide their own containers for end-users via `@name` syntax
Out of scope:
- Extensive service configuration and deep dependency trees support for built-in container. Behat is not your DIC framework
- Sharing scalar, non-object parameters using container. Use YAML anchors and references for configuration sharing
- Multiple containers per suite. Would introduce unnecessary complexity. Also, easily achievable manually through composition
- PSR-11 support. It was not accepted as a standard as of feature implementation. Support might be added later, subject to prioritisation
Usage:
- Use built-in container for simple dependency trees and when no DIC is used
- Use external container (ideally the one used for application itself) for deep dependency trees
- Use extension-provided containers when working with frameworks (if provided by framework)
Background:
Given a file named "features/container.feature" with:
"""
Feature:
Scenario:
Given service has no state
When service gets a state of 1 in first context
Then service should have a state of 1 in second context
Scenario:
Given service has no state
When service gets a state of 33 in first context
Then service should have a state of 33 in second context
"""
And a file named "features/bootstrap/FirstContext.php" with:
"""
<?php use Behat\Behat\Context\Context;
class FirstContext implements Context {
public function __construct(SharedService $service) {
$this->service = $service;
}
/** @Given service has no state */
public function noState() {
PHPUnit_Framework_Assert::assertNull($this->service->number);
}
/** @When service gets a state of :number in first context */
public function setState($number) {
$this->service->number = $number;
}
}
"""
And a file named "features/bootstrap/SecondContext.php" with:
"""
<?php use Behat\Behat\Context\Context;
class SecondContext implements Context {
public function __construct(SharedService $service) {
$this->service = $service;
}
/** @Then service should have a state of :number in second context */
public function checkState($number) {
PHPUnit_Framework_Assert::assertSame($number, $this->service->number);
}
}
"""
And a file named "features/bootstrap/SharedService.php" with:
"""
<?php
class SharedService {
public $number = null;
}
"""
And a file named "features/bootstrap/SharedServiceExpecting1.php" with:
"""
<?php
class SharedServiceExpecting1 extends SharedService {
public function __construct($arg) {
if (1 !== $arg) {
throw new \InvalidArgumentException();
}
}
}
"""
And a file named "features/bootstrap/SharedServiceWithFactory.php" with:
"""
<?php
class SharedServiceWithFactory extends SharedService {
private function __construct() {}
public static function factoryMethod() {
return new self();
}
}
"""
Scenario: External container
Given a file named "behat.yml" with:
"""
default:
suites:
default:
contexts:
- FirstContext:
- "@shared_service"
- SecondContext:
- "@shared_service"
services: MyContainer
"""
And a file named "features/bootstrap/MyContainer.php" with:
"""
<?php use Behat\Behat\HelperContainer\ServiceContainer;
class MyContainer implements ServiceContainer {
private $service;
public function has($id) {
return $id == 'shared_service';
}
public function get($id) {
if ($id !== 'shared_service') throw new \InvalidArgumentException();
return isset($this->service) ? $this->service : $this->service = new SharedService();
}
}
"""
When I run "behat --no-colors -f progress features/container.feature"
Then it should pass
Scenario: External container with a factory method
Given a file named "behat.yml" with:
"""
default:
suites:
default:
contexts:
- FirstContext:
- "@shared_service"
- SecondContext:
- "@shared_service"
services: MyContainer::factoryMethod
"""
And a file named "features/bootstrap/MyContainer.php" with:
"""
<?php use Behat\Behat\HelperContainer\ServiceContainer;
class MyContainer implements ServiceContainer {
private $service;
private function __construct() {}
public static function factoryMethod() {
return new self();
}
public function has($id) {
return $id == 'shared_service';
}
public function get($id) {
if ($id !== 'shared_service') throw new \InvalidArgumentException();
return isset($this->service) ? $this->service : $this->service = new SharedService();
}
}
"""
When I run "behat --no-colors -f progress features/container.feature"
Then it should pass
Scenario: Simplest built-in container configuration
Given a file named "behat.yml" with:
"""
default:
suites:
default:
contexts:
- FirstContext:
- "@shared_service"
- SecondContext:
- "@shared_service"
services:
shared_service: SharedService
"""
When I run "behat --no-colors -f progress features/container.feature"
Then it should pass
Scenario: Built-in container with service arguments
Given a file named "behat.yml" with:
"""
default:
suites:
default:
contexts:
- FirstContext:
- "@shared_service"
- SecondContext:
- "@shared_service"
services:
shared_service:
class: SharedServiceExpecting1
arguments:
- 1
"""
When I run "behat --no-colors -f progress features/container.feature"
Then it should pass
Scenario: Built-in container with factory-based services
Given a file named "behat.yml" with:
"""
default:
suites:
default:
contexts:
- FirstContext:
- "@shared_service"
- SecondContext:
- "@shared_service"
services:
shared_service:
class: SharedServiceWithFactory
factory_method: factoryMethod
"""
When I run "behat --no-colors -f progress features/container.feature"
Then it should pass
Scenario: External Interop container
Given a file named "behat.yml" with:
"""
default:
suites:
default:
contexts:
- FirstContext:
- "@shared_service"
- SecondContext:
- "@shared_service"
services: MyContainer
"""
And a file named "features/bootstrap/MyContainer.php" with:
"""
<?php use Interop\Container\ContainerInterface;
class MyContainer implements ContainerInterface {
private $service;
public function has($id) {
return $id == 'shared_service';
}
public function get($id) {
if ($id !== 'shared_service') throw new \InvalidArgumentException();
return isset($this->service) ? $this->service : $this->service = new SharedService();
}
}
"""
When I run "behat --no-colors -f progress features/container.feature"
Then it should pass
Scenario: Container provided by an extension
Given a file named "behat.yml" with:
"""
default:
suites:
default:
contexts:
- FirstContext:
- "@shared_service"
- SecondContext:
- "@shared_service"
services: "@my_extension.container"
extensions:
MyExtension.php: ~
"""
And a file named "features/bootstrap/MyContainer.php" with:
"""
<?php use Behat\Behat\HelperContainer\ServiceContainer;
class MyContainer implements ServiceContainer {
private $service;
public function has($id) {
return $id == 'shared_service';
}
public function get($id) {
if ($id !== 'shared_service') throw new \InvalidArgumentException();
return isset($this->service) ? $this->service : $this->service = new SharedService();
}
}
"""
And a file named "MyExtension.php" with:
"""
<?php
use Behat\Testwork\ServiceContainer\Extension;
use Behat\Testwork\ServiceContainer\ExtensionManager;
use Behat\Behat\HelperContainer\ServiceContainer\HelperContainerExtension;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class MyExtension implements Extension {
public function getConfigKey() { return 'container_provider'; }
public function configure(ArrayNodeDefinition $builder) { }
public function initialize(ExtensionManager $extensionManager) {}
public function process(ContainerBuilder $container) {}
public function load(ContainerBuilder $container, array $config) {
$definition = new Definition('MyContainer', array());
$definition->addTag(HelperContainerExtension::HELPER_CONTAINER_TAG);
if (method_exists($definition, 'setShared')) {
$definition->setShared(false); // <- Starting Symfony 2.8
} else {
$definition->setScope($container::SCOPE_PROTOTYPE); // <- Up to Symfony 2.8
}
$container->setDefinition('my_extension.container', $definition);
}
}
return new MyExtension;
"""
When I run "behat --no-colors -f progress features/container.feature"
Then it should pass