Nate.Vegas

Learn PHP

Behavior-Driven Development With Behat

Behavior-Driven Development With Behat and Mink

Unit testing is not the only practice software engineers should use to ensure a project is operating correctly. Behavior-driven development has been growing in popularity since 2008. In short, BDD starts with a “story.” Whoever is in charge of designing the software’s flow writes use-cases for features in human-readable language. Tests are then run using headless browsers and other automation tools to ensure the story can be execute properly.

A short story in Gherkin, Behat’s markup language, might look like this:

1
2
3
4
5
Scenario: Search for red hat
    Given I am on "/search"
    And I fill in "search_input" with "red hat"
    And press "submit"
    Then I should see "red hat - The best clothing item you will ever buy."

This story looks eerily similar to a functional requirements document because, well, that’s what it is. Typically someone doing quality assurance would have to manually follow this scenario every time a new feature or bug fix has been implemented in order to properly regression test. But Behat can automatically test these scenario in a variety of browsers, headless or not. This is great for testing complex features like those involving AJAX requests. Mink defines how Behat should translate the Gherkin steps into browser commands in order to test the site. We can also define our own custom steps here.

Let’s take a look at how we can implement Behat to test an e-commerce site.

Set Up

Clone the simple e-commerce site and checkout the shop-basic tag. This post assumes you are working in either OSX or Linux.

1
2
3
4
5
6
git clone https://github.com/nater1067/Shop-Top.git
cd Shop-Top
git checkout -b shop-basic
curl -sS https://getcomposer.org/installer | php
php composer.phar install
vagrant up

Press enter after the composer install for each field to keep default values. Now you should be able to navigate to http://127.0.0.1:8080/search to see a working ecommerce site. Try buying a red hat to see how the site works. (Note: You will not be charged for the any red hats on this site.)

Including Behat and Mink Libraries

We need to add a few dependencies to our project using composer. This took a little time to figure out, and at the time of writing this post works. However be warned - you may need to adjust the version numbers in order to get this working in the future.

1
2
3
4
5
6
7
8
9
10
11
12
13
# composer.json
{
    # When you see "..." it means there may be code here.
    # Don't delete anything unless it's mentioned in a post.
    # ...
    "require": {
        # ...
        "behat/symfony2-extension": "~1.1",
        "behat/mink-extension": "~1.3",
        "behat/mink-selenium2-driver": "~1.1",
        "behat/mink-goutte-driver": "~1.0"
    }
}

Run composer update to download the the Behat and Mink libraries. Then create the necessary Behat files.

1
2
php composer.phar update
bin/behat --init

This creates a new directory, “features”, which holds our Gherkin and Behat FeatureContext. Modify the FeatureContext to extend MinkContext. This allows us to leverage the pre-defined steps included in Mink.

1
2
3
4
5
6
7
# features/bootstrap/FeatureContext.php
use Behat\MinkExtension\Context\MinkContext;

class FeatureContext extends MinkContext
{
    /* ... */
}

We should also create a new file in the project root directory - behat.yml.

1
2
3
4
5
6
7
# behat.yml
default:
    extensions:
        Behat\MinkExtension\Extension:
            base_url: http://127.0.0.1:8080
            goutte: ~
            selenium2: ~

Creating the Gherkin Story

Time to write our features in human terms. Create two new files: features/search.feature and features/buy.feature.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# features/search.feature
Feature: Clothes Search
  In order to search for clothes
  As a shopper
  I need to enter a search query and press submit

  Scenario: Search for red hat
    Given I am on "/search"
    And I fill in "search_input" with "red hat"
    And press "submit"
    Then I should see "red hat - The best clothing item you will ever buy."

  Scenario: Search for empty string
    Given I am on "/search"
    And press "submit"
    Then I should not see "Results"
1
2
3
4
5
6
7
8
9
10
11
12
# features/buy.feature
Feature: Buy Clothing Item
  In order to buy a clothing item
  As a shopper
  I need to press buy next to an item

  Scenario: Buy red hat
    Given I am on "/search"
    And I fill in "search_input" with "red hat"
    And I press "submit"
    Then I press "buy_redhat"
    Then I should see "You purchased 1 red hat!"

These files describe, in fairly simple terms, what the application should do. We should be able to search for a red hat, and see the description in the results.

Running Behat

Now that we have created a couple of Gherkin stories, we can run Behat to test our site’s behavior in a headless browser.

1
bin/behat

Behat is now using a headless browser to confirm whether our app adheres to the steps we’ve defined in the .feature files. We should see our stories outputted to the terminal. If everything works we should see a lot of green, and a summary of successful scenarios and steps.

The completed source code can be found on the behat branch of the Shop-Top repository on GitHub here: https://github.com/nater1067/Shop-Top/tree/behat .

What Comes Next

Now that you’ve got Behat running successfully within a Symfony 2 application, it’s time to get your hands dirty with more advanced test behavior-driven development. You’ve seen very simple examples of Behat tests, but Behat can do so much more than check simple forms. Try adding bundle-specific features with the Symfony 2 Behat bundle. Add some tests which check front-end functionality such as AJAX features. Tons of information can be found in the Behat docs at http://docs.behat.org .

Have an improvement for this post? What additional Behat functionality would you like included in this post? Please comment below.

Sexy URLS for Vagrant With FoxyProxy

Use Foxy Proxy to make your life easier (Mac OSX/ Linux)

If you’re like me and you don’t like seeing an ugly IP address during development, you can use FoxyProxy to easily change the http://127.0.0.1:8080 to vagrant.dev or any url of your choice.

Start off with either a Vagrant project of your own, or this basic Symfony 2 Vagrant project here: https://github.com/nater1067/Vagrant-Symfony-2 . Clone the project and follow the installation instructions. Run “vagrant up”.

Now install FoxyProxy. Firefox Users: https://addons.mozilla.org/en-US/firefox/addon/foxyproxy-standard/ Chrome Users: https://chrome.google.com/webstore/detail/foxyproxy-standard/gcknhkkoolaabfmlnjonogaaifnjlfnp?hl=en

After installing you should see a little fox somewhere on your browser. Click that fox. Make sure “Use proxies based on their pre-defined patterns and priorities” is selected.

images

Click on the new fox and select “Options”. Add a new proxy for 127.0.0.1 port 8080.

images

Then add a new url pattern. Set “symfony.dev” as the url pattern, optionally set a pattern name, make sure whitelist urls and wildcards are selected. Press save, exit foxy proxy and navigate to http://symfony.dev.

images

Spinning Up Symfony 2 Development Environments With Vagrant

How it’s gonna go

  1. Go over basics of Puppet and Vagrant
  2. Use Composer to grab symfony-standard-edition
  3. Create the VagrantFile
  4. Provisioning a Symfony 2 environment using Puppet
  5. Vagrant Up

Vagrant + Puppet Basics

For the purpose of this tutorial there are only a few things you need to know about Vagrant and Puppet. Vagrant creates virtual machines using a number of different providers. We’ll be using Virtualbox. Vagrant runs Puppet for you on the newly created virtual machine. Puppet handles the installation of various dependencies. (Think Apache, mysql, curl, etc.) This process is known as provisioning.

Why encapsulate development environments with Vagrant?

When we use Vagrant to create new virtual development environments we avoid the very real possibility that we could mess up our personal development machines. People have used virtual machines for development for years. The drawbacks of passing around USB sticks with Virtualbox images are easy to spot. For one, without a complicated and slow shared folder setup, you need to use and IDE or text editor inside the VM to modify code. You won’t be able to run this environment headless to free up clock cycles on your host machine. Managing installed applications across a teams VMs is a pain. Why not just include a Vagrantfile and a few Puppet manifests instead? Instead of passing around a virtual machine a few gigabytes in size, just include your Vagrant and Puppet in a project’s source control. That’s it. In future tutorials we will be using the environment we create here to start a new virtual machine running Symfony 2 with the above command. See vagrantup.com for more reasons why Vagrant is awesome.

Create a Symfony Project with Composer

Let’s have Composer install a Symfony 2 project in a directory, let’s call the directory symfony2. Open up a terminal window and enter the following commands:

1
2
3
curl -s https://getcomposer.org/installer | php
php composer.phar create-project symfony/framework-standard-edition symfony2 'dev-master'
cd symfony2

At the end of the composer create-project command, you should be prompted with “Some parameters are missing. Please provide them.”

Let’s install the AcmeDemoBundle so we can see the welcome page when we’re finished. When prompted for “Would you like to install Acme demo bundle? [y/N]” type y and press enter.

Go ahead and press enter for each of the remaining fields to leave the default values.

Because we are running a development environment let’s modify two lines in the web/.htaccess file. Replace occurrences of app with app_dev in these two location:

1
2
3
# /web/.htaccess
RewriteRule ^app_dev.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L]
RewriteRule .? %{ENV:BASE}/app_dev.php [L]

While we are in the new Symfony project, let’s disable some security checks in the app_dev.php file. Comment out the following lines:

1
2
3
# /web/app_dev.php
// header('HTTP/1.0 403 Forbidden');
// exit('You are not allowed to access this file. Check '.basename(FILE).' for more information.');

We have Symfony 2, but we’re not going to be able to develop in this awesome framework until it’s running on Apache. Let’s have Vagrant do the heavy-lifting for this.

Setting up Vagrant

Vagrant is powerful out of the box, and there’s not a whole lot of configuration we have to do ourselves. Make sure you have the latest version of Vagrant installed. See http://www.vagrantup.com/downloads.html for installation packages. Open the project directory in another terminal.

1
vagrant init hashicorp/precise32

Then open up the newly created Vagrantfile in your favorite text editor (use PHPStorm!) and enable Puppet provisioning. Also, ensure port 8080 is forwarded to 80 on the VM. Vagrant is now ready to go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# /Vagrantfile
VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "hashicorp/precise32"
  config.vm.network "forwarded_port", guest: 80, host: 8080

  # Enable provisioning with Puppet stand alone.  Puppet manifests
  # are contained in a directory path relative to this Vagrantfile.
  # You will need to create the manifests directory and a manifest in
  # the file default.pp in the manifests_path directory.
  #
  config.vm.provision "puppet" do |puppet|
    puppet.manifests_path = "manifests"
    puppet.manifest_file  = "site.pp"
  end

end

Provisioning with Puppet / Symfony’s Requirements

Let’s face it.. Symfony 2 is amazing and all powerful, but not the fastest framework to get started with. Let’s eliminate the need for manual intervention when setting up a new symfony application. In order to get our symfony application running we need to ensure 3 things happen:

Install our dependencies

Since we are keeping this Puppet script very basic for now, all we need to to install Apache, php, modrewrite and mysql. Create a folder called manifests in our project and a file named sites.pp in the new folder.

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
# /manifests/site.pp
exec {"apt-get update":
  path => "/usr/bin",
}

package {"apache2":
  ensure => present,
  require => Exec["apt-get update"],
}

service { "apache2":
  ensure => "running",
  require => Package["apache2"]
}

package {['mysql-server', 'mysql-client']:
  ensure => installed,
  require => Exec["apt-get update"]
}

service { 'mysql':
  ensure  => running,
  require => Package['mysql-server'],
}

package { ["php5-common", "libapache2-mod-php5", "php5-cli", "php-apc", "php5-mysql"]:
  ensure => installed,
  notify => Service["apache2"],
  require => [Exec["apt-get update"], Package['mysql-client'], Package['apache2']],
}

exec { "/usr/sbin/a2enmod rewrite" :
  unless => "/bin/readlink -e /etc/apache2/mods-enabled/rewrite.load",
  notify => Service[apache2],
  require => Package['apache2']
}

Set up a new VirtualHost

First we need to point /var/www to our /vagrant directory.
1
2
3
4
5
6
7
8
9
# /manifests/site.pp
file {"/var/www":
  ensure => "link",
  target => "/vagrant",
  require => Package["apache2"],
  notify => Service["apache2"],
  replace => yes,
  force => true,
}

Let’s set up a simple vhost pointing to our symfony project’s web directory. We should include the vhost in our git repository. Let’s place a file named vhost.conf in the manifests/assets folder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# /manifests/assets/vhost.conf
<VirtualHost *:80>
    ServerName symfony.dev
    DocumentRoot /vagrant/web
    <Directory /vagrant/web>
        # enable the .htaccess rewrites
        AllowOverride All
        Order allow,deny
        Allow from All
    </Directory>

    ErrorLog /var/log/apache2/error.log
    CustomLog /var/log/apache2/access.log combined
</VirtualHost>

Now we just have to tell Puppet to create a symlink to that file in the Apache sites-available directory.

1
2
3
4
5
6
7
8
9
# /manifests/site.pp
file { "/etc/apache2/sites-available/default":
  ensure => "link",
  target => "/vagrant/manifests/assets/vhost.conf",
  require => Package["apache2"],
  notify => Service["apache2"],
  replace => yes,
  force => true,
}

Set Apache to run as the Vagrant user

Because this is a development-only environment we can use a little hack I stumbled upon on Jeremy Kendall’s blog (http://jeremykendall.net/2013/08/09/vagrant-synced-folders-permissions/). All we need to do is change the Apache user and group to use Vagrant’s user and group. This way Apache can write to cache and log directories with no problem!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# /manifests/site.pp
exec { "ApacheUserChange" :
  command => "/bin/sed -i 's/APACHE_RUN_USER=www-data/APACHE_RUN_USER=vagrant/' /etc/apache2/envvars",
  onlyif  => "/bin/grep -c 'APACHE_RUN_USER=www-data' /etc/apache2/envvars",
  require => Package["apache2"],
  notify  => Service["apache2"],
}

exec { "ApacheGroupChange" :
  command => "/bin/sed -i 's/APACHE_RUN_GROUP=www-data/APACHE_RUN_GROUP=vagrant/' /etc/apache2/envvars",
  onlyif  => "/bin/grep -c 'APACHE_RUN_GROUP=www-data' /etc/apache2/envvars",
  require => Package["apache2"],
  notify  => Service["apache2"],
}

exec { "apache_lockfile_permissions" :
  command => "/bin/chown -R vagrant:www-data /var/lock/apache2",
  require => Package["apache2"],
  notify  => Service["apache2"],
}

Now run “vagrant up” from your project directory and watch as your Symfony 2 application comes to life! Navigate to http://127.0.0.1:8080 to see a Symfony 2 welcome page.

Conclusion

Now we have a working Symfony 2 project running on a virtual machine we can develop on immediately. To see the real beauty of having Vagrant and Puppet do the work for us, copy this project to another device, run “composer install” and then “vagrant up.” You should be able to develop on the project from any host running OSX, Windows or Linux and see your changes at http://127.0.0.1:8080 .

The source can be found on my Github account here: https://github.com/nater1067/Vagrant-Symfony-2 .

Do you know of any ways to improve the project we just created? If so please leave a comment so I can update this post based off your feedback.