Skip to content

How to create Web API and How To cover them with Functional Testing

Ievgen Shakhsuvarov edited this page Jul 16, 2019 · 2 revisions

Table of Contents

Introduction to Web API

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.

Headless Magento

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 ) Headless Magento 2

Web API in MSI

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>

WebAPI Functional Testing

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).

Implementation Details

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.

MSI Documentation:

  1. Technical Vision. Catalog Inventory
  2. Installation Guide
  3. List of Inventory APIs and their legacy analogs
  4. MSI Roadmap
  5. Known Issues in Order Lifecycle
  6. MSI User Guide
  7. DevDocs Documentation
  8. User Stories
  9. User Scenarios:
  10. Technical Designs:
  11. Admin UI
  12. MFTF Extension Tests
  13. Weekly MSI Demos
  14. Tutorials
Clone this wiki locally