-
Notifications
You must be signed in to change notification settings - Fork 248
How to create Web API and How To cover them with Functional Testing
In Magento 2, the Web API testing allows us to test Magento 2 API from the client application perspective. Magento Web API framework provides to developers ability to utilize web services that communicate with the Magento. Support for both REST (Representational State Transfer) and SOAP (Simple Object Access Protocol) included on the Framework level. In Magento 2, the web API coverage is the same for both REST and SOAP.
More about Web API configuration you can find on the Magento DevDocs.
Magento has been constantly improving its Web APIs. Another big step forward on this way was integration with Swagger. After this integration developer can request a JSON Schema listing all the REST URLs supported by Magento instance (exactly the same as WSDL files for SOAP APIs).
This is an example, of what you can get for out-of-the-box Magento installation: http://devdocs.magento.com/swagger/. *documentation on the Magento devdocs website is generated with SwaggerUI using a schema derived from the latest build of Magento 2 Community Edition. So using the built-in ability to fetch the schema means you can always be sure you are getting the exact URLs supported by the site, taking into account which modules are active. More about Swagger integration with Magento 2 read on the DevDocs.
Improving Web API is very important because of concept which is called Headless Magento gets more and more popularity and business model where Magento 2 integrated with other external systems (like Drupal, Wordpress etc) or custom UI (for example, Single Page Application UI written on Angular or similar JS frameworks) which interacts with Magento Back-end business logic.
Slide from the presentation given by Riccardo Tempesta (@RicTempesta on Twitter )
The list of all possible Web APIs could be Found in InventoryAPI module /app/code/Magento/InventoryApi/etc/webapi.xml. It's important to underline, that because WebAPIs are entry-points for APIs belonging to Service Layer of the module, we put WebAPI configuration to the module which stores Inventory API (InventoryAPI), but not to the module which holds the implementation of these API (Inventory).
Our typical Repository methods are well-mapped for RESTful APIs
Repository::get -> HTTP GET
Repository::save -> HTTP POST (in case when we create new entity) or HTTP PUT (in case we update entity by ID)
Repository::delete -> HTTP DELETE
Repository::getList -> HTTP GET
Here you can see the example of such Mapping for Stock entity:
<!-- Stock -->
<route url="/V1/inventory/stock" method="GET">
<service class="Magento\InventoryApi\Api\StockRepositoryInterface" method="getList"/>
<resources>
<resource ref="Magento_InventoryApi::stock"/>
</resources>
</route>
<route url="/V1/inventory/stock/:stockId" method="GET">
<service class="Magento\InventoryApi\Api\StockRepositoryInterface" method="get"/>
<resources>
<resource ref="Magento_InventoryApi::stock"/>
</resources>
</route>
<route url="/V1/inventory/stock" method="POST">
<service class="Magento\InventoryApi\Api\StockRepositoryInterface" method="save"/>
<resources>
<resource ref="Magento_InventoryApi::stock_edit"/>
</resources>
</route>
<route url="/V1/inventory/stock/:stockId" method="PUT">
<service class="Magento\InventoryApi\Api\StockRepositoryInterface" method="save"/>
<resources>
<resource ref="Magento_InventoryApi::stock_edit"/>
</resources>
</route>
<route url="/V1/inventory/stock/:stockId" method="DELETE">
<service class="Magento\InventoryApi\Api\StockRepositoryInterface" method="deleteById"/>
<resources>
<resource ref="Magento_InventoryApi::stock_delete"/>
</resources>
</route>
Each typical route declaration consists of:
- Specifying URL by which current API would be accessible
- HTTP Method (GET, POST, PUT, DELETE) which should be used
- Service class and method which is binded to URL specified above
- Access control list, which provides an ability to specify who can access this API (for example, “anonymous” means anyone can access the service).
After that (*also after you created an integration as described here) you can make an external REST/SOAP calls to your Magento system.
For example, after executing the command:
$curl http://127.0.0.1/index.php/rest/V1/inventory/stock/42
The Magento framework will then parse the URL, extract the arguments from the URL, and provide as arguments to the Magento\InventoryApi\Api\StockRepositoryInterface::get($stockId)
method call, in our case $stockId binded to 42 value provided as a part of the URL.
<!-- Example: curl http://127.0.0.1/index.php/rest/V1/inventory/stock/42 -->
<route url="/V1/inventory/stock/:stockId" method="GET">
<service class="Magento\InventoryApi\Api\StockRepositoryInterface" method="get"/>
<resources>
<resource ref="Magento_InventoryApi::stock"/>
</resources>
</route>
The Web API testing framework allows you to test Magento Web API from the client application point of view. The tests can be used with either REST or SOAP. See How to Run the Tests for more information.
First of all, we've provided a possibility to write Functional API tests in the scope of module to which they belong.
Before that all the tests had to be located in the dev/test/api-functional/testsuite
<testsuites>
<testsuite name="Magento Web API Functional Tests">
<directory suffix="Test.php">testsuite</directory>
<directory suffix="Test.php">../../../app/code/*/*/Test/Api</directory>
</testsuite>
</testsuites>
That will improve modularity of code, make our code more cohesive and will simplify work with code (as all code belonging to one module including tests would be stored in that module). *these changes would be delivered into mainline soon.
Because API-Functional testing are tests which cover API, but not concrete implementation it makes sense to put WebAPI tests in the module which declares API contracts (InventoryAPI), but not the module which provides an implementation (Inventory).
The Web API functional testing framework depends on the integration testing framework and reuses most of the classes implemented there.
In the Web API functional tests only, the custom annotation @magentoApiDataFixture
is available for declaring fixtures. The difference of this annotation from @magentoDataFixture
is that the fixture will be committed and accessible during HTTP requests made within the test body. The usage rules of @magentoApiDataFixture
are the same as @magentoDataFixture
usage rules.
Here you can see that for testing Stock Update operation we use the fixture:
@magentoApiDataFixture ../../../../app/code/Magento/InventoryApi/Test/_files/stock/stock.php
namespace Magento\InventoryApi\Test\Api\StockRepository;
use Magento\Framework\Webapi\Rest\Request;
use Magento\InventoryApi\Api\Data\StockInterface;
use Magento\TestFramework\Assert\AssertArrayContains;
use Magento\TestFramework\TestCase\WebapiAbstract;
class UpdateTest extends WebapiAbstract
{
/**#@+
* Service constants
*/
const RESOURCE_PATH = '/V1/inventory/stock';
const SERVICE_NAME = 'inventoryApiStockRepositoryV1';
/**#@-*/
/**
* @magentoApiDataFixture ../../../../app/code/Magento/InventoryApi/Test/_files/stock/stock.php
*/
public function testUpdate()
{
$stock = $this->getStockDataByName('stock-name-1');
$stockId = $stock[StockInterface::STOCK_ID];
$expectedData = [
StockInterface::NAME => 'stock-name-1-updated',
];
$serviceInfo = [
'rest' => [
'resourcePath' => self::RESOURCE_PATH . '/' . $stockId,
'httpMethod' => Request::HTTP_METHOD_PUT,
],
'soap' => [
'service' => self::SERVICE_NAME,
'operation' => self::SERVICE_NAME . 'Save',
],
];
if (TESTS_WEB_API_ADAPTER == self::ADAPTER_REST) {
$this->_webApiCall($serviceInfo, ['stock' => $expectedData]);
} else {
$requestData = $expectedData;
$requestData['stockId'] = $stockId;
$this->_webApiCall($serviceInfo, ['stock' => $requestData]);
}
AssertArrayContains::assert($expectedData, $this->getStockDataById($stockId));
}
Our fixture in this case is pretty simple. It creates a Stock which needs to be updated in the scope of Test Case (update test case). app/code/Magento/InventoryApi/Test/_files/stock/stock.php
use Magento\Framework\Api\DataObjectHelper;
use Magento\InventoryApi\Api\Data\StockInterface;
use Magento\InventoryApi\Api\Data\StockInterfaceFactory;
use Magento\InventoryApi\Api\StockRepositoryInterface;
use Magento\TestFramework\Helper\Bootstrap;
/** @var StockInterfaceFactory $stockFactory */
$stockFactory = Bootstrap::getObjectManager()->get(StockInterfaceFactory::class);
/** @var DataObjectHelper $dataObjectHelper */
$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class);
/** @var StockRepositoryInterface $stockRepository */
$stockRepository = Bootstrap::getObjectManager()->get(StockRepositoryInterface::class);
/** @var StockInterface $stock */
$stock = $stockFactory->create();
$dataObjectHelper->populateWithArray(
$stock,
[
StockInterface::NAME => 'stock-name-1',
],
StockInterface::class
);
$stockRepository->save($stock);
It's recommended to reuse fixtures defined in the scope of Integrational tests, no need to have independent fixtures for WebAPI and Integration tests because they will look the same. Having reusable fixtures will help us follow the DRY (Don't Repeat Yourself) principle.
To keep your test environment clean, clear all entities created in fixture files or within tests itself from the DB after test execution. This can be done either directly in tearDown or by a corresponding rollback for the fixture file. This file should be named the same as a fixture, but with _rollback
suffix.
For example, in our case we have app/code/Magento/InventoryApi/Test/_files/stock/stock_rollback.php
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\InventoryApi\Api\Data\StockInterface;
use Magento\InventoryApi\Api\StockRepositoryInterface;
use Magento\TestFramework\Helper\Bootstrap;
/** @var StockRepositoryInterface $stockRepository */
$stockRepository = Bootstrap::getObjectManager()->get(StockRepositoryInterface::class);
/** @var SearchCriteriaBuilder $searchCriteriaBuilder */
$searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class);
$searchCriteria = $searchCriteriaBuilder
->addFilter(StockInterface::NAME, ['stock-name-1', 'stock-name-1-updated'], 'in')
->create();
$searchResult = $stockRepository->getList($searchCriteria);
if ($searchResult->getTotalCount()) {
$items = $searchResult->getItems();
$stock = reset($items);
$stockRepository->deleteById($stock->getStockId());
}
Also to follow best practices we recommend to prepare fixtures using WebAPI calls as well. So, for testing how Save method works, use Web API Get call to retrieve result for the entity which was saved just before.
because potentially such test is fragile:
$productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);
$productRepository->save($productRepository->get(self::SIMPLE_PRODUCT_SKU)->setData('cost', $cost));
$serviceInfo = [
'rest' => [
'resourcePath' => '/V1/products/cost-information',
'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST
],
'soap' => [
'service' => self::SERVICE_NAME,
'serviceVersion' => self::SERVICE_VERSION,
'operation' => self::SERVICE_NAME . 'Get',
],
];
$response = $this->_webApiCall($serviceInfo, ['skus' => [self::SIMPLE_PRODUCT_SKU]]);
/** @var \Magento\Catalog\Api\Data\ProductInterface $product */
$product = $productRepository->get(self::SIMPLE_PRODUCT_SKU);
$this->assertNotEmpty($response);
$this->assertEquals($product->getCost(), $cost);
Because it implies that Web API calls are made to the same host where Tests are hosted. And of it's not check with Repository Interface will fail.
Multi-Source Inventory developed by Magento 2 Community
- Technical Vision. Catalog Inventory
- Installation Guide
- List of Inventory APIs and their legacy analogs
- MSI Roadmap
- Known Issues in Order Lifecycle
- MSI User Guide
- 2.3 LIVE User Guide
- MSI Release Notes and Installation
- Overview
- Get Started with MSI
- MSI features and processes
- Global and Product Settings
- Configure Source Selection Algorithm
- Create Sources
- Create Stock
- Assign Inventory and Product Notifications
- Configure MSI backorders
- MSI Import and Export Product Data
- Mass Action Tool
- Shipment and Order Management
- CLI reference
- Reports and MSI
- MSI FAQs
- DevDocs Documentation
- Manage Inventory Management Modules (install/upgrade info)
- Inventory Management
- Reservations
- Inventory CLI reference
- Inventory API reference
- Inventory In-Store Pickup API reference
- Order Processing with Inventory Management
- Managing sources
- Managing stocks
- Link and unlink stocks and sources
- Manage source items
- Perform bulk actions
- Manage Low-Quantity Notifications
- Check salable quantities
- Manage source selection algorithms
- User Stories
- Support of Store Pickup for MSI
- Product list assignment per Source
- Source assignment per Product
- Stocks to Sales Channel Mapping
- Adapt Product Import/Export to support multi Sourcing
- Introduce SourceCode attribute for Source and SourceItem entities
- Assign Source Selector for Processing of Returns Credit Memo
- User Scenarios:
- Technical Designs:
- Module Structure in MSI
- When should an interface go into the Model directory and when should it go in the Api directory?
- Source and Stock Item configuration Design and DB structure
- Stock and Source Configuration design
- Open Technical Questions
- Inconsistent saving of Stock Data
- Source API
- Source WebAPI
- Sources to Sales Channels mapping
- Service Contracts MSI
- Salable Quantity Calculation and Mechanism of Reservations
- StockItem indexation
- Web API and How To cover them with Functional Testing
- Source Selection Algorithms
- Validation of Domain Entities
- PHP 7 Syntax usage for Magento contribution
- The first step towards pre generated IDs. And how this will improve your Integration tests
- The Concept of Default Source and Domain Driven Design
- Extension Point of Product Import/Export
- Source Selection Algorithm
- SourceItem Entity Extension
- Design Document for changing SerializerInterface
- Stock Management for Order Cancelation
- Admin UI
- MFTF Extension Tests
- Weekly MSI Demos
- Tutorials