Testing Features¶
We’ve already used this strangeFeatureContext class as a home for ourstep definitionsandHooks,but we haven’t done much to explain what it actually is.
Context classes are a keystone of testing environment in Behat. The contextclass is a simple POPO (Plain Old PHP Object) that tells Behat how to testyour features. If*.feature files are all about describinghow yourapplication behaves, then the context class is all about how to test it.
// features/bootstrap/FeatureContext.phpuseBehat\Behat\Context\Context;useBehat\Hook\BeforeFeature;useBehat\Step\Given;useBehat\Step\Then;useBehat\Step\When;classFeatureContextimplementsContext{publicfunction__construct($parameter){// instantiate context}#[BeforeFeature]publicstaticfunctionprepareForTheFeature(){// clean database or do other preparation stuff}#[Given('we have some context')]publicfunctionprepareContext(){// do something}#[When('event occurs')]publicfunctiondoSomeAction(){// do something}#[Then('something should be done')]publicfunctioncheckOutcomes(){// do something}}
A simple mnemonic for context classes is: “testing featuresin a context”.Feature descriptions tend to be very high level. It means there’s not muchtechnical detail exposed in them, so the way you will test thosefeatures pretty much depends on the context you test them in. That’s whatcontext classes are.
Tip
Behat can automatically generate this class by using theBehat command line tool with the--init option from your project’s directory. Behat has several built-intools that can help you when creating a new project. Learn more about“Initialize a New Behat Project”.
Context Class Requirements¶
In order to be used by Behat, your context class should follow these rules:
The context class should implement the
Behat\Behat\Context\Contextinterface.The context class should be called
FeatureContext. It’s a simple conventioninside the Behat infrastructure.FeatureContextis the name of thecontext class for the default suite. This can easily be changed throughsuite configuration insidebehat.php.The context class should be discoverable and loadable by Behat. That means youshould somehow tell Behat about your class file. Behat comes with a PSR-0autoloader out of the box and the default autoloading directory is
features/bootstrap. That’s why the defaultFeatureContextis loaded soeasily by Behat. You can place your own classes underfeatures/bootstrapby following the PSR-0 convention or you can even define your own customautoloading folder viabehat.php.
Note
Behat\Behat\Context\SnippetAcceptingContext andBehat\Behat\Context\CustomSnippetAcceptingContext are specialversions of theBehat\Behat\Context\Context interface that tellBehat this context expects snippets to be generated for it.
Tip
TheBehat command line toolhas an--init option that will initialize a new Behat project in yourdirectory. Learn more aboutInitialize a New Behat Project.
Contexts Lifetime¶
Your context class is initialized before each scenario is run, and every scenariohas its very own context instance. This means 2 things:
Every scenario is isolated from each other scenario’s context. You can doalmost anything inside your scenario context instance without the fear ofaffecting other scenarios - every scenario gets its own context instance.
Every step in a single scenario is executed inside a common contextinstance. This means you can set
privateinstance variables insideyourGivensteps and you’ll be able to read their new values insideyourWhenandThensteps.
Multiple Contexts¶
At some point, it could become very hard to maintain all yourstep definitionsandHooksinside a single class. You could use class inheritance and split definitionsinto multiple classes, but doing so could cause your code to become moredifficult to follow and use.
In light of these issues, Behat provides a more flexible way of helping makeyour code more structured by allowing you to use multiple contexts in a single testsuite.
In order to customise the list of contexts your test suite requires, you needto fine-tune the suite configuration insidebehat.php:
<?php// behat.phpuseFeatureContext;useSecondContext;useThirdContext;useBehat\Config\Config;useBehat\Config\Profile;useBehat\Config\Suite;$profile=newProfile('default')->withSuite(newSuite('default')->withContexts(FeatureContext::class,SecondContext::class,ThirdContext::class,));returnnewConfig()->withProfile($profile);
The firstdefault in this configuration is a name of the profile. Wewill discuss profiles a little bit later. Underthe specific profile, we have a specialsuites section, whichconfigures suites inside this profile. We will talk about test suites in moredetail in thenext chapter, for now just keep in mindthat a suite is a way to tell Behat where to find your features andhow to test them. The interesting part for us now is thecontextssection - this is an array of context class names. Behat will use the classesspecified there as your feature contexts. This means that every timeBehat sees a scenario in your test suite, it will:
Get list of all context classes from this
contextsoption.Will try to initialize all these context classes into objects.
Will search forstep definitions andHooks in all of them.
Note
Do not forget that each of these context classes should follow allcontext class requirements. Specifically - they all should implementBehat\Behat\Context\Context interface and be autoloadable byBehat.
Basically, all contexts under thecontexts section of yourbehat.phpare the same for Behat. It will find and use the methods in them the same wayit does in the defaultFeatureContext. And if you’re happy with a singlecontext class, but you don’t like the nameFeatureContext, here’show you change it:
<?php// behat.phpuseMyAwesomeContext;useBehat\Config\Config;useBehat\Config\Profile;useBehat\Config\Suite;$profile=newProfile('default')->withSuite(newSuite('default')->withContexts(MyAwesomeContext::class,));returnnewConfig()->withProfile($profile);
This configuration will tell Behat to look forMyAwesomeContextinstead of the defaultFeatureContext.
Note
Unlike profiles, Behat will not inherit any configuration of yourdefault suite. The namedefault is only used for demonstrationpurpose in this guide. If you have multiple suites that all should use thesame context, you will have to define that specific context for everyspecific suite:
<?php// behat.phpuseMyAwesomeContext;useMyWickedContext;useBehat\Config\Config;useBehat\Config\Profile;useBehat\Config\Suite;$profile=newProfile('default')->withSuite(newSuite('default')->withContexts(MyAwesomeContext::class,MyWickedContext::class,))->withSuite(newSuite('suite_a')->withContexts(MyAwesomeContext::class,MyWickedContext::class,))->withSuite(newSuite('suite_b')->withContexts(MyAwesomeContext::class,MyWickedContext::class,));returnnewConfig()->withProfile($profile);
This configuration will tell Behat to look forMyAwesomeContext andMyWickedContext when testingsuite_a andMyAwesomeContext whentestingsuite_b. In this example,suite_b will not be able to callsteps that are defined in theMyWickedContext. As you can see, even ifyou are using the namedefault as the name of the suite, Behat will notinherit any configuration from this suite.
Context Parameters¶
Context classes can be very flexible depending on how far you wantto go in making them dynamic. Most of us will want to make our contextsenvironment-independent; where should we put temporary files, which URLswill be used to access the application? These arecontext configuration options highly dependent on the environment youwill test your features in.
We already said that context classes are just plain old PHP classes.How would you incorporate environment-dependent parameters into yourPHP classes? Useconstructor arguments:
// features/bootstrap/MyAwesomeContext.phpuseBehat\Behat\Context\Context;classMyAwesomeContextimplementsContext{publicfunction__construct($baseUrl,$tempPath){$this->baseUrl=$baseUrl;$this->tempPath=$tempPath;}}
As a matter of fact, Behat gives you ability to do just that. You canspecify arguments required to instantiate your context classes throughsamecontexts setting inside yourbehat.php:
<?php// behat.phpuseMyAwesomeContext;useBehat\Config\Config;useBehat\Config\Profile;useBehat\Config\Suite;$profile=newProfile('default')->withSuite(newSuite('default')->addContext(MyAwesomeContext::class,['http://localhost:8080','/var/tmp',]));returnnewConfig()->withProfile($profile);
Arguments would be passed to theMyAwesomeContext constructor inthe order they were specified here. If you are not happy with the ideaof keeping an order of arguments in your head, you can use argumentnames instead:
<?php// behat.phpuseMyAwesomeContext;useBehat\Config\Config;useBehat\Config\Profile;useBehat\Config\Suite;$profile=newProfile('default')->withSuite(newSuite('default')->addContext(MyAwesomeContext::class,['baseUrl'=>'http://localhost:8080','tempPath'=>'/var/tmp',]));returnnewConfig()->withProfile($profile);
As a matter of fact, if you do, the order in which you specify thesearguments becomes irrelevant:
<?php// behat.phpuseMyAwesomeContext;useBehat\Config\Config;useBehat\Config\Profile;useBehat\Config\Suite;$profile=newProfile('default')->withSuite(newSuite('default')->addContext(MyAwesomeContext::class,['tempPath'=>'/var/tmp','baseUrl'=>'http://localhost:8080',]));returnnewConfig()->withProfile($profile);
Taking this a step further, if your context constructor arguments areoptional:
publicfunction__construct($baseUrl='http://localhost',$tempPath='/var/tmp'){$this->baseUrl=$baseUrl;$this->tempPath=$tempPath;}
You then can specify only the parameter that you actually need to change:
<?php// behat.phpuseMyAwesomeContext;useBehat\Config\Config;useBehat\Config\Profile;useBehat\Config\Suite;$profile=newProfile('default')->withSuite(newSuite('default')->addContext(MyAwesomeContext::class,['tempPath'=>'/var/tmp',]));returnnewConfig()->withProfile($profile);
In this case, the default values would be used for other parameters.
Context Traits¶
PHP 5.4 have brought an interesting feature to the language - traits.Traits are a mechanism for code reuse in single inheritance languageslike PHP. Traits are implemented as a compile-time copy-paste in PHP.That means if you put some step definitions or hooks inside a trait:
// features/bootstrap/ProductsDictionary.phptraitProductsDictionary{#[Given('there is a(n) :product, which costs £:price')]publicfunctionthereIsAWhichCostsPs($product,$price){thrownewPendingException();}}
And then use it in your context:
// features/bootstrap/MyAwesomeContext.phpuseBehat\Behat\Context\Context;classMyAwesomeContextimplementsContext{useProductsDictionary;}
It will just work as you expect it to.
Context traits come in handy if you’d like to have separate contexts,but still need to use the very same step definition in both of them. Instead ofhaving the same code in both context classes – and having to maintain itin both – you should create a single Trait that you would thenuse inboth context classes.
Note
Given that step definitionscannot be duplicated within a Suite, this will only workfor contexts used in separate suites.
In other words, if your Suite uses at least two different Contexts, andthose context classesuse the same Trait, this will result in a duplicatestep definition and Behat will complain by throwing aRedundant exception.
Behat