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!