├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── behat.yml.dist ├── composer.json ├── docker-compose.yml ├── features ├── bootstrap │ └── SampleContext.php └── sample.feature ├── license.txt ├── nginx-site.conf ├── ruleset.xml ├── screenshot.png ├── src ├── RestTestingContext │ ├── BaseContext.php │ └── RestContext.php └── RestTestingExtension │ ├── Context │ ├── Initializer │ │ └── RestTestingAwareInitializer.php │ ├── RestTestingAwareContext.php │ └── RestTestingContext.php │ ├── RestTestingHelper.php │ └── ServiceContainer │ └── RestTestingExtension.php └── www ├── .htaccess ├── router.php └── sample.employees.json /.gitignore: -------------------------------------------------------------------------------- 1 | /behat.yml 2 | /www/employees.* 3 | 4 | # Ignore Zend Studio and Eclipse project related files. 5 | /.buildpath 6 | /.project 7 | /.settings 8 | 9 | # Ignore PhpStorm project related files. 10 | /.idea 11 | 12 | # Ignore files generated by Composer. 13 | /composer.lock 14 | /composer.phar 15 | /vendor 16 | 17 | # Ignore the empty folder created by Docker image richarvey/nginx-php-fpm 18 | # @see https://gitlab.com/ric_harvey/nginx-php-fpm/issues/243 19 | /www/[ 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.3 5 | - 7.2 6 | - 7.1 7 | - 7.0 8 | - 5.6 9 | - 5.5 10 | - 5.4 11 | 12 | services: 13 | - docker 14 | 15 | env: 16 | - BEHAT_VERSION="3.0.*" 17 | - BEHAT_VERSION="3.1.*" 18 | - BEHAT_VERSION="3.2.*" 19 | - BEHAT_VERSION="3.3.*" 20 | 21 | before_install: 22 | - touch www/employees.json && chmod 777 www/employees.json 23 | - docker-compose up -d 24 | - docker ps -a 25 | 26 | install: 27 | - composer require behat/behat:${BEHAT_VERSION} 28 | - if [ $(phpenv version-name) == "7.2" ]; then composer install --no-interaction --prefer-source; else composer install --no-interaction --prefer-source --no-dev; fi 29 | - composer show -i 30 | - vendor/bin/behat --version 31 | 32 | script: 33 | - vendor/bin/behat 34 | - if [ $(phpenv version-name) == "7.2" ]; then ./vendor/bin/phpcs -v --standard=ruleset.xml features src www; fi 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 3.0.3 / TDB 2 | =========== 3 | 4 | 3.0.2 / 2018-12-17 5 | ================== 6 | 7 | * [#6] fix a broken step definition. Thanks @lfbn 8 | * Run tests under PHP 7.2 and PHP 7.3. 9 | * Use Docker to run tests. 10 | 11 | 3.0.1 / 2017-03-16 12 | ================== 13 | 14 | * Add support for Symfony 3. 15 | * Sample code for adding HTTP headers when testing API calls. 16 | * Add Travis CI test environments PHP 7.1, Behat 3.2 and Behat 3.3. 17 | 18 | 3.0.0 / 2016-01-09 19 | ================== 20 | 21 | * Add support for Behat 3. 22 | * Add Behat 3 extension RestTestingExtension. 23 | * Add PHP 7 and HHVM environments when running Travis CI. 24 | * Use JSON format instead of PHP serialized string when storing test data. 25 | * Support to be used standalone or as standard Composer/Packagist package. 26 | * Sample Behat configurations and features included. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | [![Build Status](https://travis-ci.org/deminy/behat-rest-testing.svg?branch=master)](https://travis-ci.org/deminy/behat-rest-testing) 3 | [![Latest Stable Version](https://poser.pugx.org/deminy/behat-rest-testing/v/stable.svg)](https://packagist.org/packages/deminy/behat-rest-testing) 4 | [![Latest Unstable Version](https://poser.pugx.org/deminy/behat-rest-testing/v/unstable.svg)](https://packagist.org/packages/deminy/behat-rest-testing) 5 | [![License](https://poser.pugx.org/deminy/behat-rest-testing/license.svg)](https://packagist.org/packages/deminy/behat-rest-testing) 6 | 7 | This repo is to help developers to easily understand how to do feature tests with Behat, and to start writing feature 8 | tests for REST APIs, with following features included: 9 | 10 | * Core contexts/steps for testing REST APIs. 11 | * Sample RESTful services, and sample feature tests against the services. 12 | * Best of all: To start writing feature tests for the project you are working on, you may use this repo in your project 13 | via _Composer_ if you happen to use _Composer_ to manage 3rd-party libraries. 14 | 15 | **NOTE**: Following instructions focus on Behat 3.0.6+ and PHP 5.4+. If you use Behat 2.x and/or PHP 5.3 (5.3.3+), 16 | please check branch "[1.x](https://github.com/deminy/behat-rest-testing/tree/1.x)" for details. 17 | 18 | # Dependencies 19 | 20 | ## Branch master 21 | 22 | * [PHP](http://www.php.net) 5.4+ 23 | * [Behat](https://github.com/Behat/Behat) 3.0.6 to 3.3.x 24 | * [Behat Web API Extension](https://github.com/Behat/WebApiExtension) 25 | 26 | ## Branch 1.x (old releases for Behat 2.x) 27 | 28 | * [PHP](http://www.php.net) 5.3.3+ 29 | * [Behat](https://github.com/Behat/Behat) >=2.4.0, <=3.0.0. 30 | 31 | # Installation - Source 32 | 33 | You will need to download _Composer_ and run the install command under the same directory where the 'composer.json' 34 | file is located: 35 | 36 | ```bash 37 | curl -s http://getcomposer.org/installer | php && ./composer.phar install 38 | ``` 39 | 40 | # Installation - Composer 41 | 42 | You may also install using [Composer](https://github.com/composer/composer) if you want to use this repo in your own 43 | project. 44 | 45 | Step 1. Add the repo as a dependency. 46 | 47 | ```json 48 | "require": { 49 | "deminy/behat-rest-testing": "~3.0.0" 50 | } 51 | ``` 52 | 53 | **NOTE**: This is for running with Behat 3 only. If you use Behat 2.x, please check 54 | [installation instructions for v1.x](https://github.com/deminy/behat-rest-testing/blob/1.x/README.md) for details. 55 | 56 | Step 2. Run Composer: `php composer.phar install`. 57 | 58 | # How to Test 59 | 60 | ## 1. Set up and run REST API server. 61 | 62 | You can have a virtual host set up under Apache, with DocumentRoot set to "www/" of this repo and DirectoryIndex set 63 | to "router.php". Please make sure that module mod_rewrite is enabled, otherwise the REST server won't be able to handle 64 | requests properly. You may also need to update option "base_url" in the configuration file "behat.yml". 65 | 66 | Alternatively, you may consider to use the 67 | [PHP 5.4+ built-in web server](http://php.net/manual/en/features.commandline.webserver.php). 68 | 69 | To start the REST API server using PHP 5.4+ built-in web server, please run command similar to following: 70 | 71 | ```bash 72 | php -S localhost:8081 www/router.php 73 | ``` 74 | 75 | The web server now serves as the REST API server. You can visit URL http://localhost:8081 to see if the server runs 76 | properly or not (If everything is good, the URL should return string "OK" back). 77 | 78 | ## 2. Create the configuration file "behat.yml" (optional). 79 | 80 | For the sample test provided, you can create the file by copying directly from file "behat.yml.dist" without any 81 | modifications required. 82 | 83 | Note that you don't have to do this if you prefer to use file "behat.yml.dist" directly. 84 | 85 | ## 3. Run the test command. 86 | 87 | Now, run following command to test sample features: 88 | 89 | ```bash 90 | ./vendor/bin/behat 91 | # OR 92 | ./vendor/bin/behat -p default # explicitly to use profile "default" 93 | ``` 94 | 95 | If everything is good, you should see the output as in following screenshot: 96 | 97 | ![output when running Behat sample tests](https://raw.github.com/deminy/behat-rest-testing/master/screenshot.png "") 98 | 99 | # TODOs 100 | 101 | * Support different environments (development, QA, staging, production, etc). 102 | 103 | # License 104 | 105 | MIT license. 106 | -------------------------------------------------------------------------------- /behat.yml.dist: -------------------------------------------------------------------------------- 1 | # behat.yml 2 | # Behat configuration file for Behat 3.0.0+. 3 | # For more information on the configuration file behat.yml, please read http://behat.readthedocs.org/en/v3.0/ 4 | # For more information on YAML file format, please read http://en.wikipedia.org/wiki/YAML 5 | 6 | default: 7 | autoload: 8 | '': %paths.base%/features/bootstrap 9 | # formatters: 10 | # progress: ~ 11 | extensions: 12 | Behat\WebApiExtension: 13 | base_url: http://127.0.0.1 14 | Behat\RestTestingExtension: 15 | suites: 16 | default: 17 | paths: [ %paths.base%/features ] # Where Behat will look for your *.feature files. 18 | contexts: 19 | - Behat\WebApiExtension\Context\WebApiContext 20 | - Behat\RestTestingExtension\Context\RestTestingContext 21 | - Behat\RestTestingContext\RestContext 22 | - SampleContext 23 | 24 | # This 2nd profile is not a working copy (no features defined for it), and is just for demonstration purpose. 25 | another_profile: 26 | autoload: 27 | '': %paths.base%/features/bootstrap 28 | extensions: 29 | Behat\WebApiExtension: 30 | base_url: http://127.0.0.1 31 | suites: 32 | other_features: 33 | paths: 34 | - %paths.base%/features 35 | contexts: [ 'Behat\WebApiExtension\Context\WebApiContext', 'SampleContext' ] 36 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deminy/behat-rest-testing", 3 | "description": "Demo on doing feature tests with Behat 3 and how to start writing feature tests for REST APIs.", 4 | "keywords": [ 5 | "PHP", 6 | "Behat", 7 | "REST" 8 | ], 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Demin Yin", 13 | "email": "deminy@deminy.net", 14 | "homepage": "http://deminy.net" 15 | } 16 | ], 17 | "require": { 18 | "php": ">=5.4.0", 19 | "behat/behat": ">=3.0.6 <=3.4.0", 20 | "behat/web-api-extension" : "dev-master" 21 | }, 22 | "require-dev": { 23 | "squizlabs/php_codesniffer" : ">=2.0" 24 | }, 25 | "autoload" : { 26 | "psr-4" : { 27 | "Behat\\RestTestingContext\\" : "src/RestTestingContext/", 28 | "Behat\\RestTestingExtension\\" : "src/RestTestingExtension/" 29 | } 30 | }, 31 | "extra": { 32 | "branch-alias": { 33 | "dev-master": "3.0.x-dev" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | web: 5 | image: richarvey/nginx-php-fpm:latest 6 | ports: 7 | - "80:80" 8 | volumes: 9 | - ./nginx-site.conf:/etc/nginx/sites-available/default.conf 10 | - ./www:/var/www/html 11 | -------------------------------------------------------------------------------- /features/bootstrap/SampleContext.php: -------------------------------------------------------------------------------- 1 | iSetHeaderWithValue('Accept', 'application/json'); 37 | } 38 | 39 | /** 40 | * @AfterScenario 41 | */ 42 | public static function afterScenario() 43 | { 44 | } 45 | 46 | /** 47 | * @Then /^value "([^"]+)" should be an?( positive)? (int|integer).?$/ 48 | * @param string $value 49 | * @param string $fieldProperty 50 | * @param string $fieldType 51 | * @return void 52 | * @throws \Exception 53 | */ 54 | public function typeOfTheFieldIs($value, $fieldProperty, $fieldType) 55 | { 56 | switch (strtolower($fieldType)) { 57 | case 'int': 58 | case 'integer': 59 | if (empty($fieldProperty)) { 60 | $regex = '/^(0|[1-9]\d*)$/'; 61 | } elseif (strpos($fieldProperty, 'positive') !== false) { 62 | $regex = '/^[1-9]\d*$/'; 63 | } else { 64 | throw new \Exception('Unsupported field property: ' . $fieldProperty); 65 | } 66 | 67 | if (! preg_match($regex, $value)) { 68 | throw new \Exception( 69 | sprintf( 70 | 'Value "%s" is not of the correct type: %s %s!', 71 | $value, 72 | $fieldProperty, 73 | $fieldType 74 | ) 75 | ); 76 | } 77 | // TODO: We didn't check if the value is as expected here. 78 | break; 79 | default: 80 | throw new \Exception('Unsupported data type: ' . $fieldType); 81 | break; 82 | } 83 | } 84 | 85 | /** 86 | * @return string 87 | */ 88 | protected static function getDataFile() 89 | { 90 | return __DIR__ . '/../../www/employees.json'; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /features/sample.feature: -------------------------------------------------------------------------------- 1 | Feature: Testing sample REST services 2 | In order to maintain user information through the services 3 | as a service user 4 | I want to see if the services work as expected 5 | 6 | Scenario: Creating a New Employee 7 | When I send a POST request to "/employee" with values: 8 | | employeeId | 7 | 9 | | name | James Bond | 10 | | age | 27 | 11 | # Next step will add "Accept-Charset: utf-8" in HTTP header when making the API call. The header doesn't 12 | # have any actual effect on the APIs nor the tests; we have it included/listed here just for demonstration, 13 | # in case you need to know how to add HTTP headers when testing API calls. 14 | And I set header "Accept-Charset" with value "utf-8" 15 | Then response code should be 200 16 | And the response should be "true" 17 | 18 | Scenario: Finding an Existing Employee 19 | When I send a GET request to "/employee/7" 20 | Then response code should be 200 21 | And the response should contain json: 22 | """" 23 | { 24 | "name": "James Bond", 25 | "age": "27" 26 | } 27 | """" 28 | And field "age" in the response should be an integer "27" 29 | And in the response there is no field called "gender" 30 | 31 | Scenario: Updating an Existing Employee 32 | When I send a PUT request to "/employee/7" with values: 33 | | age | 38 | 34 | Then response code should be 200 35 | And the response should be "true" 36 | 37 | Scenario: Deleting Existing and Non-existing Employees 38 | Given I send a DELETE request to "/employee/7" 39 | Then response code should be 200 40 | And the response should be "true" 41 | Given I send a DELETE request to "/employee/8" 42 | Then response code should be 400 43 | And the response should contain "Unable to delete because the employee does not exist." 44 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) <2013> Demin Yin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | Developer’s Certificate of Origin 1.1 23 | 24 | By making a contribution to this project, I certify that: 25 | 26 | (a) The contribution was created in whole or in part by me and I 27 | have the right to submit it under the open source license 28 | indicated in the file; or 29 | 30 | (b) The contribution is based upon previous work that, to the best 31 | of my knowledge, is covered under an appropriate open source 32 | license and I have the right under that license to submit that 33 | work with modifications, whether created in whole or in part 34 | by me, under the same open source license (unless I am 35 | permitted to submit under a different license), as indicated 36 | in the file; or 37 | 38 | (c) The contribution was provided directly to me by some other 39 | person who certified (a), (b) or (c) and I have not modified 40 | it. 41 | 42 | (d) I understand and agree that this project and the contribution 43 | are public and that a record of the contribution (including all 44 | personal information I submit with it, including my sign-off) is 45 | maintained indefinitely and may be redistributed consistent with 46 | this project or the open source license(s) involved. -------------------------------------------------------------------------------- /nginx-site.conf: -------------------------------------------------------------------------------- 1 | # This Nginx configuration file is based on https://gitlab.com/ric_harvey/nginx-php-fpm/blob/master/conf/nginx-site.conf 2 | server { 3 | server_name _; 4 | listen 80; 5 | listen [::]:80 default ipv6only=on; 6 | 7 | error_log /dev/stdout info; 8 | access_log /dev/stdout; 9 | 10 | root /var/www/html; 11 | index router.php; 12 | try_files $uri /router.php =404; 13 | 14 | location / { 15 | fastcgi_pass unix:/var/run/php-fpm.sock; 16 | fastcgi_index router.php; 17 | fastcgi_param SCRIPT_FILENAME $document_root/router.php; 18 | fastcgi_param SCRIPT_NAME $fastcgi_script_name; 19 | include fastcgi_params; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Custimized PSR2 coding standard for Behat-Rest-Testing 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 0 58 | 59 | 60 | 61 | 62 | 63 | features/bootstrap/*Context.php 64 | 65 | 66 | 67 | www/router.php 68 | 69 | 70 | 71 | www/router.php 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deminy/behat-rest-testing/63af4f233769b2cbb26999debfc4349aa5e063dd/screenshot.png -------------------------------------------------------------------------------- /src/RestTestingContext/BaseContext.php: -------------------------------------------------------------------------------- 1 | 5 | * @license MIT license 6 | */ 7 | 8 | namespace Behat\RestTestingContext; 9 | 10 | use Behat\Behat\Context\Context; 11 | use Behat\Behat\Context\SnippetAcceptingContext; 12 | use Behat\RestTestingExtension\Context\RestTestingAwareContext; 13 | use Behat\RestTestingExtension\RestTestingHelper; 14 | use Behat\WebApiExtension\Context\WebApiContext; 15 | use GuzzleHttp\Message\ResponseInterface; 16 | use GuzzleHttp\Stream\Stream; 17 | 18 | /** 19 | * Base context. 20 | */ 21 | class BaseContext implements RestTestingAwareContext, SnippetAcceptingContext 22 | { 23 | /** 24 | * @var RestContext 25 | */ 26 | protected static $restContext; 27 | 28 | /** 29 | * @var WebApiContext 30 | */ 31 | protected static $webApiContext; 32 | 33 | /** 34 | * @var Context[] 35 | */ 36 | protected static $contexts = array(); 37 | 38 | /** 39 | * Store data used across different contexts and steps. 40 | * 41 | * @var array 42 | */ 43 | protected static $data = array(); 44 | 45 | /** 46 | * @param array $params 47 | */ 48 | public function __construct(array $params = array()) 49 | { 50 | $this->addContext(); 51 | } 52 | 53 | /** 54 | * Get data by field name, or return all data if no field name provided. 55 | * 56 | * @param string $name Field name. 57 | * @return mixed 58 | * @throws \Exception 59 | */ 60 | public static function get($name = null) 61 | { 62 | if (!isset($name)) { 63 | return self::$data; 64 | } else { 65 | if (static::exists($name)) { 66 | return self::$data[$name]; 67 | } else { 68 | throw new \Exception("Requested data field '{$name}' not exist."); 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Set value on given field name. 75 | * 76 | * @param string $name Field name. 77 | * @param mixed $value Field value. 78 | * @return void 79 | */ 80 | public static function set($name, $value) 81 | { 82 | self::$data[$name] = $value; 83 | } 84 | 85 | /** 86 | * Check if specified field name exists or not. 87 | * 88 | * @param string $name Field name. 89 | * @return boolean 90 | */ 91 | public static function exists($name) 92 | { 93 | return array_key_exists($name, self::$data); 94 | } 95 | 96 | /** 97 | * @param $name 98 | * @return mixed 99 | * @throws Exception 100 | */ 101 | public function __get($name) 102 | { 103 | return self::get($name); 104 | } 105 | 106 | /** 107 | * @param string $name 108 | * @return mixed $value 109 | * @return $this 110 | * @throws Exception 111 | */ 112 | public function __set($name, $value) 113 | { 114 | self::set($name, $value); 115 | 116 | return $this; 117 | } 118 | 119 | /** 120 | * @param Context $context 121 | * @return $this 122 | */ 123 | protected function addContext(Context $context = null) 124 | { 125 | $context = $context ?: $this; 126 | self::$contexts[get_class($context)] = $context; 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * @param string $name 133 | * @return Context|null 134 | */ 135 | protected function getContext($name) 136 | { 137 | return (array_key_exists($name, self::$contexts) ? self::$contexts[$name] : null); 138 | } 139 | 140 | /** 141 | * @return ResponseInterface 142 | */ 143 | protected function getResponse() 144 | { 145 | return RestTestingHelper::getProperty(self::getWebApiContext(), 'response'); 146 | } 147 | 148 | /** 149 | * @param ResponseInterface $response 150 | * @return void 151 | */ 152 | protected function setResponse(ResponseInterface $response) 153 | { 154 | RestTestingHelper::setProperty(self::getWebApiContext(), 'response', $response); 155 | } 156 | 157 | /** 158 | * @param string $body 159 | * @return void 160 | */ 161 | protected function setResponseBody($body) 162 | { 163 | self::getResponse()->setBody(Stream::factory($body)); 164 | } 165 | 166 | /** 167 | * @return RestContext 168 | */ 169 | public static function getRestContext() 170 | { 171 | return self::$restContext; 172 | } 173 | 174 | /** 175 | * @param RestContext $restContext 176 | * @return void 177 | */ 178 | public static function setRestContext(RestContext $restContext) 179 | { 180 | self::$restContext = $restContext; 181 | } 182 | 183 | /** 184 | * @return WebApiContext 185 | */ 186 | public static function getWebApiContext() 187 | { 188 | return self::$webApiContext; 189 | } 190 | 191 | /** 192 | * @param WebApiContext $webApiContext 193 | * @return void 194 | */ 195 | public static function setWebApiContext(WebApiContext $webApiContext) 196 | { 197 | self::$webApiContext = $webApiContext; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/RestTestingContext/RestContext.php: -------------------------------------------------------------------------------- 1 | 5 | * @license MIT license 6 | */ 7 | 8 | namespace Behat\RestTestingContext; 9 | 10 | use Behat\Behat\Context\Step; 11 | use Exception; 12 | 13 | /** 14 | * Rest context. 15 | */ 16 | class RestContext extends BaseContext 17 | { 18 | /** 19 | * @Given /^the response should contain field "([^"]*)"$/ 20 | * @param string $name 21 | * @return void 22 | * @throws Exception 23 | */ 24 | public function theResponseHasAField($name) 25 | { 26 | if (!array_key_exists($name, $this->getResponseData())) { 27 | throw new Exception("Field '{$name}' not found in response."); 28 | } 29 | } 30 | 31 | /** 32 | * @Then /^in the response there is no field called "([^"]*)"$/ 33 | * @param string $name 34 | * @return void 35 | * @throws Exception 36 | */ 37 | public function theResponseShouldNotHaveAField($name) 38 | { 39 | if (array_key_exists($name, $this->getResponseData())) { 40 | throw new Exception("Field '{$name}' should not have been found in response, but was."); 41 | } 42 | } 43 | 44 | /** 45 | * @Then /^field "([^"]+)" in the response should be "([^"]*)"$/ 46 | * @param string $name 47 | * @param string $value 48 | * @return void 49 | * @throws Exception 50 | */ 51 | public function valueOfTheFieldEquals($name, $value) 52 | { 53 | $this->theResponseHasAField($name); 54 | 55 | $responseData = $this->getResponseData(); 56 | if ($responseData[$name] != $value) { 57 | throw new Exception( 58 | sprintf( 59 | 'Value "%s" was expected for field "%s", but value "%s" found instead.', 60 | $value, 61 | $name, 62 | $responseData[$name] 63 | ) 64 | ); 65 | } 66 | } 67 | 68 | /** 69 | * @Then /^field "([^"]+)" in the response should be an? (int|integer) "([^"]*)"$/ 70 | * @param string $name 71 | * @param string $type 72 | * @param string $value 73 | * @return void 74 | * @throws Exception 75 | * @todo Need to be better designed. 76 | */ 77 | public function fieldIsOfTypeWithValue($name, $type, $value) 78 | { 79 | $this->valueOfTheFieldEquals($name, $value); 80 | 81 | switch (strtolower($type)) { 82 | case 'int': 83 | case 'integer': 84 | if (!preg_match('/^(0|[1-9]\d*)$/', $value)) { 85 | throw new Exception( 86 | sprintf( 87 | 'Field "%s" is not of the correct type: %s!', 88 | $name, 89 | $type 90 | ) 91 | ); 92 | } 93 | // TODO: We didn't check if the value is as expected here. 94 | break; 95 | default: 96 | throw new Exception('Unsupported data type: ' . $type); 97 | break; 98 | } 99 | } 100 | 101 | /** 102 | * @Given /^the response should be "([^"]*)"$/ 103 | * @param string $string 104 | * @return void 105 | * @throws Exception 106 | */ 107 | public function theResponseShouldBe($string) 108 | { 109 | $body = $this->getResponseBody(); 110 | if ($body !== $string) { 111 | throw new Exception("'{$string}' was expected for response body, but '{$body}' found instead."); 112 | } 113 | } 114 | 115 | /** 116 | * @return string 117 | */ 118 | public function getResponseBody() 119 | { 120 | return (string) $this->getResponse()->getBody(); 121 | } 122 | 123 | /** 124 | * @return mixed 125 | */ 126 | public function getResponseData() 127 | { 128 | return $this->decodeJson($this->getResponseBody()); 129 | } 130 | 131 | /** 132 | * Decode JSON string. 133 | * 134 | * @param string $string A JSON string. 135 | * @return mixed 136 | * @throws Exception 137 | * @see http://www.php.net/json_last_error 138 | */ 139 | protected function decodeJson($string) 140 | { 141 | $json = json_decode($string, true); 142 | 143 | switch (json_last_error()) { 144 | case JSON_ERROR_NONE: 145 | return $json; 146 | break; 147 | case JSON_ERROR_DEPTH: 148 | $message = 'Maximum stack depth exceeded'; 149 | break; 150 | case JSON_ERROR_STATE_MISMATCH: 151 | $message = 'Underflow or the modes mismatch'; 152 | break; 153 | case JSON_ERROR_CTRL_CHAR: 154 | $message = 'Unexpected control character found'; 155 | break; 156 | case JSON_ERROR_SYNTAX: 157 | $message = 'Syntax error, malformed JSON'; 158 | break; 159 | case JSON_ERROR_UTF8: 160 | $message = 'Malformed UTF-8 characters, possibly incorrectly encoded'; 161 | break; 162 | default: 163 | $message = 'Unknown error'; 164 | break; 165 | } 166 | 167 | throw new Exception('JSON decoding error: ' . $message); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/RestTestingExtension/Context/Initializer/RestTestingAwareInitializer.php: -------------------------------------------------------------------------------- 1 | 5 | * @license MIT license 6 | */ 7 | 8 | namespace Behat\RestTestingExtension\Context\Initializer; 9 | 10 | use Behat\Behat\Context\Context; 11 | use Behat\Behat\Context\Initializer\ContextInitializer; 12 | use Behat\RestTestingContext\BaseContext; 13 | use Behat\RestTestingContext\RestContext; 14 | use Behat\WebApiExtension\Context\WebApiContext; 15 | 16 | /** 17 | * RestTesting-aware contexts initializer. 18 | * 19 | * Make RestTestingContext accessable in all RestTestingContext contexts. 20 | */ 21 | class RestTestingAwareInitializer implements ContextInitializer 22 | { 23 | /** 24 | * Initializes provided context. 25 | * 26 | * @param Context $context 27 | * @return void 28 | */ 29 | public function initializeContext(Context $context) 30 | { 31 | if ($context instanceof WebApiContext) { 32 | BaseContext::setWebApiContext($context); 33 | } elseif ($context instanceof RestContext) { 34 | BaseContext::setRestContext($context); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/RestTestingExtension/Context/RestTestingAwareContext.php: -------------------------------------------------------------------------------- 1 | 5 | * @license MIT license 6 | */ 7 | 8 | namespace Behat\RestTestingExtension\Context; 9 | 10 | use Behat\Behat\Context\Context; 11 | 12 | /** 13 | * Guzzle Client-aware interface for contexts. 14 | * 15 | * @see RestTestingAwareInitializer 16 | */ 17 | interface RestTestingAwareContext extends Context 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/RestTestingExtension/Context/RestTestingContext.php: -------------------------------------------------------------------------------- 1 | 5 | * @license MIT license 6 | */ 7 | 8 | namespace Behat\RestTestingExtension\Context; 9 | 10 | /** 11 | * Provides web API description definitions. 12 | */ 13 | class RestTestingContext implements RestTestingAwareContext 14 | { 15 | } 16 | -------------------------------------------------------------------------------- /src/RestTestingExtension/RestTestingHelper.php: -------------------------------------------------------------------------------- 1 | 5 | * @license MIT license 6 | */ 7 | 8 | namespace Behat\RestTestingExtension; 9 | 10 | /** 11 | * The RestTestingHelper class. 12 | */ 13 | class RestTestingHelper 14 | { 15 | /** 16 | * Finds a property for the given class. 17 | * 18 | * @param object|string $class The class instance or name. 19 | * @param string $name The name of a property. 20 | * @param boolean $access Make the property accessible? 21 | * @return ReflectionProperty The property. 22 | * @throws Exception If the property does not exist. 23 | */ 24 | public static function findProperty($class, $name, $access = true) 25 | { 26 | $reflection = new \ReflectionClass($class); 27 | 28 | while (! $reflection->hasProperty($name)) { 29 | if (! ($reflection = $reflection->getParentClass())) { 30 | throw new Exception(sprintf('Class "%s" does not have property "%s" defined.', $class, $name)); 31 | } 32 | } 33 | 34 | $property = $reflection->getProperty($name); 35 | $property->setAccessible($access); 36 | 37 | return $property; 38 | } 39 | 40 | /** 41 | * Returns the current value of a property. 42 | * 43 | * @param object|string $class The class instance or name. 44 | * @param string $name The name of a property. 45 | * @return mixed The current value of the property. 46 | */ 47 | public static function getProperty($class, $name) 48 | { 49 | return static::findProperty($class, $name)->getValue(is_object($class) ? $class : null); 50 | } 51 | 52 | /** 53 | * Sets the new value of a property. 54 | * 55 | * @param object|string $class The class instance or name. 56 | * @param string $name The name of a property. 57 | * @param mixed $value The new value. 58 | * @return void 59 | */ 60 | public static function setProperty($class, $name, $value) 61 | { 62 | static::findProperty($class, $name)->setValue(is_object($class) ? $class : null, $value); 63 | } 64 | 65 | /** 66 | * A helper method for testing protected/private static/non-static methods. 67 | * 68 | * @param string $className 69 | * @param string $methodName 70 | * @return \ReflectionMethod 71 | */ 72 | public static function getMethod($className, $methodName) 73 | { 74 | $class = new \ReflectionClass($className); 75 | $method = $class->getMethod($methodName); 76 | $method->setAccessible(true); 77 | 78 | return $method; 79 | } 80 | 81 | /** 82 | * Call a protected/private static/non-static method of given class. 83 | * 84 | * @param string|object $class 85 | * @param string $methodName 86 | * @param array $args 87 | * @return mixed 88 | */ 89 | public static function callMethod($class, $methodName, array $args = array()) 90 | { 91 | $method = self::getMethod((is_object($class) ? get_class($class) : $class), $methodName); 92 | $class = is_object($class) ? $class : (new $class()); 93 | 94 | return $method->invokeArgs($class, $args); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/RestTestingExtension/ServiceContainer/RestTestingExtension.php: -------------------------------------------------------------------------------- 1 | 4 | * @license MIT license 5 | */ 6 | 7 | namespace Behat\RestTestingExtension\ServiceContainer; 8 | 9 | use Behat\Behat\Context\ServiceContainer\ContextExtension; 10 | use Behat\Testwork\ServiceContainer\Extension as ExtensionInterface; 11 | use Behat\Testwork\ServiceContainer\ExtensionManager; 12 | use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; 13 | use Symfony\Component\DependencyInjection\ContainerBuilder; 14 | use Symfony\Component\DependencyInjection\Definition; 15 | use Symfony\Component\DependencyInjection\Reference; 16 | 17 | /** 18 | * Web API extension for Behat. 19 | */ 20 | class RestTestingExtension implements ExtensionInterface 21 | { 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function getConfigKey() 26 | { 27 | return 'rest_testing'; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function initialize(ExtensionManager $extensionManager) 34 | { 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function configure(ArrayNodeDefinition $builder) 41 | { 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function load(ContainerBuilder $container, array $config) 48 | { 49 | $this->loadContextInitializer($container, $config); 50 | } 51 | 52 | /** 53 | * @param ContainerBuilder $container 54 | * @param array $config 55 | * @return void 56 | */ 57 | private function loadContextInitializer(ContainerBuilder $container, array $config) 58 | { 59 | $definition = new Definition( 60 | 'Behat\RestTestingExtension\Context\Initializer\RestTestingAwareInitializer', 61 | array( 62 | $config, 63 | ) 64 | ); 65 | $definition->addTag(ContextExtension::INITIALIZER_TAG); 66 | $container->setDefinition('rest_testing.context_initializer', $definition); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function process(ContainerBuilder $container) 73 | { 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /www/.htaccess: -------------------------------------------------------------------------------- 1 | DirectoryIndex router.php 2 | 3 | 4 | RewriteEngine on 5 | RewriteBase / 6 | RewriteCond %{REQUEST_FILENAME} !-f 7 | RewriteCond %{REQUEST_FILENAME} !-d 8 | RewriteRule ^ router.php [L] 9 | 10 | -------------------------------------------------------------------------------- /www/router.php: -------------------------------------------------------------------------------- 1 | 4 | * @license MIT license 5 | */ 6 | 7 | /** 8 | * This script simulates 4 types REST services (GET, POST, PUT and DELETE), manipulating employee data which are stored 9 | * in file "employees.json" in JSON format: 10 | * { 11 | * "7" : { 12 | * "name" : "James Bond", 13 | * "age" : 27 14 | * } 15 | * } 16 | */ 17 | 18 | /** 19 | * Handle bad HTTP request. 20 | * 21 | * @param string $message Message to be returned for a bad HTTP request. 22 | * 23 | * @return void 24 | */ 25 | function badRequest($message) 26 | { 27 | header('HTTP/1.1 400 Bad Request'); 28 | exit($message); 29 | } 30 | 31 | $file = __DIR__ . '/employees.json'; 32 | 33 | // Get all employees information. 34 | $data = is_readable($file) ? file_get_contents($file) : null; 35 | $employees = !empty($data) ? json_decode($data, true) : array(); 36 | 37 | // Validate request URL. 38 | switch ($_SERVER['REQUEST_METHOD']) { 39 | case 'GET': 40 | if (in_array($_SERVER['REQUEST_URI'], array('', '/'))) { 41 | exit('OK'); 42 | } 43 | // NOTE: no break statement here. 44 | case 'PUT': 45 | // For PUT requests, variable $_REQUEST might always be empty when using PHP 5.4+ built-in web server. 46 | $requestData = json_decode(file_get_contents('php://input'), true); 47 | // NOTE: No break statement here. 48 | case 'DELETE': 49 | if (!preg_match('#^/employee/(\d+)$#', $_SERVER['REQUEST_URI'], $matches)) { 50 | badRequest('Bad REST request.'); 51 | } else { 52 | $employeeId = (int) $matches[1]; 53 | } 54 | break; 55 | case 'POST': 56 | if ('/employee' != $_SERVER['REQUEST_URI']) { 57 | badRequest('Bad REST request.'); 58 | } else { 59 | $requestData = json_decode(file_get_contents('php://input'), true); 60 | 61 | if (is_array($requestData) && array_key_exists('employeeId', $requestData)) { 62 | $employeeId = (int) $requestData['employeeId']; 63 | } 64 | 65 | if (empty($employeeId)) { 66 | badRequest('Unsupported REST request.'); 67 | } 68 | } 69 | break; 70 | default: 71 | badRequest('Unsupported REST request.'); 72 | break; 73 | } 74 | 75 | // Process request. 76 | switch ($_SERVER['REQUEST_METHOD']) { 77 | case 'GET': 78 | exit(array_key_exists($employeeId, $employees) ? json_encode($employees[$employeeId]) : json_encode(false)); 79 | break; 80 | case 'POST': 81 | if (!array_key_exists($employeeId, $employees)) { 82 | $employees[$employeeId] = array( 83 | 'name' => array_key_exists('name', $requestData) ? $requestData['name'] : null, 84 | 'age' => array_key_exists('age', $requestData) ? (int) $requestData['age'] : null, 85 | ); 86 | file_put_contents($file, json_encode($employees)); 87 | } else { 88 | badRequest('Unable to insert because the employee already exists.'); 89 | } 90 | break; 91 | case 'PUT': 92 | if (array_key_exists($employeeId, $employees)) { 93 | if (array_key_exists('name', $requestData)) { 94 | $name = $requestData['name']; 95 | } else { 96 | $name = array_key_exists('name', $employees[$employeeId]) ? $employees[$employeeId]['name'] : null; 97 | } 98 | 99 | if (array_key_exists('age', $requestData)) { 100 | $age = (int) $requestData['age']; 101 | } else { 102 | $age = array_key_exists('age', $employees[$employeeId]) ? $employees[$employeeId]['age'] : null; 103 | } 104 | 105 | $employees[$employeeId] = array( 106 | 'name' => $name, 107 | 'age' => $age, 108 | ); 109 | file_put_contents($file, json_encode($employees)); 110 | } else { 111 | badRequest('Unable to update because the employee does not exist.'); 112 | } 113 | break; 114 | case 'DELETE': 115 | if (array_key_exists($employeeId, $employees)) { 116 | unset($employees[$employeeId]); 117 | file_put_contents($file, json_encode($employees)); 118 | } else { 119 | badRequest('Unable to delete because the employee does not exist.'); 120 | } 121 | break; 122 | default: 123 | badRequest('Unsupported REST request.'); 124 | break; 125 | } 126 | 127 | exit(json_encode(true)); 128 | -------------------------------------------------------------------------------- /www/sample.employees.json: -------------------------------------------------------------------------------- 1 | { 2 | "7" : { 3 | "name" : "James Bond", 4 | "age" : 27 5 | } 6 | } 7 | --------------------------------------------------------------------------------