Testing PHP web applications with Cucumber

Published by at 3rd November 2012 4:43 pm

Ever since I first heard of Cucumber, it's seemed like something I would find really useful. Like many developers, especially those who use PHP regularly, I know full well that I should make a point of writing proper automated tests for my web apps, but invariably wind up just thinking "I haven't got time to get my head around a testing framework and it'll take ages to set up, so I'll just click around and look for bugs". This does get very, very tedious quite quickly, however.

At work I've reached a point with a web app I'm building where I needed to test it extensively to make sure it worked OK. I soon began to get very, very fed up of the repetitive clicking around necessary to test the application, so I began looking around for a solution. I gave Selenium IDE a try, but I found that to be annoyingly unreliable when recording tests. I'd heard of Cucumber, so I did some googling, found some resources, and began tinkering with that. Quite quickly, I had a few basic acceptance tests up and running that were much more reliable than Selenium IDE, and much less tedious to use than manual testing. Within a very short space of time, I realised that Cucumber was one of those tools that was going to dramatically improve my coding experience, much like when I switched from Subversion to Git.

What's so great about Cucumber compared to other acceptance testing solutions?

  • Cucumber scenarios are written using Gherkin, a simple syntax that makes it easy for customers to set out exactly what behaviour they want to see. Far from being tedious requirement documents, these set out in a simple and intuitive way what should happen once the application is complete. By requiring customers to think carefully about what they want and get it down in writing, you can ensure the customer has a good idea what they want before you write any code, making it much less likely they'll turn around afterwards and say "No, that's not what we want". This, more than anything, is for me the true power of Cucumber - it allows customers and developers to easily collaborate to set out what the web app will do, and gets you automated tests into the bargain as well.
  • Because Cucumber is packaged as a Ruby gem, it's easy to install it and any other Ruby modules it may require.
  • You can use Capybara to test your web app. Capybara is a very handy Ruby gem that allows you to easily interact with your web app, and it allows several different drivers to be used. If you don't need JavaScript, for instance, you can use Mechanize for faster tests. If you do, you can use selenium-webdriver to automate the browser instead, and it will load an instance of Firefox and use that for testing.
  • It can also be used for testing RESTful web services. HTTParty is another handy Ruby gem that can be used for testing an API.

One question you may ask is 'Why use a Ruby tool to test PHP apps?'. Well, there is Behat, a very similar tool for PHP, so you can use that if you'd prefer. However, I personally have found that it's not too much of a problem switching context between writing Ruby code for the acceptance tests and PHP code for the application itself. Ruby also has some advantages here - RVM is a very handy tool for running multiple instances of Ruby, and RubyGems makes it easy to install any additional modules you may need. You don't really need to know much Ruby to use it - this is essentially my first encounter with Ruby barring a few small tutorials, but I haven't had any significant issues with it. Finally, the Cucumber community seems to be very active, which is always a plus.

When searching for a tutorial on getting Cucumber working with PHP, I only found one good one, and that didn't cover a lot of the issues I'd have liked to cover, not did it cover actually using Cucumber as part of the development process, so I had to puzzle out much of it myself. So hopefully, by covering more of the ground that your average PHP developer is likely to need, I can show you just how useful Cucumber can be when added to your PHP development toolkit.

In this tutorial, we'll build a very simple todo-list application using the Slim framework, but we'll use Cucumber to test it throughout to ensure that it works the way we want it to. Hopefully, by doing this, we'll get a rock-solid web app that meets our requirements exactly.

First of all, you'll want to install RVM to make it easier to manage multiple Ruby installs. You may be able to use your system's Ruby install, but RVM is usually a safer bet:

\curl -L https://get.rvm.io | bash -s stable --ruby

This was sufficient to install RVM on Mac OS X. On Ubuntu, I also had to install the openssl and zlib packages. Before installing RVM, use apt-get to install the required packages:

sudo apt-get install curl git git-core zlib1g-dev zlibc libxml2-dev libxslt1-dev libyaml-dev build-essential checkinstall openssl libreadline6 libreadline6-dev zlib1g libssl-dev libsqlite3-dev sqlite3 autoconf libc6-dev ncurses-dev automake libtool bison subversion pkg-config

Once RVM is installed, then close and reopen your terminal so that RVM is loaded. Then, install the correct packages:

rvm pkg install openssl zlib

Now we can install our new copy of Ruby. On Ubuntu, I had to install Ruby 1.8.7 first:

1rvm install 1.8.7
2rvm use 1.8.7

Then I installed Ruby 1.9.3:

rvm install 1.9.3 --with-openssl-dir=$HOME/.rvm/usr

Whereas on OS X, this is all that was required:

rvm install 1.9.3

Once that's done, run the following to set the version of Ruby being used

rvm use 1.9.3

With that done, you should be able to install the required Ruby gems. Now, you could install these manually, like this:

1gem install cucumber
2gem install rspec
3gem install mechanize
4gem install capybara
5gem install selenium-webdriver
6gem install capybara-mechanize

However, there's a more convenient way. First, create a file in the project's root directory called Gemfile and put the following content into it:

1source "http://rubygems.org"
2gem "cucumber"
3gem "rspec"
4gem "mechanize"
5gem "capybara"
6gem "selenium-webdriver"
7gem "capybara-mechanize"

Then install the Bundler gem:

gem install bundler

Then use Bundler to install the required gems:

bundle install

This makes it easier to get your project set up somewhere else because you can put the Gemfile under version control, making it easier to duplicate this setup elsewhere.

With that out of the way, let's start work on our app. To save time, we'll use the Slim framework to do some of the heavy lifting for our application. Download Slim and put it in a folder on your local web server.

Now, before we actually write any code, we'll set out our first Cucumber scenario. Create a folder inside the folder you put Slim inside and call it features. Inside it, create a new file called todo.feature and put the following content into it:

1Feature: Todo
2
3 In order to use the site
4 As a user
5 I want to be able to submit, view and delete to-do list items
6
7 Scenario: New item
8 Given I am on the home page
9 When I click on New Item
10 And I fill in the item
11 And I click the button Submit
12 Then I should see the new item added to the list

Notice how simple this is? Everything is written as an example of how an end user would interact with the site. There's nothing hard about this - it just describes what the site needs to do.

The first line is just the name of this feature. The following three lines are just a comment. Then the Scenario line gives a name to this particular scenario - a Scenario is just a series of steps that describes an action.

Then, we see the Given line. This sets out the starting conditions. Note that you can easily set out multiple starting conditions using the And keyword on subsequent lines, as we do later in the file. Here, we're just making sure we're on the home page.

Next, we see the When line. This, and the subsequent And lines, set out what actions we want to take when going through this step. In this example, we're clicking on a link marked 'New Item', filling in a text input, and clicking the Submit button. So we're already thinking about how our application is going to work, before we've written a line of code.

Finally, we see the Then line. This sets out what should have happened once we've finished going through this step. Here we want to make sure the new item has been added to the list.

Now, go to the folder you unpacked Slim into and run cucumber from the shell. You should see something like this:

1Feature: Todo
2
3 In order to use the site
4 As a user
5 I want to be able to submit, view and delete to-do list items
6
7 Scenario: New item # features/todo.feature:7
8 Given I am on the home page # features/todo.feature:8
9 When I click on New Item # features/todo.feature:9
10 And I fill in the item # features/todo.feature:10
11 And I click the button Submit # features/todo.feature:11
12 Then I should see the new item added to the list # features/todo.feature:12
13
141 scenario (1 undefined)
155 steps (5 undefined)
160m0.004s
17
18You can implement step definitions for undefined steps with these snippets:
19
20Given /^I am on the home page$/ do
21 pending # express the regexp above with the code you wish you had
22end
23
24When /^I click on New Item$/ do
25 pending # express the regexp above with the code you wish you had
26end
27
28When /^I fill in the item$/ do
29 pending # express the regexp above with the code you wish you had
30end
31
32When /^I click the button Submit$/ do
33 pending # express the regexp above with the code you wish you had
34end
35
36Then /^I should see the new item added to the list$/ do
37 pending # express the regexp above with the code you wish you had
38end
39
40If you want snippets in a different programming language,
41just make sure a file with the appropriate file extension
42exists where cucumber looks for step definitions.

At this stage, Cucumber isn't doing anything much, it's just telling you that these steps haven't been defined as yet. To define a step, you simply write some Ruby code that expresses that step.

Let's do that. Under features, create a new directory called step_definitions. Inside that, create a file called todo_steps.rb and paste the code snippets returned by Cucumber into it. Once that has been saved, run cucumber again and you should see something like this:

1Feature: Todo
2
3 In order to use the site
4 As a user
5 I want to be able to submit, view and delete to-do list items
6
7 Scenario: New item # features/todo.feature:7
8 Given I am on the home page # features/step_definitions/todo_steps.rb:1
9 TODO (Cucumber::Pending)
10 ./features/step_definitions/todo_steps.rb:2:in `/^I am on the home page$/'
11 features/todo.feature:8:in `Given I am on the home page'
12 When I click on New Item # features/step_definitions/todo_steps.rb:5
13 And I fill in the item # features/step_definitions/todo_steps.rb:9
14 And I click the button Submit # features/step_definitions/todo_steps.rb:13
15 Then I should see the new item added to the list # features/step_definitions/todo_steps.rb:17
16
171 scenario (1 pending)
185 steps (4 skipped, 1 pending)
190m0.004s

So far, the steps we've written don't actually do anything - each step contains nothing but the pending statement. We need to replace the code inside each of those steps with some Ruby code that implements that step. As the first step in this scenario is still pending, Cucumber skips all the remaining steps.

Let's implement these steps. First of all, we need to set some configuration options. In the features folder, create a new folder called support, and under that create a new file called env.rb. In there, place the following code:

1require 'rspec/expectations'
2require 'capybara'
3require 'capybara/mechanize'
4require 'capybara/cucumber'
5require 'test/unit/assertions'
6require 'mechanize'
7
8World(Test::Unit::Assertions)
9
10Capybara.default_driver = :mechanize
11Capybara.app_host = "http://localhost"
12World(Capybara)

This includes all of the Ruby gems required for our purposes, and sets Capybara to use the Mechanize driver for testing web apps. If you've not heard of it before, Capybara can be thought of as a way of scripting a web browser that supports numerous drivers, some of which are headless and some of which aren't. Here we're using Mechanize, which is headless, but later on we'll use Selenium to show you how it would work with a non-headless web browser.

With that done, the next job is to actually implement the steps. Head back to features/step_definitions/todo_steps.rb and edit it as follows:

1Given /^I am on the home page$/ do
2 visit "http://localhost/~matthewdaly/todo/index.php"
3end
4
5When /^I click on New Item$/ do
6 pending # express the regexp above with the code you wish you had
7end
8
9When /^I fill in the item$/ do
10 pending # express the regexp above with the code you wish you had
11end
12
13When /^I click the button Submit$/ do
14 pending # express the regexp above with the code you wish you had
15end
16
17Then /^I should see the new item added to the list$/ do
18 pending # express the regexp above with the code you wish you had
19end

Don't forget to replace the URL in that first step with the one pointing at your index.php for your local copy of Slim. At this point we're only implementing the first step, so that's all we need to do for now. Once that's done, go back to the root of the web app and run cucumber again. You should see something like this:

1Feature: Todo
2
3 In order to use the site
4 As a user
5 I want to be able to submit, view and delete to-do list items
6
7 Scenario: New item # features/todo.feature:7
8 Given I am on the home page # features/step_definitions/todo_steps.rb:1
9 When I click on New Item # features/step_definitions/todo_steps.rb:5
10 TODO (Cucumber::Pending)
11 ./features/step_definitions/todo_steps.rb:6:in `/^I click on New Item$/'
12 features/todo.feature:9:in `When I click on New Item'
13 And I fill in the item # features/step_definitions/todo_steps.rb:9
14 And I click the button Submit # features/step_definitions/todo_steps.rb:13
15 Then I should see the new item added to the list # features/step_definitions/todo_steps.rb:17
16
171 scenario (1 pending)
185 steps (3 skipped, 1 pending, 1 passed)
190m0.036s

Our first step has passed! Now, we move onto the next step. Open features/step_definitions/todo_steps.rb again, and amend the second step definition as follows:

1When /^I click on New Item$/ do
2 click_link ('New Item')
3end

Now, hang on a minute here. This Ruby code is pretty easy to understand - it just clicks on a link with the title, ID or text 'New Item'. But we don't want to have to rewrite this step for every single link in the application. Wouldn't it be great if we could have this step definition accept any text and click on the appropriate link, so we could reuse it elsewhere? Well, we can. Change the second step to look like this:

1When /^I click on (.*)$/ do |link|
2 click_link (link)
3end

What's happening here is that we capture the text after the word 'on' using a regular expression and pass it through to the step definition as the variable link. Then, we have Capybara click on that link. Pretty simple, and it saves us on some work in future.

Now run cucumber again, and you should see something like this:

1Feature: Todo
2
3 In order to use the site
4 As a user
5 I want to be able to submit, view and delete to-do list items
6
7 Scenario: New item # features/todo.feature:7
8 Given I am on the home page # features/step_definitions/todo_steps.rb:1
9 When I click on New Item # features/step_definitions/todo_steps.rb:5
10 no link with title, id or text 'New Item' found (Capybara::ElementNotFound)
11 (eval):2:in `send'
12 (eval):2:in `click_link'
13 ./features/step_definitions/todo_steps.rb:6:in `/^I click on (.*)$/'
14 features/todo.feature:9:in `When I click on New Item'
15 And I fill in the item # features/step_definitions/todo_steps.rb:9
16 And I click the button Submit # features/step_definitions/todo_steps.rb:13
17 Then I should see the new item added to the list # features/step_definitions/todo_steps.rb:17
18
19Failing Scenarios:
20cucumber features/todo.feature:7 # Scenario: New item
21
221 scenario (1 failed)
235 steps (1 failed, 3 skipped, 1 passed)
240m0.042s

We've got our second step in place, but it's failing because there is no link with the text 'New Item'. Let's remedy that. Head back to the folder you put Slim in, and open index.php.

1<?php
2require 'Slim/Slim.php';
3
4\Slim\Slim::registerAutoloader();
5
6$app = new \Slim\Slim();
7
8// GET route
9$app->get('/', function () {
10 $template = <<<EOT
11<!DOCTYPE html>
12<html>
13 <head>
14 <title>Todo list</title>
15 </head>
16 <body>
17 <a href="index.php/newitem">New Item</a>
18 </body>
19</html>
20EOT;
21 echo $template;
22});
23
24$app->run();
25?>

Here I've stripped out most of the default code and comments so we can see more easily what's happening. If you haven't used Slim before, it works by letting you define routes that are accessed via HTTP GET, POST, PUT or DELETE methods, and define what the response will be to each one. Here, we've defined a simple controller for GET requests to '/', and we return a template that includes a link with the text 'New Item'.

Now, run cucumber again and you should see the following:

1Feature: Todo
2
3 In order to use the site
4 As a user
5 I want to be able to submit, view and delete to-do list items
6
7 Scenario: New item # features/todo.feature:7
8 Given I am on the home page # features/step_definitions/todo_steps.rb:1
9 When I click on New Item # features/step_definitions/todo_steps.rb:5
10 Received the following error for a GET request to http://localhost/~matthewdaly/todo/newitem: '404 => Net::HTTPNotFound for http://localhost/~matthewdaly/todo/newitem -- unhandled response' (RuntimeError)
11 (eval):2:in `send'
12 (eval):2:in `click_link'
13 ./features/step_definitions/todo_steps.rb:6:in `/^I click on (.*)$/'
14 features/todo.feature:9:in `When I click on New Item'
15 And I fill in the item # features/step_definitions/todo_steps.rb:9
16 And I click the button Submit # features/step_definitions/todo_steps.rb:13
17 Then I should see the new item added to the list # features/step_definitions/todo_steps.rb:17
18
19Failing Scenarios:
20cucumber features/todo.feature:7 # Scenario: New item
21
221 scenario (1 failed)
235 steps (1 failed, 3 skipped, 1 passed)
240m0.153s

Our second step is still failing, but only because we haven't yet defined a route for the destination when we click on the link, so let's fix that. Open up index.php again and change it to look like this:

1<?php
2require 'Slim/Slim.php';
3
4\Slim\Slim::registerAutoloader();
5
6$app = new \Slim\Slim();
7
8// GET route
9$app->get('/', function () {
10 $template = <<<EOT
11<!DOCTYPE html>
12<html>
13 <head>
14 <title>Todo list</title>
15 </head>
16 <body>
17 <a href="index.php/newitem">New Item</a>
18 </body>
19</html>
20EOT;
21 echo $template;
22});
23
24$app->get('/newitem', function () {
25 $template = <<<EOT
26<!DOCTYPE html>
27<html>
28 <head>
29 <title>Todo list</title>
30 </head>
31 <body>
32 <form action="index.php/submitnewitem" method="POST">
33 <label>New todo item text<input type="text" name="item" /></label>
34 <input type="submit" value="Submit" />
35 </form>
36 </body>
37</html>
38EOT;
39 echo $template;
40});
41
42$app->run();
43?>

We're just adding a new route to handle what happens when we click the link here. The new page also has a form for submitting the new item.

With that done, the second step should be in place. Run cucumber again and you should see something like this:

1Feature: Todo
2
3 In order to use the site
4 As a user
5 I want to be able to submit, view and delete to-do list items
6
7 Scenario: New item # features/todo.feature:7
8 Given I am on the home page # features/step_definitions/todo_steps.rb:1
9 When I click on New Item # features/step_definitions/todo_steps.rb:5
10 And I fill in the item # features/step_definitions/todo_steps.rb:9
11 TODO (Cucumber::Pending)
12 ./features/step_definitions/todo_steps.rb:10:in `/^I fill in the item$/'
13 features/todo.feature:10:in `And I fill in the item'
14 And I click the button Submit # features/step_definitions/todo_steps.rb:13
15 Then I should see the new item added to the list # features/step_definitions/todo_steps.rb:17
16
171 scenario (1 pending)
185 steps (2 skipped, 1 pending, 2 passed)
190m0.048s

So onto the third step. We've already created the input for filling in the item, so all we need to do to make this step pass is write an appropriate step definition:

1When /^I fill in the item$/ do
2 fill_in 'item', :with => 'Feed cat'
3end

With that done, run cucumber again and this step should pass:

1Feature: Todo
2
3 In order to use the site
4 As a user
5 I want to be able to submit, view and delete to-do list items
6
7 Scenario: New item # features/todo.feature:7
8 Given I am on the home page # features/step_definitions/todo_steps.rb:1
9 When I click on New Item # features/step_definitions/todo_steps.rb:5
10 And I fill in the item # features/step_definitions/todo_steps.rb:9
11 And I click the button Submit # features/step_definitions/todo_steps.rb:13
12 TODO (Cucumber::Pending)
13 ./features/step_definitions/todo_steps.rb:14:in `/^I click the button Submit$/'
14 features/todo.feature:11:in `And I click the button Submit'
15 Then I should see the new item added to the list # features/step_definitions/todo_steps.rb:17
16
171 scenario (1 pending)
185 steps (1 skipped, 1 pending, 3 passed)
190m0.117s

Now we need to implement the step for clicking the Submit button. As with clicking on the New Item link, we can make this step generic to save us time later:

1When /^I click the button (.*)$/ do |button|
2 click_button (button)
3end

With that done, run cucumber again and you should see something like this:

1Feature: Todo
2
3 In order to use the site
4 As a user
5 I want to be able to submit, view and delete to-do list items
6
7 Scenario: New item # features/todo.feature:7
8 Given I am on the home page # features/step_definitions/todo_steps.rb:1
9 When I click on New Item # features/step_definitions/todo_steps.rb:5
10 And I fill in the item # features/step_definitions/todo_steps.rb:9
11 And I click the button Submit # features/step_definitions/todo_steps.rb:13
12 Received the following error for a POST request to http://localhost/~matthewdaly/todo/index.php/index.php/submitnewitem: '404 => Net::HTTPNotFound for http://localhost/~matthewdaly/todo/index.php/index.php/submitnewitem -- unhandled response' (RuntimeError)
13 (eval):2:in `send'
14 (eval):2:in `click_button'
15 ./features/step_definitions/todo_steps.rb:14:in `/^I click the button (.*)$/'
16 features/todo.feature:11:in `And I click the button Submit'
17 Then I should see the new item added to the list # features/step_definitions/todo_steps.rb:17
18
19Failing Scenarios:
20cucumber features/todo.feature:7 # Scenario: New item
21
221 scenario (1 failed)
235 steps (1 failed, 1 skipped, 3 passed)
240m0.210s

The step is failing here because submitting the new item generates a 404 error. We need to handle the POST. Open up index.php again and edit it to look like this:

1<?php
2require 'Slim/Slim.php';
3
4\Slim\Slim::registerAutoloader();
5
6$app = new \Slim\Slim();
7
8// GET route
9$app->get('/', function () {
10 $template = <<<EOT
11<!DOCTYPE html>
12<html>
13 <head>
14 <title>Todo list</title>
15 </head>
16 <body>
17 <a href="index.php/newitem">New Item</a>
18 </body>
19</html>
20EOT;
21 echo $template;
22});
23
24$app->get('/newitem', function () {
25 $template = <<<EOT
26<!DOCTYPE html>
27<html>
28 <head>
29 <title>Todo list</title>
30 </head>
31 <body>
32 <form action="index.php/submitnewitem" method="POST">
33 <label>New todo item text<input type="text" name="item" /></label>
34 <input type="submit" value="Submit" />
35 </form>
36 </body>
37</html>
38EOT;
39 echo $template;
40});
41
42$app->post('/submitnewitem', function () {
43 $item = $_POST['item'];
44 $template = <<<EOT
45<!DOCTYPE html>
46<html>
47 <head>
48 <title>Todo list</title>
49 </head>
50 <body>
51 <p>$item</p>
52 </body>
53</html>
54EOT;
55 echo $template;
56});
57
58$app->run();
59?>

Here we're cheating a little bit. In a working application we'd want to store the to-do list items in a database, but to keep this tutorial simple we'll just output the result of the POST request and leave implementing a database to store the items as an exercise for the reader.

Now, run cucumber again and you should see that this step now passes:

1Feature: Todo
2
3 In order to use the site
4 As a user
5 I want to be able to submit, view and delete to-do list items
6
7 Scenario: New item # features/todo.feature:7
8 Given I am on the home page # features/step_definitions/todo_steps.rb:1
9 When I click on New Item # features/step_definitions/todo_steps.rb:5
10 And I fill in the item # features/step_definitions/todo_steps.rb:9
11 And I click the button Submit # features/step_definitions/todo_steps.rb:13
12 Then I should see the new item added to the list # features/step_definitions/todo_steps.rb:17
13 TODO (Cucumber::Pending)
14 ./features/step_definitions/todo_steps.rb:18:in `/^I should see the new item added to the list$/'
15 features/todo.feature:12:in `Then I should see the new item added to the list'
16
171 scenario (1 pending)
185 steps (1 pending, 4 passed)
190m0.067s

On to our final step. We want to make sure the page contains the text we submitted, which is very easy to do with Capybara. Change the final step to look like this:

1Then /^I should see the new item added to the list$/ do
2 page.should have_content('Feed cat')
3end

Now run cucumber again and you should see that the scenario has now passed:

1Feature: Todo
2
3 In order to use the site
4 As a user
5 I want to be able to submit, view and delete to-do list items
6
7 Scenario: New item # features/todo.feature:7
8 Given I am on the home page # features/step_definitions/todo_steps.rb:1
9 When I click on New Item # features/step_definitions/todo_steps.rb:5
10 And I fill in the item # features/step_definitions/todo_steps.rb:9
11 And I click the button Submit # features/step_definitions/todo_steps.rb:13
12 Then I should see the new item added to the list # features/step_definitions/todo_steps.rb:17
13
141 scenario (1 passed)
155 steps (5 passed)
160m0.068s

We're nearly done here, but first there's a couple of other handy things you can do with Cucumber that I'd like to show you. We've been using the Mechanize driver for Capybara, which is very fast and efficient. However, it's effectively a text-mode browser like Lynx, so it can't be used to test any functionality that relies on JavaScript. However, Mechanize isn't the only driver available for Capybara, and you can switch to the JavaScript driver when necessary so you can test. The default JavaScript driver is Selenium, which will launch an instance of Firefox and use that for the test.

It's easy to switch to the JavaScript driver when you need it. Just tag the scenario with @javascript, as in this example:

1Feature: Todo
2
3 In order to use the site
4 As a user
5 I want to be able to submit, view and delete to-do list items
6
7 @javascript
8 Scenario: New item
9 Given I am on the home page
10 When I click on New Item
11 And I fill in the item
12 And I click the button Submit
13 Then I should see the new item added to the list

Now run cucumber again and this time it will fire up an instance of Firefox and use that to run the tests. This can also be handy for debugging purposes since, unlike with Mechanize, you can see the pages.

Finally, what about if you want to test the same functionality multiple times with different input? You don't want to have to write out multiple scenarios that are virtually identical, even if you have refactored them to make them more useful. What you need is a way to repeat the same test, only with different input each time.

Handily, Cucumber can do this too. First, let's refactor the code for our step definitions so the final step can handle any text:

1Given /^I am on the home page$/ do
2 visit "http://localhost/~matthewdaly/todo/index.php"
3end
4
5When /^I click on (.*)$/ do |link|
6 click_link (link)
7end
8
9When /^I fill in the item with (.*)$/ do |item|
10 fill_in 'item', :with => item
11end
12
13When /^I click the button (.*)$/ do |button|
14 click_button (button)
15end
16
17Then /^I should see the text (.*)$/ do |text|
18 page.should have_content(text)
19end

Here we've changed the third and fifth items so we can pass any value we want through to them. As I mentioned earlier, this is good practice since it means we don't have to write more code for our tests than we need to.

With that done, open up the feature file and amend it to look like this:

1Feature: Todo
2
3 In order to use the site
4 As a user
5 I want to be able to submit, view and delete to-do list items
6
7 @javascript
8 Scenario Outline: New item
9 Given I am on the home page
10 When I click on New Item
11 And I fill in the item with <item>
12 And I click the button Submit
13 Then I should see the text <item>
14 Examples:
15 | item |
16 | Feed cat |
17 | Stop milk |
18 | Take over world |

If you then run cucumber again, the scenario should run three times, each time entering different text:

1Feature: Todo
2
3 In order to use the site
4 As a user
5 I want to be able to submit, view and delete to-do list items
6
7 @javascript
8 Scenario Outline: New item # features/todo.feature:8
9 Given I am on the home page # features/step_definitions/todo_steps.rb:1
10 When I click on New Item # features/step_definitions/todo_steps.rb:5
11 And I fill in the item with <item> # features/step_definitions/todo_steps.rb:9
12 And I click the button Submit # features/step_definitions/todo_steps.rb:13
13 Then I should see the text <item> # features/step_definitions/todo_steps.rb:17
14
15 Examples:
16 | item |
17 | Feed cat |
18 | Stop milk |
19 | Take over world |
20
213 scenarios (3 passed)
2215 steps (15 passed)
230m25.936s

With only a few changes, we're now running the same scenario over and over again with different input, and testing the output is correct for each one. This makes it very easy to test repetitive content. For instance, if you had an e-commerce site with lots of products and you wanted to test the pages for some of the products, you could put them in a table like this. You can have more than one column if necessary, so you could write a scenario like this:

1 Scenario Outline: Test products
2 Given I am on the home page
3 When I search for <product>
4 And I click on the first result
5 Then I should not see any errors
6 And I should see the text <productname>
7
8 Examples:
9 | product | productname |
10 | supersprocket | Super Sprocket 3000 |

As you can see, Cucumber is a really simple way to start testing your web apps, and can really improve the quality of your code. Even if you've never used Ruby before, Capybara's API is very simple and intuitive, and should adequately cover most of what you need to do when testing a web app.

As I mentioned, the PHP community in general has been a bit slack in terms of getting proper automated tests working. But Cucumber makes it so simple, and offers so many other benefits, such as human-readable tests and getting stakeholders more involved in the development process, that there's really no excuse not to use it. Hope you've enjoyed this tutorial, and that it's encouraged you to start using Cucumber to test your own web apps.