mirror of
https://github.com/silverstripe/silverstripe-behat-extension
synced 2024-10-22 17:05:32 +02:00
Webservices+Phockito+TestSession
This commit is contained in:
parent
8141ced2fd
commit
45e8d6da8d
@ -84,9 +84,10 @@ In order to run specific tests only, use their feature file name:
|
||||
|
||||
This will start a Firefox browser by default. Other browsers and profiles can be configured in `behat.yml`.
|
||||
|
||||
## Tutorial
|
||||
## Tutorials
|
||||
|
||||
See [docs/tutorial.md](docs/tutorial.md)
|
||||
* [Tutorial: Testing Form Submissions](docs/tutorial.md)
|
||||
* [Tutorial: Webservice Mocking with Phockito and TestSession](docs/webservice-mocking.md)
|
||||
|
||||
## Configuration
|
||||
|
||||
|
187
docs/webservice-mocking.md
Normal file
187
docs/webservice-mocking.md
Normal file
@ -0,0 +1,187 @@
|
||||
# Mocking SOAP Webservices with Phockito and Behat
|
||||
|
||||
## Overview
|
||||
|
||||
Web applications typically don't live in isolation: They provide data to third parties,
|
||||
as well as consume data from other services, and often even write back to those services.
|
||||
SOAP is a common web service layer to achieve this level of interaction.
|
||||
|
||||
Web services have a couple of disadvantages when it comes to testing though:
|
||||
|
||||
- They're slow down our tests, responding in seconds rather than milliseconds
|
||||
- Their data can change over time, making it hard to start with a clean slate
|
||||
- They're not isolated, meaning multiple test runs accessing a webservice in parallel can cause side effects for each other
|
||||
- Since their data isn't defined in our tests, its hard to understand the assumptions and requirements of a test.
|
||||
|
||||
This is where mocks objects come in, which replace the "real"
|
||||
webservice with a fake data defined in our test framework.
|
||||
The challenge is to get mocks defined as in-memory objects via PHP
|
||||
while executing Behat steps on the commandline, but applying those
|
||||
same mocks in a different process when Behat/Mink/Selenium
|
||||
perform a web request. That's solved by generated PHP code which is
|
||||
included as part of the bootstrap process.
|
||||
|
||||
There's several parts to this:
|
||||
|
||||
- [Behat](http://behat.org) parses features into executable steps
|
||||
- [Mink](http://mink.behat.org) interacts with [Selenium](http://selenium.googlecode.com) to remote control a browser
|
||||
- PHP's built-in `SoapClient` is used for the "real" API connection
|
||||
- A "gateway" class which encapsulates the SOAP interactions
|
||||
- [Phockito](https://github.com/hafriedlander/phockito) as a mocking framework to "hardcode" method returns
|
||||
- The `TestSessionStubCodeWriter` which writes PHP code to be included only in test runs.
|
||||
In our case it contains Phockito mock object definitions.
|
||||
|
||||
## Example: A Currency Conversion Rate Viewer
|
||||
|
||||
How the pieces fit together is best illustrated as an example.
|
||||
We'll create a currency rate viewer,
|
||||
based on a [free online webservice](http://www.webservicex.net/CurrencyConvertor.asmx?WSDL).
|
||||
The example assumes you have a basic knowledge of [Behat](http://behat.org) and
|
||||
the [Behat SilverStripe extension](https://github.com/silverstripe-labs/silverstripe-behat-extension).
|
||||
Let's explain the feature through the Gherkin language as Behat steps:
|
||||
|
||||
```feature
|
||||
Feature:
|
||||
As a website visitor
|
||||
I want to see currency conversion rates
|
||||
In order to decide whether its worth buying
|
||||
|
||||
Scenario: View conversion rates on homepage
|
||||
Given I have a currency rate from "NZD" to "EUR" of "1.56"
|
||||
And I go to "/convert?from=NZD&to=EUR"
|
||||
Then I should see "NZD -> EUR: 1.56"
|
||||
```
|
||||
|
||||
Follow the [Behat+SilverStripe installation instructions](https://github.com/silverstripe-labs/silverstripe-behat-extension), then install the Phockito mocking framework:
|
||||
|
||||
```
|
||||
composer require hafriedlander/phockito:*
|
||||
```
|
||||
|
||||
First we'll create a `CurrencyGateway` class which encapsulates a `SoapClient`
|
||||
through the [gateyway design pattern](http://martinfowler.com/eaaCatalog/gateway.html).
|
||||
It wraps each SOAP method in its own method, reading and writing native PHP types such as arrays and integers. This decouples the business logic from the underlying service layer,
|
||||
and allows us to mock the return values later without requiring access to the live webservice.
|
||||
|
||||
```php
|
||||
// mysite/code/CurrencyGateway.php
|
||||
class CurrencyGateway {
|
||||
protected $client;
|
||||
|
||||
function __construct($client = null) {
|
||||
$this->client = new SoapClient('http://www.webservicex.net/CurrencyConvertor.asmx?WSDL');
|
||||
}
|
||||
|
||||
function convert($from, $to) {
|
||||
return $this->client->ConversionRate($from, $to);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The controller logic for this is really simple.
|
||||
We'll stick to request parameters and plaintext responses just to keep the code
|
||||
manageable, a more realistic controller would likely use a form and HTML formatted responses.
|
||||
Its important that our `CurrencyGateway` is instanciated through the
|
||||
use of [dependency injection](http://doc.silverstripe.org/framework/en/trunk/reference/injector),
|
||||
so we can replace its implementation with a mock object later.
|
||||
|
||||
```php
|
||||
// mysite/code/MyController.php
|
||||
class ConvertController extends Controller {
|
||||
function index($request) {
|
||||
$gateway = Injector::inst()->get('CurrencyGateway');
|
||||
$from = $request->getVar('from');
|
||||
$to = $request->getVar('to');
|
||||
$rate = $gateway->convert($from, $to);
|
||||
$this->response->addHeader('Content-Type', 'text/plain');
|
||||
return "$from -> $to: $rate";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The controller needs to be hooked up to a route (in `mysite/_config/_config.yml`):
|
||||
|
||||
```yml
|
||||
Director:
|
||||
rules:
|
||||
'convert/$Action': 'ConvertController'
|
||||
```
|
||||
|
||||
If you haven't already, now's the time to initialize your Behat tests
|
||||
through a call to `vendor/bin/behat --init @mysite`.
|
||||
Copy the feature steps from above into a new `mysite/tests/behat/features/view-rates.feature` file.
|
||||
Open the already generated `FeatureContext.php` file and add the following code.
|
||||
|
||||
```php
|
||||
// mysite/tests/behat/features/bootstrap/Context/FeatureContext.php
|
||||
class FeatureContext extends SilverStripeContext {
|
||||
|
||||
protected $stubCodeWriter;
|
||||
|
||||
public function __construct() {
|
||||
// ...
|
||||
|
||||
$this->stubCodeWriter = Injector::inst()->get('TestSessionStubCodeWriter');
|
||||
}
|
||||
|
||||
/**
|
||||
* @BeforeScenario
|
||||
*/
|
||||
public function initTestSessionStubCode() {
|
||||
$php = <<<PHP
|
||||
\$mock = Phockito::mock('CurrencyGateway');
|
||||
Injector::inst()->registerService(\$mock, 'CurrencyGateway');
|
||||
PHP;
|
||||
$this->stubCodeWriter->write($php);
|
||||
}
|
||||
|
||||
/**
|
||||
* @AfterScenario
|
||||
*/
|
||||
public function resetTestSessionStubCode() {
|
||||
$this->stubCodeWriter->reset();
|
||||
}
|
||||
|
||||
public function getTestSessionState() {
|
||||
return array_merge(
|
||||
parent::getTestSessionState(),
|
||||
array('stubfile' => $this->stubCodeWriter->getFilePath())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^I have a currency rate from "([^"]*)" to "([^"]*)" of "([^"]*)"$/
|
||||
*/
|
||||
public function stepGivenACurrency($from, $to, $rate) {
|
||||
$php = <<<PHP
|
||||
Phockito::when(\$mock->convert('$from','$to'))->return($rate);
|
||||
PHP;
|
||||
$writer->write($php);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `TestSessionStubCodeWriter` takes care of writing out PHP to a specified file.
|
||||
It defaults to `testSessionStubCode.php` inside your webroot. The file only lives
|
||||
for the duration of a test session, and is regenerated for each scenario to
|
||||
avoid side effects (hence the methods tagged with `@BeforeScenario` and `@AfterScenario`).
|
||||
|
||||
A useful pattern here is to set up objects via `@BeforeScenario`, in our case
|
||||
a mock gateway in `initTestSessionStubCode()`. This object can be used in later
|
||||
step definitions like `stepGivenACurrency()` to mock webservice responses
|
||||
without any further setup or duplication.
|
||||
|
||||
The generated code which is executed on every web request reads:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$mock = Phockito::mock('CurrencyGateway');
|
||||
Injector::inst()->registerService($mock, 'CurrencyGateway');
|
||||
Phockito::when($mock->convert('EUR','NZD'))->return(1.56);
|
||||
```
|
||||
|
||||
Keep in mind escaping rules for PHP when placed in a heredoc block:
|
||||
Variables are resolved when the string is constructed, unless escaped with a backslash.
|
||||
|
||||
The test session started in your browser by Selenium/Behat needs to know
|
||||
which file to include, which is handled by the `getTestSessionState()` method.
|
Loading…
Reference in New Issue
Block a user