-
Notifications
You must be signed in to change notification settings - Fork 42
/
2016-07-19-behat-structure-functional-tests.html
293 lines (271 loc) · 14.2 KB
/
2016-07-19-behat-structure-functional-tests.html
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
---
layout: post
title: 'Behat: structure your functional tests'
author: vcomposieux
date: '2016-07-19 14:15:31 +0200'
date_gmt: '2016-07-19 12:15:31 +0200'
categories:
- Non classé
tags: []
---
{% raw %}
<p><strong>Introduction</strong></p>
<p>In order to ensure that your application is running well, it's important to write functional tests.<br />
Behat is the most used tool with Symfony to handle your functional tests and that's great because it's really a complete suite.<br />
You should nevertheless know how to use it wisely in order to cover useful and complete test cases and that's the goal of this blog post.</p>
<p> </p>
<p><strong>Functional testing: what's that?</strong></p>
<p>When we are talking about functional testing we often mean that we want to automatize human-testing scenarios over the application.</p>
<p>However, it is important to write the following test types to cover the functional scope:</p>
<ul>
<li><strong>Interface tests</strong>: Goal here is to realize interface verifications to ensure that our web application features can be used over a browser,</li>
<li><strong>Integration tests</strong>: Goal of these tests is to ensure that sour code (already unit-tested) which makes the application running is acting as it should when all components are linked together.</li>
</ul>
<p>Idea is to develop and run both integration tests and interface tests with Behat.</p>
<p>Before we can go, please note that we will use a <strong>Selenium</strong> server which will receive orders by <strong>Mink</strong> (a Behat extension) and will pilot our browser (Chrome in our configuration).</p>
<p>To be clear on the architecture we will use, here is a scheme that will resume the role of all elements:</p>
<p>[caption id="attachment_1997" align="alignnone" width="781"]<a href="http://blog.eleven-labs.com/wp-content/uploads/2016/07/behat_en.jpg"><img class="wp-image-1997 size-full" src="http://blog.eleven-labs.com/wp-content/uploads/2016/07/behat_en.jpg" alt="Behat architecture schema" width="781" height="251" /></a> Behat architecture scheme[/caption]</p>
<p> </p>
<p><strong>Behat set up</strong></p>
<p>First step is to install Behat and its extensions as dependencies in our <strong>composer.json</strong> file:</p>
<pre class="theme:github lang:js decode:true">"require-dev": {
"behat/behat": "~3.1",
"behat/symfony2-extension": "~2.1",
"behat/mink": "~1.7",
"behat/mink-extension": "~2.2",
"behat/mink-selenium2-driver": "~1.3",
"emuse/behat-html-formatter": "dev-master"
}</pre>
<p>In order to make your future contexts autoloaded, you also have to add this little <strong>PSR-4</strong> section:</p>
<pre class="theme:github lang:yaml decode:true ">"autoload-dev": {
"psr-4": {
"Acme\Tests\Behat\Context\": "features/context/"
}
}</pre>
<p>Now, let's create our <strong>behat.yml</strong> file in our project root directory in order to define our tests execution.</p>
<p>Here is the configuration file we will start with:</p>
<pre class="theme:github lang:yaml decode:true">default:
suites: ~
extensions:
Behat\Symfony2Extension: ~
Behat\MinkExtension:
base_url: "http://acme.tld/"
selenium2:
browser: chrome
wd_host: 'http://selenium-host:4444/wd/hub'
default_session: selenium2
emuse\BehatHTMLFormatter\BehatHTMLFormatterExtension:
name: html
renderer: Twig,Behat2
file_name: index
print_args: true
print_outp: true
loop_break: true
formatters:
pretty: ~
html:
output_path: %paths.base%/web/reports/behat</pre>
<p>We will talk of all of these sections in their defined order so let's start with the <strong>suites</strong> section which is empty at this time but we will implement it later when we will have some contexts to add into it.</p>
<p>Then, we load some Behat extensions:</p>
<ul>
<li><strong>Behat\Symfony2Extension</strong> will allow us to inject Symfony services into our contexts (useful for integrations tests mostly),</li>
<li><strong>Behat\MinkExtension</strong> will allow us to pilot Selenium (drive itself the Chrome browser) so we fill in all the necessary information like the hostname, the Selenium server port number and the base URL we will use for testing,</li>
<li><strong>emuse\BehatHTMLFormatter\BehatHTMLFormatterExtension</strong> will generate a HTML report during tests execution (which is great to show to our customer for instance).</li>
</ul>
<p>Finally, in the <strong>formatters</strong> section we keep the <strong>pretty</strong> formatter in order to keep an output in our terminal and the HTML reports will be generated at the same time in the <strong>web/reports/behat</strong> directory in order to make them available over HTTP (it should not be a problem as you should not execute functional tests in production, be careful to restrict access in this case).</p>
<p>Now that Behat is ready and configured we will prepare our functional tests that we will split into two distinct Behat suites: <strong>integration</strong> and <strong>interface</strong>.</p>
<p> </p>
<p><strong>Writing functional tests (features)</strong></p>
<p>In our example, we will write tests in order to ensure that a new user can register over a registration page.</p>
<p>We will have to start by writing our tests scenarios (in a <strong>.feature</strong> file) that we will put into a <strong>features/</strong> directory located at the project root directory.</p>
<p>So for instance, we will have the following scenario:</p>
<p><span style="text-decoration: underline;">File</span>: <strong>features/registration/register.feature</strong>:</p>
<p> </p>
<pre class="theme:github lang:default decode:true">Feature: Register
In order to create an account
As a user
I want to be able to register on the application
Scenario: I register when I fill my username and password only
Given I am on the registration page
And I register with username "johndoe" and password "azerty123"
When I submit the form
Then I should see the registration confirmation</pre>
<p> </p>
<p><strong>Integration tests</strong></p>
<p>As said previously, these tests are here to ensure all code written for the registration page can be executed and linked without any errors.</p>
<p>To do so, we will create a new integration context that concerns the registration part under directory <strong>features/context/registration</strong>:</p>
<p><span style="text-decoration: underline;">File</span>: <strong>features/context/registration/IntegrationRegisterContext</strong>:</p>
<pre class="theme:github lang:php decode:true "><?php
namespace Acme\Tests\Behat\Context\Registration;
use Acme\AppBundle\Entity\User;
use Acme\AppBundle\Registration\Registerer;
use Behat\Behat\Context\Context;
/**
* Integration register context.
*/
class IntegrationRegisterContext implements Context
{
/**
* Registerer
*/
protected $registerer;
/**
* User
*/
protected $user;
/**
* boolean
*/
protected $response;
/**
* Constructor.
*
* @param Registerer $registerer
*/
public function __construct(Registerer $registerer)
{
$this->registerer = $registerer;
}
/**
* @Given I am on the registration page
*/
public function iAmOnTheRegistrationPage()
{
$this->user = new User();
}
/**
* @Given /I register with username "(?P<username>[^"]*)" and password "(?P<password>[^"]*)"/
*/
public function iRegisterWithUsernameAndPassword($username, $password)
{
$this->user->setUsername($username);
$this->user->setPassword($password);
}
/**
* @When I submit the form
*/
public function iSubmitTheForm()
{
$this->response = $this->registerer->register($this->user);
}
/**
* @Then I should see the registration confirmation message
*/
public function iShouldSeeTheRegistrationConfirmation()
{
if (!$this->response) {
throw new \RuntimeException('User is not registered.');
}
}
}</pre>
<p>Integration test for this part is now done for our feature. Let's write the interface test now!</p>
<p> </p>
<p><strong>Interface tests</strong></p>
<p>This test will be based on the same feature file without modifying the original written scenarios we wrote at the beginning. That's why it is important to write a generic test that can be implemented both in an integration test and in an interface test.</p>
<p>So let's create that context that will be used for interface test (prefixed by Mink in our case, but you can prefix it by anything you want) under the directory <strong>features/context/registration</strong>.</p>
<p><span style="text-decoration: underline;">File</span>: <strong>features/context/registration/MinkRegisterContext</strong>:</p>
<p> </p>
<pre class="theme:github lang:php decode:true "><?php
namespace Acme\Tests\Behat\Context\Registration;
use Acme\AppBundle\Entity\User;
use Acme\AppBundle\Registration\Registerer;
use Behat\Behat\Context\Context;
use Behat\MinkExtension\Context\MinkContext;
/**
* Mink register context.
*/
class MinkRegisterContext extends MinkContext
{
/**
* @Given I am on the registration page
*/
public function iAmOnTheRegistrationPage()
{
$this->visit('/register');
}
/**
* @Given /I register with username "(?P<username>[^"]*)" and password "(?P<password>[^"]*)"/
*/
public function iRegisterWithUsernameAndPassword($username, $password)
{
$this->fillField('registration[username]', $username);
$this->fillField('registration[password]', $password);
}
/**
* @When I submit the form
*/
public function iSubmitTheForm()
{
$this->pressButton('Register');
}
/**
* @Then I should see the registration confirmation message
*/
public function iShouldSeeTheRegistrationConfirmation()
{
$this->assertPageContainsText('Congratulations, you are now registered!');
}
}</pre>
<p>We just implemented an interface test based on the same scenario that the one we used for integration test so this class has exactly the same four methods with the same Behat annotations that we have implemented in our integration test class.</p>
<p>The only difference here is that in this context we ask Mink to ask to Selenium to do actions on the interface of our application by executing a browser instead of testing the code itself.</p>
<p> </p>
<p><strong>Context definition</strong></p>
<p>One more thing now, we have to add previously created contexts in our <strong>suites</strong> section in the <strong>behat.yml</strong> configuration file.</p>
<pre class="theme:github lang:yaml decode:true "> suites:
integration:
paths:
- %paths.base%/features/registration
contexts:
- Acme\Tests\Behat\Context\Registration\IntegrationRegisterContext:
- "@acme.registration.registerer"
interface:
paths:
- %paths.base%/features/registration
contexts:
- Behat\MinkExtension\Context\MinkContext: []
- Acme\Tests\Behat\Context\Registration\MinkRegisterContext: []</pre>
<p>It is important to see here that we can clearly split these kind of tests into two distinct parts <strong>integration</strong> and <strong>interface</strong>: each one will be executed with its own contexts.</p>
<p>Also, as we have loaded the Symfony2 extension during the Behat set up, we have the possibility to inject Symfony services in our contexts and that case occurs here with the <strong>acme.registration.registerer</strong> service.</p>
<p> </p>
<p><strong>Tests execution</strong></p>
<p>In order to run all tests, simply execute in the project root directory: <strong>bin/behat -c behat.yml</strong>.</p>
<p>If you want to run the integration tests only: <strong>bin/behat -c behat.yml --suite=integration</strong>.</p>
<p>HTML report will be generated under the <strong>web/reports/behat/</strong> as specified in the configuration that will allow you to have a quick overview of failed tests which is cool when you have a lot of tests.</p>
<p> </p>
<p><strong>Link multiple contexts together</strong></p>
<p>At last, sometime you could need information from another context. For instance, imagine that you have a second step just after the register step. You will have to create two new <strong>IntegrationProfileContext</strong> and <strong>MinkProfileContext</strong> contexts.<br />
We will only talk about integration context in the following to simplify understanding.<br />
In this new step <strong>IntegrationProfileContext</strong>, you need some information obtained in the first step <strong>IntegrationRegisterContext</strong>.</p>
<p>This can be achieved thanks to the <strong>@BeforeScenario</strong> Behat annotation.</p>
<p><span style="text-decoration: underline;">File</span>: <strong>features/context/registration/IntegrationProfileContext</strong>:</p>
<p> </p>
<pre class="theme:github lang:php decode:true "><?php
namespace Acme\Tests\Behat\Context\Registration;
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
/**
* Integration registration profile context.
*/
class IntegrationProfileContext implements Context
{
/**
* IntegrationRegisterContext
*/
protected $registerContext;
/**
* @BeforeScenario
*/
public function gatherContexts(BeforeScenarioScope $scope)
{
$environment = $scope->getEnvironment();
$this->registerContext = $environment->getContext(
'Acme\Tests\Behat\Context\Registration\IntegrationRegisterContext'
);
}
}</pre>
<p>You now have an accessible property <strong>$registerContext</strong> and can access informations from this context.</p>
<p> </p>
<p><strong>Conclusion</strong></p>
<p>Everything starts from well-written tests which have to be thoughtful in order to allow a technical implementation on both integration tests and interface tests.<br />
The choosen structure about classifying its tests is also important in order to quickly find tests when the application grows.</p>
{% endraw %}