raml使用_使用RAML测试API

raml使用

In a recent article I looked at RESTful API Modeling Language (RAML). I provided an overview of what RAML is all about, how to write it and some of its uses.

最近的文章中,我介绍了RESTful API建模语言(RAML)。 我概述了RAML的全部含义,如何编写及其用途。

This time, I’m going to look at some of the ways in which you can use RAML for testing. We’ll start by using RAML to validate responses from an API. Then we’ll look at an approach you could take to mock an API server, using a RAML file to create mock HTTP responses.

这次,我将介绍一些使用RAML进行测试的方法。 我们将从使用RAML验证来自API的响应开始。 然后,我们将研究一种可以使用RAML文件创建模拟HTTP响应来模拟API服务器的方法。

验证API响应 (Validating API Responses)

First, let’s define a simple RAML file for a fictional API. I’ve left out some routes, but it will be enough to demonstrate the principles.

首先,让我们为虚构的API定义一个简单的RAML文件。 我省略了一些路线,但是足以证明这些原理。

#%RAML 0.8
title: Albums
version: v1
baseUri: http://localhost:8000
traits:
  - secured:
      description: Some requests require authentication
      queryParameters:
        accessToken:
          displayName: Access Token
          description: An access token is required for secure routes
          required: true
  - unsecured:
      description: This is not secured
/account:
  displayName: Account
  get:
    description: Get the currently authenticated user's account details.    
    is: [secured]
    responses:
      200:
        body:
          application/json: 
            schema: |
              { "$schema": "http://json-schema.org/schema#",
                "type": "object",
                "description": "A user",
                "properties": {
                  "id":  { 
                    "description": "Unique numeric ID for this user",
                    "type": "integer" 
                  },
                  "username":  { 
                    "description": "The user's username",
                    "type": "string" 
                  },                  
                  "email":  { 
                    "description": "The user's e-mail address",
                    "type": "string",
                    "format": "email" 
                  },
                  "twitter": {
                    "description": "User's Twitter screen name (without the leading @)",
                    "type": "string",
										"maxLength": 15
                  }
                },
                "required": [ "id", "username" ]
              }
            example: |
              {
                "id": 12345678,
                "username": "joebloggs",
                "email": "joebloggs@example.com",                                
                "twitter": "joebloggs"
              }
  put:
    description: Update the current user's account
/albums:
  displayName: Albums
  /{id}:      
    displayName: Album
    uriParameters: 
      id:
        description: Numeric ID which represents the album
    /tracks:
      displayName: Album Tracklisting
      get:
        responses:
          200:
            body:
              application/json: 
                schema: |
                  { "$schema": "http://json-schema.org/schema#",
                    "type": "array",
                    "description": "An array of tracks",
                    "items": {
                      "id":  { 
                        "description": "Unique numeric ID for this track",
                        "type": "integer" 
                      },
                      "name":  { 
                        "description": "The name of the track",
                        "type": "string" 
                      }
                    },
                    "required": [ "id", "name" ]
                  }
                example: |
                  [                    
                    {
                      "id": 12345,
                      "name": "Dark & Long"
                    },
                    {
                      "id": 12346,
                      "name": "Mmm Skyscraper I Love You"
                    }
                  ]

Notice we’ve defined a trait for secured routes, which expects an access token as a query parameter. We’ve defined a few available routes, and defined some JSON schemas to specify the format of the results. We’ve also included some example responses; these are what we’re going to use to generate mock responses.

注意,我们已经为安全路由定义了特征,该特征希望将访问令牌作为查询参数。 我们定义了一些可用的路由,并定义了一些JSON模式来指定结果的格式。 我们还提供了一些示例响应; 这些就是我们将用来生成模拟响应的东西。

Let’s create an application which we’ll use for both parts of this tutorial. You’ll find it on Github.

让我们创建一个将在本教程的两个部分中使用的应用程序。 您可以在Github上找到它。

In this first part I’ll show how you can parse a RAML file, extract the schema for a given route and then use this to test against.

在第一部分中,我将展示如何解析RAML文件,提取给定路由的模式,然后使用它进行测试。

Create a project directory, and create the file test/fixture/api.raml with the contents above.

创建一个项目目录,并使用上面的内容创建文件test/fixture/api.raml

We’re going to use Guzzle to access the API, PHPUnit as a testing framework and this PHP-based RAML parser. So, create a composer.json to define these dependencies:

我们将使用Guzzle来访问API, PHPUnit作为测试框架以及此基于PHP的RAML解析器 。 因此,创建一个composer.json来定义这些依赖项:

{
    "name": "sitepoint/raml-testing",
    "description": "A simple example of testing APIs against RAML definitions",
    "require": {
        "alecsammon/php-raml-parser": "dev-master",
        "guzzle/guzzle": "~3.9@dev",				
        "phpunit/phpunit": "~4.6@dev"
    },
    "authors": [
        {
            "name": "lukaswhite",
            "email": "hello@lukaswhite.com"
        }
    ],
		"autoload": {
        "psr-0": {
          "Sitepoint\\": "src/"
        }
      },
    "minimum-stability": "dev"
}

Run composer install to download the required packages.

运行composer install以下载所需的软件包。

Now, let’s create a simple test which validates the response from an API. We’ll begin with a phpunit.xml file:

现在,让我们创建一个简单的测试,以验证API的响应。 我们将从phpunit.xml文件开始:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    colors="true"
    convertErrorsToExceptions="false"
    convertNoticesToExceptions="false"
    convertWarningsToExceptions="false"
    stopOnFailure="false"
    bootstrap="vendor/autoload.php"
    strict="true"
    verbose="true"
    syntaxCheck="true">

    <testsuites>
        <testsuite name="PHP Raml Parser">
            <directory>./test</directory>
        </testsuite>
    </testsuites>

    <filter>
        <whitelist>
            <directory suffix=".php">./src</directory>
        </whitelist>
    </filter>

</phpunit>

The PHP RAML Parser currently shows a deprecation error. To get around this, we’re setting convertErrorsToExceptions, convertNoticesToExceptions and convertWarningsToExceptions to false.

当前,PHP RAML解析器显示弃用错误。 为了解决这个问题,我们将convertErrorsToExceptionsconvertNoticesToExceptionsconvertWarningsToExceptionsfalse

Let’s create a skeleton test class. Name this test/AccountTest.php, and start by defining the setUp() method:

让我们创建一个框架测试类。 将此test/AccountTest.php命名为test/AccountTest.php ,然后从定义setUp()方法开始:

<?php

class AccountTest extends PHPUnit_Framework_TestCase
{
    /**
     * @var \Raml\Parser
     */
    private $parser;

    public function setUp()
    {
        parent::setUp();
        $parser = new \Raml\Parser();
        $this->api = $parser->parse(__DIR__.'/fixture/api.raml');

        $routes = $this->api->getResourcesAsUri()->getRoutes();

        $response = $routes['GET /account']['response']->getResponse(200);

        $this->schema = $response->getSchemaByType('application/json');
    }

}

Here we’re parsing the RAML file, then extracting all of the defined routes. Next we’re pulling out the route identified by the string GET /account. From that, we’re extracting the definition of a successful response, and from that, we’re grabbing the JSON schema which defines the expected structure of the JSON response.

在这里,我们解析RAML文件,然后提取所有已定义的路由。 接下来,我们提取由字符串GET /account标识的路由。 从中,我们提取出成功响应的定义,并从中获取定义JSON响应的预期结构的JSON模式。

Now we can create a simple test which calls our endpoint, checks that we get back a 200 status, that the response format is JSON and that it validates against the schema.

现在,我们可以创建一个简单的测试,该测试调用端点,检查是否返回200状态,响应格式为JSON 根据模式进行验证。

/** @test */
public function shouldBeExpectedFormat()
{    
	$accessToken = 'some-secret-token';

	$client = new \Guzzle\Http\Client();
        
	$request = $client->get($this->api->getBaseUri() . '/account', [
		'query' => [
			'accessToken' => $accessToken,
		]
	]);
        
	$response = $client->send($request);
        
	// Check that we got a 200 status code
	$this->assertEquals( 200, $response->getStatusCode() );
	
	// Check that the response is JSON       
	$this->assertEquals( 'application/json', $response->getHeader('Content-Type')->__toString());
	
	// Check the JSON against the schema
	$this->assertTrue($this->schema->validate($response->getBody()));

}

It’s as simple as that; the RAML Parser provides a validator against our defined JSON schema.

就这么简单; RAML解析器针对我们定义的JSON模式提供了一个验证器。

There are a number of ways in which you could use RAML to test your APIs. As well as JSON schemas, RAML also supports XML schemas – so the principle of checking the results in XML format would be broadly similar. You could test that the appropriate status codes are returned, that the routes defined in your RAML all exist, and so on.

您可以使用多种方法使用RAML来测试API。 除了JSON模式,RAML还支持XML模式-因此,以XML格式检查结果的原理大致相似。 您可以测试是否返回了适当的状态代码,是否存在RAML中定义的路由,等等。

In the next section, we’ll look at using RAML to mock API responses.

在下一节中,我们将研究使用RAML模拟API响应。

使用RAML模拟API (Mocking an API using RAML)

Another neat thing we can do with RAML is use it to mock an API. There are probably too many variations across different APIs to create a one-size-fits-all mocking class, so let’s build one which can be tweaked to your requirements.

我们可以对RAML进行的另一项整洁的事情是使用它来模拟API。 跨不同的API可能存在太多变化,无法创建一个“一刀切”的模拟类,因此让我们构建一个可以根据您的要求进行调整的类。

What we’ll do is create three things:

我们要做的是创建三件事:

  • A “response” class, which encapsulates standard HTTP response data, such as the status code and body

    “响应”类,用于封装标准的HTTP响应数据,例如状态码和正文
  • A class which uses RAML to respond to “URLs”

    使用RAML响应“ URL”的类
  • A simple “server” which you can run on a web server

    您可以在网络服务器上运行的简单“服务器”

To keep things simple, we’ll use the same code as in the previous section. We just need to add an additional dependency; FastRoute, a simple and fast routing component which we’ll use to determine the route we’re going to respond to. Add it to the require section of your composer.json file:

为简单起见,我们将使用与上一节相同的代码。 我们只需要添加一个额外的依赖项即可。 FastRoute ,一个简单而快速的路由组件,我们将使用它来确定要响应的路由。 将其添加到composer.json文件的require部分:

"nikic/fast-route": "~0.3.0"

Now, let’s create a really simple Response class; create this in src/Sitepoint/Response.php:

现在,让我们创建一个非常简单的Response类; 在src/Sitepoint/Response.php创建它:

<?php namespace Sitepoint;

class Response {

	/**
	 * The HTTP status code
	 * @var integer
	 */
	public $status;

	/**
	 * The body of the response
	 * @var string
	 */
	public $body;

	/**
	 * An array of response headers
	 * @var array
	 */
	public $headers;

	/**
	 * Constructor
	 * 
	 * @param integer $status The HTTP status code
	 */
	public function __construct($status = 200)
	{
		$this->status = $status;
		
		$this->headers = [
			'Content-Type' => 'application/json'
		];
	}

	/**
	 * Sets the response body
	 * 
	 * @param string $body
	 */
	public function setBody($body)
	{
		$this->body = $body;
		$this->headers['Content-Length'] = strlen($body);
	}

}

Nothing too complex here. Note that we’re going to mock an API which only “speaks” JSON, so we’re forcing the Content-Type to application/json.

这里没有什么太复杂的。 请注意,我们将模拟仅“说” JSON的API,因此我们将Content-Type强制为application/json

Now let’s start a class which given an HTTP verb and path, will look through a RAML file to find a matching route, and return an appropriate response. We’ll do so by pulling out the example from the appropriate type of response. For the purposes of this component, that will always be a successful (status code 200) JSON response.

现在,让我们开始一个类,该类提供了HTTP动词和路径,将遍历RAML文件以查找匹配的路由,并返回适当的响应。 我们将通过从适当类型的响应中提取example来完成此操作。 就此组件而言,这将始终是成功的(状态码200 )JSON响应。

Create the file src/Sitepoint/RamlApiMock.php, and begin the class with the following:

创建文件src/Sitepoint/RamlApiMock.php ,并从以下内容开始该类:

<?php namespace Sitepoint;

class RamlApiMock {

	/**
	 * Constructor
	 * 
	 * @param string $ramlFilepath Path to the RAML file to use
	 */
	public function __construct($ramlFilepath)
	{
		// Create the RAML parser and parse the RAML file
		$parser = new \Raml\Parser();
		$api = $parser->parse($ramlFilepath);

		// Extract the routes
		$routes = $api->getResourcesAsUri()->getRoutes();
		$this->routes = $routes;

		// Iterate through the available routes and add them to the Router
		$this->dispatcher = \FastRoute\simpleDispatcher(function(\FastRoute\RouteCollector $r) use ($routes) {
			foreach ($routes as $route) {
				$r->addRoute($route['method'], $route['path'], $route['path']);
			}
		});

	}
	
}

Let’s look at what we’re doing here.

让我们看看我们在这里做什么。

As before, we’re parsing a RAML file. Then we’re extracting all of the available routes. Next we iterate through those, and add them to a collection which is compatible with the FastRoute component. Lucky for us, it uses the same format, so it’ll be able to translate the following:

和以前一样,我们正在解析一个RAML文件。 然后,我们提取所有可用的路线。 接下来,我们迭代这些,并将它们添加到与FastRoute组件兼容的集合中。 幸运的是,它使用相同的格式,因此可以翻译以下内容:

/albums/123/tracks

to this:

对此:

/albums/{id}/tracks

The addRoute() method takes as its third argument the name of a function designed to handle the route. Ordinarily, you’d define separate functions to handle each route accordingly. However, since we’re going to handle all routes using the same code, we’re overriding this behaviour slightly, and rather than a function name we’re adding the path a second time. This way, we can extract the path in our handler to determine what response we need to send.

addRoute()方法将旨在处理路由的函数的名称作为第三个参数。 通常,您将定义单独的函数来相应地处理每个路由。 但是,由于我们将使用相同的代码处理所有路由,因此我们将略微覆盖此行为,而不是函数名,而是第二次添加路径。 这样,我们可以在处理程序中提取路径,以确定我们需要发送什么响应。

Let’s create a dispatch() method.

让我们创建一个dispatch()方法。

/**
	 * Dispatch a route
	 * 
	 * @param  string $method  The HTTP verb (GET, POST etc)
	 * @param  string $url     The URL
	 * @param  array  $data    An array of data (Note, not currently used)
	 * @param  array  $headers An array of headers (Note, not currently used)
	 * @return Response
	 */
	public function dispatch($method, $url, $data = array(), $headers = array())
	{
		// Parse the URL
		$parsedUrl = parse_url($url);
		$path = $parsedUrl['path'];

		// Attempt to obtain a matching route
		$routeInfo = $this->dispatcher->dispatch($method, $path);

		// Analyse the route
		switch ($routeInfo[0]) {
			case \FastRoute\Dispatcher::NOT_FOUND:
				// Return a 404
				return new Response(404);				
				break;

			case \FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
				// Method not allows (405)
				$allowedMethods = $routeInfo[1];				
				// Create the response...
				$response = new Response(405);
				// ...and set the Allow header
				$response->headers['Allow'] = implode(', ', $allowedMethods);
				return $response;
				break;

			case \FastRoute\Dispatcher::FOUND:
				$handler = $routeInfo[1];
				$vars = $routeInfo[2];
				$signature = sprintf('%s %s', $method, $handler);
				$route = $this->routes[$signature];

				// Get any query parameters
				if (isset($parsedUrl['query'])) {					
					parse_str($parsedUrl['query'], $queryParams);							
				} else {
					$queryParams = [];
				}

				// Check the query parameters
				$errors = $this->checkQueryParameters($route, $queryParams);
				if (count($errors)) {
					$response = new Response(400);
					$response->setBody(json_encode(['errors' => $errors]));
					return $response;
				}				

				// If we get this far, is a successful response
				return $this->handleRoute($route, $vars);
		
				break;
		}

	}

So what’s going on here?

那么这是怎么回事?

We start by parsing the URL and extracting the path, then use FastRoute to try and find a matching route.

我们首先分析URL并提取路径,然后使用FastRoute尝试查找匹配的路由。

The RouteCollection’s dispatch() method returns an array, with its first element telling us whether it’s a valid route, whether it’s a valid route but invalid method, or simply not found.

RouteCollectiondispatch()方法返回一个数组,其第一个元素告诉我们这是一条有效的路由,这是一条有效的路由但无效的方法,或者根本找不到。

If we can’t find a matching route, we generate a 404 Not Found. If the method isn’t supported we generate a 405 Method Not Allowed, popping the allowed methods into the appropriate header.

如果找不到匹配的路由,则会生成404 Not Found 。 如果不支持该方法,我们将生成405 Method Not Allowed ,将允许的方法弹出到适当的标题中。

The third case is where it gets interesting. We generate a “signature” by concatenating the method and path, so it looks something like this:

第三种情况是它变得有趣。 我们通过连接方法和路径来生成“签名”,因此看起来像这样:

GET /account

or:

要么:

GET /albums/{id}/tracks

We can then use that to grab the route definition from the $routes property, which you’ll recall we pulled out of our RAML file.

然后,我们可以使用它从$routes属性中获取路由定义,您会记得我们从RAML文件中提取了该定义。

The next step is to create an array of query parameters, and then call a function which checks them – we’ll come to that particular function in a moment. Because different API’s can handle errors very differently, you may wish to modify this to suit your API – in this example I’m simply returning a 400 Bad Request, with the body containing a JSON representation of the specific validation errors.

下一步是创建查询参数数组,然后调用一个检查参数的函数-我们稍后将介绍该特定函数。 因为不同的API可以处理错误的方式非常不同,所以您可能希望修改此错误以适合您的API –在此示例中,我仅返回一个400 Bad Request ,其主体包含特定验证错误的JSON表示形式。

At this point, you may wish to add some additional checks or validation. You could, for example, check whether the request has the appropriate security credentials supplied. We’re going to implement this via the required accessToken query parameter, which we’ve defined as a trait in the RAML file.

此时,您可能希望添加一些其他检查或验证。 例如,您可以检查请求是否提供了适当的安全凭证。 我们将通过必需的accessToken查询参数来实现此accessToken ,该参数已在RAML文件中定义为特征。

Finally, we call the handleRoute() method, passing the route definition and any URI parameters. Before we look at that, let’s return to our query parameters validation.

最后,我们调用handleRoute()方法,传递路由定义和任何URI参数。 在开始讨论之前,让我们回到查询参数验证中。

/**
	 * Checks any query parameters
	 * @param  array 	$route  The current route definition, taken from RAML
	 * @param  array 	$params The query parameters
	 * @return boolean
	 */
	public function checkQueryParameters($route, $params)
	{
		// Get this route's available query parameters
		$queryParameters = $route['response']->getQueryParameters();

		// Create an array to hold the errors
		$errors = [];

		if (count($queryParameters)) {

			foreach($queryParameters as $name => $param) {				

				// If the display name is set then great, we'll use that - otherwise we'll use
				// the name
				$displayName = (strlen($param->getDisplayName())) ? $param->getDisplayName() : $name;

				// If the parameter is required but not supplied, add an error
				if ($param->isRequired() && !isset($params[$name])) {
					$errors[$name] = sprintf('%s is required', $displayName);
				}

				// Now check the format
				if (isset($params[$name])) {

					switch ($param->getType()) {
						case 'string':
							if (!is_string($params[$name])) {
								$errors[$name] = sprintf('%s must be a string');
							}
							break;
						case 'number':
							if (!is_numeric($params[$name])) {
								$errors[$name] = sprintf('%s must be a number');
							}
							break;
						case 'integer':
							if (!is_int($params[$name])) {
								$errors[$name] = sprintf('%s must be an integer');
							}
							break;
						case 'boolean':
							if (!is_bool($params[$name])) {
								$errors[$name] = sprintf('%s must be a boolean');
							}
							break;
						// date and file are omitted for brevity
					}

				}

			}
		}

		// Finally, return the errors
		return $errors;
	}

This is pretty simple stuff – and note that I’ve left out certain parameter types to keep things simple – but this can be used to play around with query parameters, and reject requests where those parameters don’t fit the specifications laid out in our RAML file.

这是非常简单的内容-请注意,我省略了某些参数类型以使事情保持简单-但这可用于处理查询参数,并在那些参数不符合我们的规范要求的情况下拒绝请求RAML文件。

Finally, the handleRoute() function:

最后, handleRoute()函数:

/** 
	 * Return a response for the given route
	 * 
	 * @param  array 	$route  The current route definition, taken from RAML
	 * @param  array 	$vars   An optional array of URI parameters
	 * @return Response
	 */
	public function handleRoute($route, $vars)
	{
		// Create a reponse
		$response = new Response(200);	

		// Return an example response, from the RAML
		$response->setBody($route['response']->getResponse(200)->getExampleByType('application/json'));

		// And return the result
		return $response;

	}

What we’re doing here is extracting the example from the appropriate route, then returning it as a response with a status code of 200.

我们在这里所做的是从适当的路由中提取示例,然后将其作为响应返回,其状态码为200。

At this point, you could use the RamlApiMock in your unit tests. However, with a simple addition, we can also provide this mocking component as a web service, simply by wrapping a call to it with some simple routing logic.

此时,您可以在单元测试中使用RamlApiMock 。 但是,通过简单的添加,我们也可以将此模拟组件作为Web服务提供,只需通过使用一些简单的路由逻辑包装对它的调用即可。

To do this, create an index.php file with the following contents:

为此,请创建一个具有以下内容的index.php文件:

<?php
require_once 'vendor/autoload.php';

use Sitepoint\RamlApiMock;

// The RAML library is currently showing a deprecated error, so ignore it
error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE);

// Create the router
$router = new RamlApiMock('./test/fixture/api.raml');

// Handle the route
$response = $router->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);

// Set the HTTP response code
http_response_code($response->status);

// Optionally set some response headers
if (count($response->headers)) {
	foreach ($response->headers as $name => $value) {
		header(sprintf('%s: %s', $name, $value));
	}
}

// Print out the body of the response
print $response->body;

Hopefully this is pretty self-explanatory; we’re instantiating our “router”, handling the requested URL and method combination, then sending back the response with the appropriate status code and any headers.

希望这是不言而喻的; 我们正在实例化“路由器”,处理请求的URL和方法组合,然后将适当的状态代码和任何标头发送回响应。

Now run the server with the following:

现在,使用以下命令运行服务器:

php -S localhost:8000

Because APIs vary considerably, it’s likely you’ll need to modify this example to suit your implementation. Hopefully, it gives you enough to get started.

由于API的差异很大,您可能需要修改此示例以适合您的实现。 希望它给您足够的入门知识。

摘要 (Summary)

In this article I’ve looked at RAML in the context of testing and mocking APIs.

在本文中,我已经在测试和模拟API的背景下研究了RAML。

Since RAML provides an unambiguous and comprehensive statement of how an API should function, it’s very useful both for testing against and providing mock responses.

由于RAML提供了关于API应该如何工作的明确而全面的声明,因此对于测试和提供模拟响应都非常有用。

There’s much more you can do with RAML, and these examples only really touch the surface of how RAML can be used in testing, but hopefully I’ve provided you with a few ideas.

使用RAML可以做更多的事情,这些示例仅真正涉及如何在测试中使用RAML,但是希望我为您提供了一些想法。

翻译自: https://www.sitepoint.com/testing-apis-raml/

raml使用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值