Written by Maciej Kosiedowski
Published May 14, 2018

How to improve the quality of your PHP code? Part 3 – End-to-end / integration testing

In the final part of our series, it’s time to set up end-to-end / integration testing environment & ensure that we’re ready to check the quality of our work.

In previous parts of this series we’ve set up a build tool, few static code analyzers and started writing unit tests. If you haven’t read those, check them out!

To make our testing stack more complete, it’s good to have some tests which will check whether your code runs with real environments and whether it will perform well in more complex business scenarios.

Here we can utilize a tool built for Behavior Driven Development – official PHP’s Cucumber implementation – Behat. You can install it by running

php composer.phar require --dev behat/behat

And, of course, adding a target to build.xml (Phing setup was described in the first part of the article)

<target name="behat">
   <exec executable="bin/behat" passthru="true" checkreturn="true"/>
</target>
…
<target name="run" depends="phpcs,phpcpd,phan,phpspec,behat"/>

Then you should create a specification for a test in file features/price.feature:

Feature: Price Comparison
  In order to compare prices
  As a customer
  I need to break the currency barrier

  Scenario: Compare EUR and PLN
    Given I use nbp.pl comparator
    When I compare “100EUR” and “100PLN”
    Then It should return some result

This test scenario is pretty easy to read and should give you a good impression of how the feature is supposed to work. Unfortunately, computers usually don’t really understand human language, so now it’s time to write the code for each step.

You can generate the code template for it by running ./bin/behat --init. It should create a class looking 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()
    {
    }
}

Then you can run:
bin/behat --dry-run --append-snippets
Behat will automatically create functions for each step defined in the scenario.
Now you can start implementing the real checks by filling the functions’ bodies:

// features/bootstrap/FeatureContext.php
<?php

use Behat\Behat\Context\Context;
use Domain\Price;
use Domain\PriceComparator;
use Infrastructure\NBPPriceConverter;

/**
* Defines application features from the specific context.
*/
class FeatureContext implements Context
{
   /** @var PriceComparator */
   private $priceComparator;

   /** @var int */
   private $result;

   /**
    * Initializes context.
    *
    * Every scenario gets its own context instance.
    * You can also pass arbitrary arguments to the
    * context constructor through behat.yml.
    */
   public function __construct()
   {
   }

   /**
    * @Given I use nbp.pl comparator
    */
   public function iUseNbpPlComparator()
   {
       $this->priceComparator = new PriceComparator(new NBPPriceConverter());
   }

   /**
    * @When I compare :price1 and :price2
    */
   public function iCompareAnd($price1, $price2)
   {
       preg_match('/(\d+)([A-Z]+)/', $price1, $match1);
       preg_match('/(\d+)([A-Z]+)/', $price2, $match2);
       $price1 = new Price($match1[1], $match1[2]);
       $price2 = new Price($match2[1], $match2[2]);
       $this->result = $this->priceComparator->compare($price1, $price2);
   }

   /**
    * @Then It should return some result
    */
   public function itShouldReturnSomeResult()
   {
       if (!is_int($this->result)) {
           throw new \DomainException('Returned value is not integer');
       }
   }
}

Finally, run all your tests using ./bin/phing. You should get the following result:

Buildfile: /home/maciej/workspace/php-testing/build.xml

MyProject > phpcs:                                                                                                                                                                                                                
                                                                                                                                                                                                                                 

MyProject > phpcpd:                                                                                                                                                                                                               
                                                                                                                                                                                                                                 
phpcpd 4.0.0 by Sebastian Bergmann.

0.00% duplicated lines out of 103 total lines of code.

Time: 17 ms, Memory: 4.00MB

MyProject > phan:                                                                                                                                                                                                                 
                                                                                                                                                                                                                                 

MyProject > phpspec:                                                                                                                                                                                                              
                                                                                                                                                                                                                                 
/  skipped: 0%  /  pending: 0%  / passed: 100%  /  failed: 0%   /  broken: 0%   /  3 examples
2 specs
3 examples (3 passed)
15ms


MyProject > behat:                                                                                                                                                                                                                
                                                                                                                                                                                                                                 
Feature: Price Comparison
 In order to compare prices
 As a customer
 I need to break the currency barrier

 Scenario: Compare EUR and PLN          # features/price.feature:6
   Given I use nbp.pl comparator        # FeatureContext::iUseNbpPlComparator()
   When I compare "100EUR" and "100PLN" # FeatureContext::iCompareAnd()
   Then It should return some result    # FeatureContext::itShouldReturnSomeResult()

1 scenario (1 passed)
3 steps (3 passed)
0m0.01s (9.13Mb)


MyProject > run:                                                                                                                                                                                                                  



BUILD FINISHED                                                                                                                                                                                                                    

Total time: 1.1000 second

As you can see, Behat prepared a nice report stating what our application did and what was the result. Next time when your project manager will ask you which scenarios you covered with tests, you can just give him a Behat output!

Structure of the test

Each test consist of:

  • Some preparation of the scenario, expressed with “Given” part
  • Some action covered by “When” part
  • Some check noted as “Then” part

Each part can contain multiple steps concatenated with “And” keyword, eg.:

Scenario: Compare EUR and PLN
    Given nbp.pl comparator is available
    And I use nbp.pl comparator
    When I compare "100EUR" and "100PLN"
    And I save the result
    Then It should return some result
    And the first amount should be greater

Contexts

Behat allows you to define multiple contexts for your tests. This means that you can split your steps code into multiple classes, as well as test your scenarios from different perspectives.

You can eg. write code for web context which will run your test steps using your application HTTP controller. You can also create “domain” context which will run your business logic using just PHP API calls. This way you can, for instance, test your business logic integration separately from end-to-end application tests.

For more information on how to set up many contexts in Behat, please refer to the documentation at http://behat.org/en/latest/user_guide/context.html.

How can I use Behat?

As mentioned in the beginning, you can use Behat for integration tests. It is often the case that your code is dependent on some external 3rd party systems. When we were writing unit tests in part II, we always assumed that external dependencies are working as expected. With Behat you can write tests scenarios which will automatically run your code and check if it behaves correctly with real-world services.

What is most important, Behat is great for testing complex end-to-end scenarios of your system’s usage. It allows you to hide the complex code required to run test’s step behind a nice human readable name and write a scenario which everyone will understand.

Hooray!

You just learned how to set up six useful tools in your project:

  • PHing for running your builds
  • PHPCS for automatically checking your code style
  • PHPCPD for detecting duplicated code
  • Phan for advanced static code analysis
  • PHPSpec for unit testing
  • Behat for end-to-end and integration testing

Now, you can add ./bin/phing to your git commit hooks and set up your Continuous Integration to run tests with every commit. Suddenly nothing stops you now from being able to write quality PHP code! Well done!

Written by Maciej Kosiedowski
Published May 14, 2018