APIs e Microsserviços

21 jun, 2018

Testes de contrato para microsserviços com Pact PHP

Publicidade

O desenvolvimento de sistemas sempre modificou-se rapidamente ao decorrer do tempo. Novos paradigmas de programação foram criados, novos tipos de linguagem, metodologias e processos. A arquitetura dos softwares também não poderia ficar fora, começando com uma arquitetura monolítica, passando para uma arquitetura orientada a serviços (SOA), até a arquitetura com maior destaque no momento, os microsserviços.

O aumento da utilização das arquiteturas baseadas em serviços e microsserviços fez com que um problema entrasse em foco: a quebra de contrato entre o fornecedor do serviço e o cliente.

Um contrato é uma coleção de acordos entre um cliente (Consumer) e uma API (Provider) que descreve as interações que podem ocorrer entre eles.

Consumer Driven Contracts é um padrão que impulsiona o desenvolvimento do provider do ponto de vista do consumer. É o TDD — Test Driven Development (desenvolvimento dirigido por testes) para serviços.

Basicamente, o teste funciona da seguinte maneira: o sistema consumidor realiza uma chamada para o provedor e recebe os dados de retorno. A estrutura de dados retornada é comparada com a estrutura definida no contrato. Se os dados não forem iguais, o teste falha, indicando que a API está retornando dados diferentes do que o cliente está esperando, ou seja, houve a quebra de contrato de algum dado recebido.

A ideia geral é garantir a integridade da API antes que o sistema consumidor processe os dados da requisição enviada pelo provedor, que pode enviar alguma informação inconsistente que o consumidor ainda não consegue identificar como válida.

Testes de contrato com Pact PHP

O Pact, é um framework de teste que ajuda você a escrever contratos, e garante que esses contratos estejam atendidos. O Pact tem implementações para várias linguagens. Entre elas: Java, .NET, Javascript, Go, Python, Swift e PHP.

Para melhor entender como o Pact funciona, vamos construir um exemplo do zero com PHP. Os exemplos abaixo são baseados na API do Meetup.com. Temos um cenário com um consumer/client e um provider/api.

Primeiro vamos adicionar as dependências que precisamos para realizar os testes. Para isso, vamos utilizar o composer.

$ composer require phpunit/phpunit — dev
$ composer require mattersight/phppact — dev

Ou criando um composer.json com o seguinte conteúdo:

{
  "require-dev": {
     "phpunit/phpunit": "^6.2",
     "mattersight/phppact": "^3.0”
  }
}

Vamos criar um diretório src, onde adicionaremos nossa classe de client com o nome de ‘ExampleOneMeetupApiClient.php’:

<?php
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Uri;
class ExampleOneMeetupApiClient
{
    const version = "2";
    /**
     * @var GuzzleHttp\Client
     */
    private $httpClient;
    /**
     * @var \GuzzleHttp\Psr7\Uri
     */
    private $baseUri;
    public function __construct($baseUri)
    {
        $this->httpClient = new Client();
        $this->baseUri = $baseUri;
    }
    /**
     * @return \Psr\Http\Message\ResponseInterface
     */
    public function categories()
    {
        $uri = $this->baseUri;
        $uri = $uri->withPath(ExampleOneMeetupApiClient::version . '/categories');
        $response = $this->httpClient->get($uri, [
            'headers' => ['Content-Type' => 'application/json']
        ]);
        return $response;
    }
}

Nesta classe estamos fazendo um get para a API de consulta de categorias no sistema do meetup.

Agora criamos um diretório tests, e neste diretório, um arquivo chamado ‘phpunit.example.one.xml’ para configuração do PHPUnit com as informações necessárias para executar nossos testes:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="../vendor/autoload.php">
    <testsuites>
        <testsuite name="PhpPact Meetup API Example One Tests">
            <directory>./</directory>
        </testsuite>
    </testsuites>
    <listeners>
        <listener class="PhpPact\Consumer\Listener\PactTestListener">
            <arguments>
                <array>
                    <element>
                        <string>PhpPact Meetup API Example One Tests</string>
                    </element>
                </array>
            </arguments>
        </listener>
    </listeners>
    <php>
        <env name="PACT_MOCK_SERVER_HOST" value="localhost"/>
        <env name="PACT_MOCK_SERVER_PORT" value="7200"/>
        <env name="PACT_CONSUMER_NAME" value="ExampleOne"/>
        <env name="PACT_CONSUMER_VERSION" value="1.0.0"/>
        <env name="PACT_CONSUMER_TAG" value="master"/>
        <env name="PACT_PROVIDER_NAME" value="ExampleAPI"/>
        <env name="PACT_OUTPUT_DIR" value="/tmp"/>
    </php>
</phpunit>

E criamos nossa classe de teste no diretório tests que já criamos acima, com o nome de ‘ExampleOneMeetupAPIClientTest.php’ com o seguinte conteúdo:

<?php
require_once ('../src/ExampleOneMeetupApiClient.php');
use PhpPact\Consumer\InteractionBuilder;
use PhpPact\Consumer\Model\ConsumerRequest;
use PhpPact\Consumer\Model\ProviderResponse;
use PhpPact\Consumer\Matcher\Matcher;
use PhpPact\Standalone\MockService\MockServerEnvConfig;
use PHPUnit\Framework\TestCase;
class ExampleOneMeetupAPIClientTest extends TestCase
{
	const version = '2';
	/**
 	* @test
 	*/
	public function testCategories()
	{
    	// build the request
    	$path = '/' . self::version . '/categories';
    	// build the request
    	$request = new ConsumerRequest();
    	$request
        	->setMethod('GET')
        	->setPath($path)
        	->addHeader('Content-Type', 'application/json');
    	// build the response
    	$matcher = new Matcher();
    	$category1 = new \stdClass();
    	$category1->name = $matcher->regex('Games','[gbBG]');
    	$category1->sort_name = 'Games';
    	$category1->id = 11;
    	$category1->shortname = 'Games';
    	$body = new \stdClass();
    	$body->results= $matcher->eachLike($category1);
    	$response = new ProviderResponse();
    	$response
        	->setStatus(200)
        	->addHeader('Content-Type', 'application/json')
        	->setBody($body);
    	// build up the expected results and appropriate responses
    	$config  	= new MockServerEnvConfig();
    	$mockService = new InteractionBuilder($config);
    	$mockService->given("General Meetup Categories")
        	->uponReceiving("A GET request to return JSON using Meetups category api under version 2")
        	->with($request)
        	->willRespondWith($response);
    	$service = new \ExampleOneMeetupApiClient($config->getBaseUri()); // Pass in the URL to the Mock Server.
    	$serviceResponse = $service->categories();
    	// do some asserts on the return
    	$this->assertEquals('200', $serviceResponse->getStatusCode(), "Let's make sure we have an OK response");
    	// do something with the body returned
    	$body = (string) $serviceResponse->getBody();
    	$this->assertTrue((json_decode($body) ? true : false), "Expect the JSON to be decoded without error");
    	$hasException = false;
    	try {
        	$mockService->verify();
    	} catch(\Exception $e) {
        	$hasException = true;
    	}
    	$this->assertFalse($hasException, "We expect the pacts to validate");
	}
}

Nessa classe estamos testando a api de consulta de categorias do meetup. Para isso, estamos criando um mock da resposta da api. Instanciamos nossa classe de client passando a url do Mock Server, depois testamos o código de resposta, o formato da resposta e verificamos o conteúdo da resposta, para assim garantir que não temos quebra de contrato.

E por fim, executamos nosso teste. Entre no diretório tests e execute:

$ php ../vendor/phpunit/phpunit/phpunit -c phpunit.example.one.xml

Temos o seguinte retorno, mostrando os testes que passaram ou não:

PHPUnit 6.5.8 by Sebastian Bergmann and contributors.
Starting the mock service with command '/home/fsilva/Documents/dev/php-pact/vendor/mattersight/phppact/src/PhpPact/Standalone/Installer/../../../../pact/bin/pact-mock-service' 'service' '--consumer=ExampleOne' '--provider=ExampleAPI' '--pact-dir=/tmp' '--pact-file-write-mode=overwrite' '--host=localhost' '--port=7200'.
[2018-04-18 22:33:17] INFO  WEBrick 1.3.1
[2018-04-18 22:33:17] INFO  ruby 2.2.2 (2015-04-13) [x86_64-linux]
[2018-04-18 22:33:17] INFO  WEBrick::HTTPServer#start: pid=11881 port=7200
.                                                                1 / 1 (100%)[2018-04-18 22:33:17] INFO  going to shutdown ...
[2018-04-18 22:33:18] INFO  WEBrick::HTTPServer#start done.
I, [2018-04-18T22:33:17.859136 #11881]  INFO -- : Registered expected interaction GET /2/categories
D, [2018-04-18T22:33:17.860161 #11881] DEBUG -- : {
  "description": "General Meetup Categories",
  "providerState": "A GET request to return JSON using Meetups category api under version 2",
  "request": {
 "method": "GET",
 "path": "/2/categories",
 "headers": {
   "Content-Type": "application/json"
 }
  },
  "response": {
 "status": 200,
 "headers": {
   "Content-Type": "application/json"
 },
 "body": {
   "results": {
     "json_class": "Pact::ArrayLike",
     "contents": {
       "name": {
         "json_class": "Pact::Term",
         "data": {
           "generate": "Games",
           "matcher": {"json_class":"Regexp","o":0,"s":"[gbBG]"}
         }
       },
       "sort_name": "Games",
       "id": 11,
       "shortname": "Games"
     },
     "min": 1
   }
 }
  }
}
I, [2018-04-18T22:33:17.870039 #11881]  INFO -- : Received request GET /2/categories
D, [2018-04-18T22:33:17.870314 #11881] DEBUG -- : {
  "path": "/2/categories",
  "query": "",
  "method": "get",
  "headers": {
 "Content-Type": "application/json",
 "Host": "localhost:7200",
 "User-Agent": "GuzzleHttp/6.3.2 curl/7.55.1 PHP/7.1.16",
 "Version": "HTTP/1.1"
  }
}
I, [2018-04-18T22:33:17.870853 #11881]  INFO -- : Found matching response for GET /2/categories
D, [2018-04-18T22:33:17.871244 #11881] DEBUG -- : {
  "status": 200,
  "headers": {
 "Content-Type": "application/json"
  },
  "body": {
 "results": {
   "json_class": "Pact::ArrayLike",
   "contents": {
     "name": {
       "json_class": "Pact::Term",
       "data": {
         "generate": "Games",
         "matcher": {
           "json_class": "Regexp",
           "o": 0,
           "s": "[gbBG]"
         }
       }
     },
     "sort_name": "Games",
     "id": 11,
     "shortname": "Games"
   },
   "min": 1
 }
  }
}
I, [2018-04-18T22:33:17.884214 #11881]  INFO -- : Verifying - interactions matched
I, [2018-04-18T22:33:17.936178 #11881]  INFO -- : Verifying - interactions matched
I, [2018-04-18T22:33:17.941931 #11881]  INFO -- : Writing pact for ExampleAPI to /tmp/exampleone-exampleapi.json
Process exited with code 0.
PACT_BROKER_URI environment variable was not set. Skipping PACT file upload.
Time: 2.76 seconds, Memory: 6.00MB
OK (1 test, 3 assertions)

Como pode ser visto na resposta dos testes, ao final é gerado um arquivo json com o contrato, como este:

{
  "consumer": {
 "name": "ExampleOne"
  },
  "provider": {
 "name": "ExampleAPI"
  },
  "interactions": [
 {
   "description": "General Meetup Categories",
   "providerState": "A GET request to return JSON using Meetups category api under version 2",
   "request": {
     "method": "GET",
     "path": "/2/categories",
     "headers": {
       "Content-Type": "application/json"
     }
   },
   "response": {
     "status": 200,
     "headers": {
       "Content-Type": "application/json"
     },
     "body": {
       "results": [
         {
           "name": "Games",
           "sort_name": "Games",
           "id": 11,
           "shortname": "Games"
         }
       ]
     },
     "matchingRules": {
       "$.body.results": {
         "min": 1
       },
       "$.body.results[*].*": {
         "match": "type"
       },
       "$.body.results[*].name": {
         "match": "regex",
         "regex": "[gbBG]"
       }
     }
   }
 }
  ],
  "metadata": {
 "pactSpecification": {
   "version": "2.0.0"
 }
  }
}

Neste artigo, vimos um exemplo de como podem ser implementados testes de contrato, mais alguns exemplos podem ser encontrados nos seguintes repositórios:

Conclusão – Testes de contratos para microsserviços em PHP

A evolução dos sistemas para microsserviços fez com que novos problemas entrassem em foco, fazendo com que métodos alternativos de testes ganhassem mais destaque, além dos testes tradicionais. Apesar de muitas empresas terem experiências ruins na migração para este tipo de arquitetura, existem vantagens bem relevantes em relação a aderir ao uso deste tipo de tecnologia, como builds e entregas mais rápidas e modularidade das API’s.

Se quiser saber mais sobre microsserviços, no artigo Microsserviços: distribuindo serviços críticos ao negócio, falo sobre um case que a equipe na que eu trabalho na KingHost desenvolveu, abordando vantagens, desvantagens, dificuldades e como migramos nosso sistema monolítico.

E você, qual a sua opinião sobre teste de contrato? Já utilizou alguma vez? Deixe um comentário abaixo.

Obrigado pela leitura!