Behat like a boss: writing custom steps

Published on

Dec 4, 2011

how2tips

Dec 5, 2011 − Today we are starting a series of blog posts about the internals of Behat, as in "the tool you use to do BDD". This series will be focusing on using Behat (assuming you already have it running), not on how to BDD right. This first post's entry barrier is quite low, as I'm going to show you how to write custom steps for your features (we're also going to do a bit of regexps, but don't be afraid!).

Writing features for your project is an amazing, quick and solid way to test your application. Behat comes with a handful of pre-defined steps, and Mink adds even more into the mix. But there will be a time when those steps will not satisfy your hunger for BDD anymore, and you will eventually have to write your own steps. Fortunately, it's pretty simple with Behat. To make things simple, when executing tests Behat will detect unimplemented steps and present you with an empty implementation of it, including regexp. Knowing that, it's time to write our first custom step, for example, a very simple step to visit the homepage of your website:

    Feature: Visit the homepage
        In order to crawl my website
        As a visitor
        I need to be able to follow links
    
    Scenario: Click a link from the homepage
        Given I am on the homepage
        When I follow "Ponies"
        Then I should see "Here be ponies!"

As you can see, it's a very dumb step, and we could as well have used Mink's builtin I am on "/" step. But that would have ruined this post, so let's move on. When you run your feature with Behat, you should be offered the following empty implementation:

/**
 * @Given /^I am on the homepage$/
 */
public function iAmOnTheHomepage()
{
    throw new PendingException();
}

Awesome! You just have to copy and paste that piece of code to your FeatureContext and start implementing. But wait? What's a FeatureContext already? The FeatureContext is a class used by Behat to hold all the information needed to run your steps. You can read more about it in Behat's documentation on the FeatureContext class. Now back to our custom step. A simple implementation could be as follow:

/**
 * @Given /^I am on the homepage$/
 */
public function iAmOnTheHomepage()
{
    $this->visit('/');
}

Drop that in your FeatureContext, rerun your features, and enjoy a little moment of self-contentement as you have just implemented your first custom step. But a custom step is not very useful if it can't receive arguments, is it? Indeed, having a step to visit the homepage is nice, but what about a step to visit virtually any page in your website? To achieve that, you will have to do a little more regexp, or, actually, let Behat do that for you. What, says you, Behat can do that for me? Yes it can. Let's rewrite a bit of our step:

    Scenario: Click a link from the homepage
        Given I am on the "homepage"
        When I follow "Ponies!"
        Then I should see "Here be ponies!"

And now you might be thinking I'm mocking you, but run your features, and discover the new proposed implementation:

/**
 * @Given I am on the "([^"]+)"
 */
public function iAmOnThe($argument1)
{
    throw new PendingException();
}

That's right, Behat treats any string wrapped by double quotes as a possible argument of your step, and adapts the regular expression accordingly. Amazing. Of course, you're not forced to use $argument1 as your argument name, but Behat is not clever enough to guess a good name for it (but Konstantin is working on mind-reading as we speak, so it should be available very soon). We can now update our step implementation:

/**
 * @Given /^I am on the "([^"]+)"$/
 */
public function iAmOnThe($page)
{
    $map = array(
        'homepage' => '/',
        'ponies'   => '/ponies',
    );

    if (!isset($map[$page])) {
        throw new InvalidArgumentException(sprintf('Page not found in mapping: "%s"', $page));
    }

    $this->visit($map[$page]);
}

Easy huh? Now you may notice that Given I am on the "ponies" does not make much sense, which is a bit sad, since we're doing all this to add meaning to our features. Well, you can always tweak your regexp:

/**
 * @Given /^I am on the "([^"]+)"(?: page)?$/
 */
public function iAmOnThe($page)
{
    $map = array(
        'homepage' => '/',
        'ponies'   => '/ponies',
    );

    if (!isset($map[$page])) {
        throw new InvalidArgumentException(sprintf('Page not found in mapping: "%s"', $page));
    }

    $this->visit($map[$page]);
}

What have we done here? We just added a (?: page)? at the end of the regexp. For those of you who are not much into regexps (shame on you), the ? character after the closing parenthesis is a quantifier that means "0 or 1". And the ?: at the start of the parenthesis means we don't wan't this parenthesis to be a matching parenthesis (hopefuly you know enough regexps basis to know what a matching parenthesis is, in the contrary, I can only urge you to document yourself about regexps). So in the end, what we have is a non-matching, optional parenthesis. It means you can write " page" at the end of your step, or you can omit it, it will work the same. We can now have more meaningful steps about ponies:

Scenario: Visit the ponies page
    Given I am on the "ponies" page

If you crawl through Mink's BaseMinkContext you may notice that in every step, the first I is optional (That is, the regexp starts with (?:|I )). This enables you to write scenarios in a more fluent way by ommitting the I where applicable:

Scenario: Write fluent scenarios
    Given I want to write steps
    When I write a step
    And implement it
    Then I should remember to make the I optional

Our final step implementation looks like this:

/**
 * @Given /^(?:|I )am on the "([^"]+)"(?: page)?$/
 */
public function iAmOnThe($page)
{
    $map = array(
        'homepage' => '/',
        'ponies'   => '/ponies',
    );

    if (!isset($map[$page])) {
        throw new InvalidArgumentException(sprintf('Page not found in mapping: "%s"', $page));
    }

    $this->visit($map[$page]);
}

Woo! Enough for today. We already saw how to:

  • write custom steps
  • write custom steps with arguments
  • tweak the step's regexp to fit a more fluent writing style

Next time, I'll tell you about meta-step. It's an amazing way of leverage other steps in your own custom steps, so stay tuned.

Written by

KNP Labs
KNP Labs

Comments