Skip to content

Latest commit

 

History

History
701 lines (513 loc) · 23.6 KB

quick_start.rst

File metadata and controls

701 lines (513 loc) · 23.6 KB

Quick Start

Welcome to Behat! Behat is a tool to close the Behavior Driven Development (BDD) communication loop. BDD is a methodology for developing software through continuous example-based communication between developers and a business, which this application supports. This communication happens in a form that both the business and developers can clearly understand - examples. Examples are structured around the Context-Action-Outcome pattern and are written in a special format called Gherkin. The fact that Gherkin is very structural makes it very easy to automate and autotest your behaviour examples against a developing application. Automated examples are then actually used to drive this application development TDD-style.

To become a Behat'er in 30 minutes, just dive into the quick-start guide and enjoy!

Example

Let's imagine that you are building a completely new e-commerce platform. One of the key features of any online shopping platform is the ability to buy products. But before buying anything, customers should be able to tell the system which products they are interested in buying. You need a basket. So let's write our first user-story:

Feature: Product basket
  In order to buy products
  As a customer
  I need to be able to put interesting products into a basket

Note

This is a basic Gherkin feature and it is a simple description of this feature's story. Every feature starts with this same format: a line with the title of the feature, followed by three lines that describe the benefit, the role and the feature itself with any amount of additional description lines following after.

Before we begin to work on this feature, we must fulfil a promise of any user-story and have a real conversation with our business stakeholders. They might say that they want customers to see not only the combined price of the products in the basket, but the price reflecting both the VAT (20%) and the delivery cost (which depends on the total price of the products):

Feature: Product basket
  In order to buy products
  As a customer
  I need to be able to put interesting products into a basket

  Rules:
  - VAT is 20%
  - Delivery for basket under £10 is £3
  - Delivery for basket over £10 is £2

So as you can see, it already becomes tricky (ambiguous at least) to talk about this feature in terms of rules. What does it mean to add VAT? What happens when we have two products, one of which is less than £10 and another that is more? Instead you proceed with having a back-and-forth chat with stakeholders in form of actual examples of a customer adding products to the basket. After some time, you will come up with your first behaviour examples (in BDD these are called scenarios):

Feature: Product basket
  In order to buy products
  As a customer
  I need to be able to put interesting products into a basket

  Rules:
  - VAT is 20%
  - Delivery for basket under £10 is £3
  - Delivery for basket over £10 is £2

  Scenario: Buying a single product under £10
    Given there is a "Sith Lord Lightsaber", which costs £5
    When I add the "Sith Lord Lightsaber" to the basket
    Then I should have 1 product in the basket
    And the overall basket price should be £9

  Scenario: Buying a single product over £10
    Given there is a "Sith Lord Lightsaber", which costs £15
    When I add the "Sith Lord Lightsaber" to the basket
    Then I should have 1 product in the basket
    And the overall basket price should be £20

  Scenario: Buying two products over £10
    Given there is a "Sith Lord Lightsaber", which costs £10
    And there is a "Jedi Lightsaber", which costs £5
    When I add the "Sith Lord Lightsaber" to the basket
    And I add the "Jedi Lightsaber" to the basket
    Then I should have 2 products in the basket
    And the overall basket price should be £20

Note

Each scenario always follows the same basic format:

Scenario: Some description of the scenario
  Given some context
  When some event
  Then outcome

Each part of the scenario - the context, the event, and the outcome - can be extended by adding the And or But keyword:

Scenario: Some description of the scenario
  Given some context
  And more context
  When some event
  And second event occurs
  Then outcome
  And another outcome
  But another outcome

There's no actual difference between, Then, And But or any of the other words that start each line. These keywords are all made available so that your scenarios are natural and readable.

This is your and your stakeholders' shared understanding of the project written in a structured format. It is all based on the clear and constructive conversation you have had together. Now you can put this text in a simple file - features/basket.feature - under your project directory and start implementing the feature by manually checking if it fits the defined scenarios. No tools (Behat in our case) needed. That, in essence, is what BDD is.

If you are still reading, it means you are expecting more. Good! Because even though tools are not the central piece of BDD puzzle, they do improve the entire process and add a lot of benefits on top of it. For one, tools like Behat actually do close the communication loop of the story. It means that not only you and your stakeholder can together define how your feature should work before going to implement it, BDD tools allow you to automate that behaviour check after this feature is implemented. So everybody knows when it is done and when the team can stop writing code. That, in essence, is what Behat is.

Behat is an executable that you'll run from the command line to test that your application behaves exactly as you described in your *.feature scenarios.

Going forward, we'll show you how Behat can be used to automate this particular basket feature as a test verifying that the application (existing or not) works as you and your stakeholders expect (according to your conversation) it to.

That's it! Behat can be used to automate anything, including web-related functionality via the Mink library.

Note

If you want to learn more about the philosophy of "Behaviour Driven Development" of your application, see What's in a Story?

Note

Behat was heavily inspired by Ruby's Cucumber project. Since v3.0, Behat is considered an official Cucumber implementation in PHP and is part of one big family of BDD tools.

Installation

Before you begin, ensure that you have at least PHP 5.3.3 installed.

Method #1 - Composer (the recommended one)

The official way to install Behat is through Composer. Composer is a package manager for PHP. Not only can it install Behat for you right now, it will be able to easily update you to the latest version later when one comes out. If you don't have Composer already, see the Composer documentation for instructions. After that, just go into your project directory (or create a new one) and run:

$ php composer.phar require --dev behat/behat

Then you will be able to check installed Behat version using:

$ vendor/bin/behat -V

Method #2 - PHAR (an easy one)

An easier way to install Behat is to grab a latest behat.phar from the download page. Make sure that you download a 3+ release. After downloading it, just place it in your project folder (or create a new one) and check the installed version using:

$ php behat.phar -V

Development

Now we will use our newly installed Behat to automate our previously written feature under the features/basket.feature.

Our first step after describing the feature and installing Behat is configuring the test suite. A test suite is a key concept in Behat. Suites are a way for Behat to know where to find and how to test your application against your features. By default, Behat comes with a default suite, which tells Behat to search for features under the features/ folder and test them using FeatureContext class. Lets initialise this suite:

$ vendor/bin/behat --init

Note

If you installed Behat via PHAR, use php behat.phar instead of vendor/bin/behat in the rest of this article.

The --init command tells Behat to provide you with things missing to start testing your feature. In our case - it's just a FeatureContext class under the features/bootstrap/FeatureContext.php file.

Executing Behat

I think we're ready to see Behat in action! Let's run it:

$ vendor/bin/behat

You should see that Behat recognised that you have 3 scenarios. Behat should also tell you that your FeatureContext class has missing steps and proposes step snippets for you. FeatureContext is your test environment. It is an object through which you will describe how you would test your application against your features. It was generated by the --init command and now looks like this:

// features/bootstrap/FeatureContext.php

use Behat\Behat\Context\SnippetAcceptingContext;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;

class FeatureContext implements SnippetAcceptingContext
{
    /**
     * Initializes context.
     */
    public function __construct()
    {
    }
}

Defining Steps

Finally, we got to the automation part. How does Behat know what to do when it sees Given there is a "Sith Lord Lightsaber", which costs £5? You tell it. You write PHP code inside your context class (FeatureContext in our case) and tell Behat that this code represents a specific scenario step (via an attribute with a pattern):

#[Given('there is a(n) :arg1, which costs £:arg2')]
public function thereIsAWhichCostsPs($arg1, $arg2)
{
    throw new PendingException();
}

Note

Behat uses PHP Attributes for step definitions, step transformations and hooks. It also supports doc-block annotations for compatibility with legacy code, but this syntax is deprecated - see the :doc:`annotations </user_guide/annotations>` documentation for details.

#[Given('there is a(n) :arg1, which costs £:arg2')] above the method tells Behat that this particular method should be executed whenever Behat sees step that looks like ... there is a ..., which costs £....

The #[Given], #[When] and #[Then] attributes are functionally identical - they only exist separately to help keep the wording of your step definitions readable. So this pattern will match any of the following steps:

Given there is a "Sith Lord Lightsaber", which costs £5
When there is a "Sith Lord Lightsaber", which costs £10
Then there is an 'Anakin Lightsaber', which costs £10
And there is a Lightsaber, which costs £2
But there is a Lightsaber, which costs £25

Not only that, but Behat will capture tokens (words starting with :, e.g. :arg1) from the step and pass their value to the method as arguments:

// Given there is a "Sith Lord Lightsaber", which costs £5
$context->thereIsAWhichCostsPs('Sith Lord Lightsaber', '5');

// Then there is a 'Jedi Lightsaber', which costs £10
$context->thereIsAWhichCostsPs('Jedi Lightsaber', '10');

// But there is a Lightsaber, which costs £25
$context->thereIsAWhichCostsPs('Lightsaber', '25');

Note

If you need to define more complex matching algorithms, you can also use regular expressions:

#[Given('/there is an? \"([^\"]+)\", which costs £([\d\.]+)/')]
public function thereIsAWhichCostsPs($arg1, $arg2)
{
    throw new PendingException();
}

Those patterns could be quite powerful, but at the same time, writing them for all possible steps manually could become extremely tedious and boring. That's why Behat does it for you. Remember when you previously executed vendor/bin/behat you got:

--- FeatureContext has missing steps. Define them with these snippets:

    #[Given('there is a :arg1, which costs £:arg2')]
    public function thereIsAWhichCostsPs($arg1, $arg2)
    {
        throw new PendingException();
    }

Behat automatically generates snippets for missing steps and all that you need to do is copy and paste them into your context classes. Or there is an even easier way - just run:

$ vendor/bin/behat --dry-run --append-snippets

And Behat will automatically append all the missing step methods into your FeatureContext class. How cool is that?

If you executed --append-snippets, your FeatureContext should look like:

// features/bootstrap/FeatureContext.php

use Behat\Behat\Tester\Exception\PendingException;
use Behat\Behat\Context\SnippetAcceptingContext;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use Behat\Step\Given;
use Behat\Step\Then;
use Behat\Step\When;

class FeatureContext implements SnippetAcceptingContext
{
    #[Given('there is a :arg1, which costs £:arg2')]
    public function thereIsAWhichCostsPs($arg1, $arg2)
    {
        throw new PendingException();
    }

    #[When('I add the :arg1 to the basket')]
    public function iAddTheToTheBasket($arg1)
    {
        throw new PendingException();
    }

     #[Then('I should have :arg1 product(s) in the basket')]
    public function iShouldHaveProductInTheBasket($arg1)
    {
        throw new PendingException();
    }

     #[Then('the overall basket price should be £:arg1')]
    public function theOverallBasketPriceShouldBePs($arg1)
    {
        throw new PendingException();
    }
}

Note

We have removed the constructor and grouped I should have :arg1 product in the basket and I should have :arg1 products in the basket into one I should have :arg1 product(s) in the basket.

Automating Steps

Now it is finally time to start implementing our basket feature. The approach when you use tests to drive your application development is called a Test-Driven Development (or simply TDD). With TDD you start by defining test cases for the functionality you develop, then you fill these test cases with the best-looking application code you could come up with (use your design skills and imagination).

In the case of Behat, you already have defined test cases (step definitions in your FeatureContext) and the only thing that is missing is that best-looking application code we could come up with to fulfil our scenario. Something like this:

// features/bootstrap/FeatureContext.php

use Behat\Behat\Tester\Exception\PendingException;
use Behat\Behat\Context\SnippetAcceptingContext;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use Behat\Step\Given;
use Behat\Step\Then;
use Behat\Step\When;

class FeatureContext implements SnippetAcceptingContext
{
    private $shelf;
    private $basket;

    public function __construct()
    {
        $this->shelf = new Shelf();
        $this->basket = new Basket($this->shelf);
    }

    #[Given('there is a :arg1, which costs £:arg2')]
    public function thereIsAWhichCostsPs($product, $price)
    {
        $this->shelf->setProductPrice($product, floatval($price));
    }

    #[When('I add the :arg1 to the basket')]
    public function iAddTheToTheBasket($product)
    {
        $this->basket->addProduct($product);
    }

     #[Then('I should have :arg1 product(s) in the basket')]
    public function iShouldHaveProductInTheBasket($count)
    {
        // Normally you would import this class - we are using the fully qualified name
        // to highlight that Behat does not come with an assertion tool (see note below).
        \PHPUnit\Framework\Assert::assertCount(
            intval($count),
            $this->basket
        );
    }

     #[Then('the overall basket price should be £:arg1')]
    public function theOverallBasketPriceShouldBePs($price)
    {
        \PHPUnit\Framework\Assert::assertSame(
            floatval($price),
            $this->basket->getTotalPrice()
        );
    }
}

As you can see, in order to test and implement our application, we introduced 2 objects - Shelf and Basket. The first is responsible for storing products and their prices, the second is responsible for the representation of our customer basket. Through appropriate step definitions we declare products' prices and add products to the basket. We then compare the state of our Basket object with our expectations using PHPUnit assertions.

Note

Behat doesn't come with its own assertion tool, but you can use any proper assertion tool out there. A proper assertion tool is a library whose assertions throw exceptions on failure. For example, if you're familiar with PHPUnit you can use its assertions in Behat by installing it via composer:

$ php composer.phar require --dev phpunit/phpunit

and then by simply using assertions in your steps:

\PHPUnit\Framework\Assert::assertCount(
    intval($count),
    $this->basket
);

Now try to execute your feature tests:

$ vendor/bin/behat

You should see a beginning of the feature and then an error saying that class Shelf does not exist. It means we're ready to start writing actual application code!

Implementing the Feature

So now we have 2 very important things:

  1. A concrete user-aimed description of functionality we're trying to deliver.
  2. Set of failing tests that tell us what to do next.

Now is the easiest part of application development - feature implementation. Yes, with TDD and BDD implementation becomes a routine task, because you already did most of the job in the previous phases - you wrote tests, you came up with an elegant solution (as far as you could go in current context) and you chose the actors (objects) and actions (methods) that are involved. Now it's time to write a bunch of PHP keywords to glue it all together. Tools like Behat, when used in the right way, will help you to write this phase by giving you a simple set of instructions that you need to follow. You did your thinking and design, now it's time to sit back, run the tool and follow its instructions in order to write your production code.

Lets start! Run:

$ vendor/bin/behat

Behat will try to test your application with FeatureContext but will fail soon, producing something like this onto your screen:

Fatal error: Class 'Shelf' not found

Now our job is to reinterpret this phrase into an actionable instruction. Like "Create the Shelf class". Let's go and create it inside features/bootstrap:

// features/bootstrap/Shelf.php

final class Shelf
{
}

Note

We put the Shelf class into features/bootstrap/Shelf.php because features/bootstrap is an autoloading folder for Behat. Behat has a built-in PSR-0 autoloader, which looks into features/bootstrap. If you're developing your own application, you probably would want to put classes into a place appropriate for your app.

Let's run Behat again:

$ vendor/bin/behat

We will get different message on our screen:

Fatal error: Class 'Basket' not found

Good, we are progressing! Reinterpreting the message as, "Create the Basket class". Let's follow our new instruction:

// features/bootstrap/Basket.php

final class Basket
{
}

Run Behat again:

$ vendor/bin/behat

Great! Another "instruction":

Call to undefined method Shelf::setProductPrice()

Follow these instructions step-by-step and you will end up with Shelf class looking like this:

// features/bootstrap/Shelf.php

final class Shelf
{
    private $priceMap = array();

    public function setProductPrice($product, $price)
    {
        $this->priceMap[$product] = $price;
    }

    public function getProductPrice($product)
    {
        return $this->priceMap[$product];
    }
}

and Basket class looking like this:

// features/bootstrap/Basket.php

final class Basket implements \Countable
{
    private $shelf;
    private $products;
    private $productsPrice = 0.0;

    public function __construct(Shelf $shelf)
    {
        $this->shelf = $shelf;
    }

    public function addProduct($product)
    {
        $this->products[] = $product;
        $this->productsPrice += $this->shelf->getProductPrice($product);
    }

    public function getTotalPrice()
    {
        return $this->productsPrice
            + ($this->productsPrice * 0.2)
            + ($this->productsPrice > 10 ? 2.0 : 3.0);
    }

    public function count()
    {
        return count($this->products);
    }
}

Run Behat again:

$ vendor/bin/behat

All scenarios should pass now! Congratulations, you almost finished your first feature. The last step is to refactor. Look at the Basket and Shelf classes and try to find a way to make their code even more clean, easier to read and concise.

Tip

I would recommend starting from Basket::getTotalPrice() method and extracting VAT and delivery cost calculation in private methods.

After refactoring is done, you will have:

  1. Clearly designed and obvious code that does exactly the thing it should do without any gold plating.
  2. A regression test suite that will help you to be confident in your code going forward.
  3. Living documentation for the behaviour of your code that will live, evolve and die together with your code.
  4. An incredible level of confidence in your code. Not only are you confident now that it does exactly what it's supposed to do, you are confident that it does so by delivering value to the final users (customers in our case).

There are many more benefits to BDD but those are the key reasons why most BDD practitioners do BDD in Ruby, .Net, Java, Python and JS. Welcome to the family!

What's Next?

Congratulations! You now know everything you need in order to get started with behavior driven development and Behat. From here, you can learn more about the :doc:`Gherkin </user_guide/gherkin>` syntax or learn how to test your web applications by using Behat with Mink.