{"id":6590,"date":"2019-06-09T00:01:00","date_gmt":"2019-06-09T00:01:00","guid":{"rendered":"https:\/\/alexrusin.com\/?p=6590"},"modified":"2019-06-09T00:03:01","modified_gmt":"2019-06-09T00:03:01","slug":"testing-api-clients","status":"publish","type":"post","link":"https:\/\/blog.alexrusin.com\/testing-api-clients\/","title":{"rendered":"Testing API Clients"},"content":{"rendered":"\n
In this article I will describe an approach I use to test API clients. Since an API client is a boundary between your code and the outside world you should write as little code as possible to implement it. The client should strictly do its job by sending a request and returning a response. That is why we don’t really test the clients themselves but rather the code that processes the response from the client. Clients in the test code should be mocked in order not to hit a real API endpoint while running tests. <\/p>\n\n\n\n
The most frequent mistake in coding API clients is not covering all the scenarios. It is easy enough to write application code that processes a normal response from the remote server. But what happens if things go bad? SoapFault is thrown, important fields on the response are missing? That is why testing is important. It lets you go through all of the scenarios and make sure your application is ready to handle different cases.<\/p>\n\n\n\n
We will be testing a SOAP API client. Scenario is the following: we need to submit an invoice for tax purposes. I will be using Laravel framework because it lets you put things together a little bit faster, but the testing approach is generic and does not require any framework. Another reason I am using Laravel is to elaborate on the statement Facades Vs. Dependency Injection<\/em>. Let’s take a look at the setup.<\/p>\n\n\n\n We are creating two tables: orders <\/em>and invoce_records<\/em>. In Laravel we also create two models Order <\/em>and InvoiceRecord<\/em> to access data from those tables. <\/p>\n\n\n\n Below is our implementation of invoice connector that uses PHP’s native SoapClient<\/em> class.<\/p>\n\n\n\n Finally our connector manager class under test looks like this. There is not much there but we will keep adding code to it as we write tests. <\/p>\n\n\n\n The test checks if the code that receives responses from the API client creates a record in the invoice_records <\/em>table. If we receive a response back with UUID of the invoice got accepted by the remote system. <\/p>\n\n\n\n We also created an OrderFactory<\/em> to facilitate the test.<\/p>\n\n\n\n Obviously the test is going to fail because we do not have any code in the connector manager. Let’s try to go from red to green by writing code responsible for processing API response. <\/p>\n\n\n\n The code above won’t work because as you may have noticed I put a fake WSDL in <\/em>the InvoiceConnector.<\/em><\/p>\n\n\n\n We can use dependency injection. Instead of instantiating InvoiceConnector <\/em> class in submitInvoice <\/em>method, we can pass already instantiated InvoiceConnector <\/em>object as an argument to the method. To keep things more structured, however, let’s pass the object in the constructor of ConnectorManager <\/em>instead of submitInvoice <\/em>method.<\/p>\n\n\n\n We are still going to have the same parsing WSDL error because we are still instantiating InvoiceConnector <\/em>class. Now we are doing it outside the ConnectorManager <\/em>class, in the client code. If only we could swap InvoiceConnector <\/em>implementation. That is what interfaces are for. Let’s create InvoiceConnectorInterface. <\/em><\/p>\n\n\n\n We have to create a test double that we can use instead of InvoiceConnector <\/em>when testing. <\/p>\n\n\n\n Now we run phpunit and our test is passing. <\/p>\n\n\n\n Great. Now let’s write a test for a case when an error response is returned from the remote server. Does it mean we have to create another test double? What if we have five or six cases? Do we have to create a different test double for each that will fake the response we need? Not really. Let’s mock our test double using PHP Mockery framework<\/a> that comes with Laravel. Since Laravel uses Mockery internally for facades, Laravel already has integrated Mockery with phpunit. If you would like to know how to integrate Mockery with phpunit you can read about it here<\/a>.<\/p>\n\n\n\n The code above creates a mock from InvoceConnectorDouble <\/em>and sets expectations for submitInvoice <\/em>method. Then we are asserting that we are expecting to see an error recorded int the database. When running the test we will get the following:<\/p>\n\n\n\n This is expected because we do not have code that will process error response. Let’s write it. <\/p>\n\n\n\n Now our test is passing. <\/p>\n\n\n\n There are two more test cases I can think of that we need to cover. First is when a SoapFault <\/em>is thrown. The second one is when result returned from InvoiceConnectorManager <\/em>object wont have “response” property, so $result->response will blow up. I won’t go into details about testing these cases and just show you my test and connector manger classes below because the idea is the same.<\/p>\n\n\n\n As you may have noticed we wrote tests first before we wrote our code. In a way, tests drove our design. We used dependency injection and isolated InvoiceConnector<\/em> with an interface. We did that not because dependency injection and inversion of control are the right things to do. We did that because we wanted to make our code testable. <\/p>\n\n\n\n Now let’s take a look at “Facades Vs. Dependency Injection”. Since Laravel has concept of facades, we could have used real time facade instead of dependency injection. Our code would look something like this:<\/p>\n\n\n\n Then in hour test class we would have something like this:<\/p>\n\n\n\n I consider Laravel’s real-time facades to be useful for testing because you don’t have to think about how to better use dependency injection (should it be a constructor dependency injection, or method dependency injection). Laravel’s IoC container will resolve the class behind the scenes and hand to the method. When testing, it uses Mockery under the hood and gives you a mock instance of your class. Unfortunately, when IoC resolves a mock it first instantiates the class, and then gets the name of the class to create a mock. If class is instantiated before it gets mocked the constructor method gets triggered. In our case this will cause an issue, because we are instantiating SoapClient in the constructor and using fake WSDL. If we were to use a real WSDL we would have made an external call to check get the description. This is not desirable when running tests. If you run the tests on a computer without internet connection, the tests will blow up. That is why I prefer to use dependency injection to Laravel’s real-time facades.<\/p>\n","protected":false},"excerpt":{"rendered":" Since an API client is a boundary between your code and the outside world you should write as little code as possible to implement it. The client should strictly do its job by sending a request and returning a response. That is why we don’t really test the clients themselves but rather the code that processes the response from the client.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_kad_post_transparent":"","_kad_post_title":"","_kad_post_layout":"","_kad_post_sidebar_id":"","_kad_post_content_style":"","_kad_post_vertical_padding":"","_kad_post_feature":"","_kad_post_feature_position":"","_kad_post_header":false,"_kad_post_footer":false,"footnotes":""},"categories":[12,10],"tags":[],"class_list":["post-6590","post","type-post","status-publish","format-standard","hentry","category-laravel","category-php-mysql-development"],"yoast_head":"\n Schema::create('orders', function (Blueprint $table) {\n $table->increments('id');\n $table->unsignedInteger('source_id');\n $table->unsignedInteger('customer_id');\n $table->string('status')->default('Pending');\n $table->string('customer_email');\n $table->double('total', 8, 2);\n $table->double('tax_total', 8, 2);\n $table->timestamps();\n });\n\n Schema::create('invoice_records', function (Blueprint $table) {\n $table->increments('id');\n $table->unsignedInteger('order_id');\n $table->string('uuid', 50)->default('');\n $table->boolean('error')->default(false);\n $table->text('error_message')->nullable();\n $table->timestamps();\n\n $table->foreign('order_id')\n ->references('id')->on('orders')\n ->onDelete('cascade');\n });\n\nclass Order extends Model\n{\n protected $guarded = [];\n\n public function invoiceRecord()\n {\n return $this->hasOne(InvoiceRecord::class);\n }\n}\n\nclass InvoiceRecord extends Model\n{\n protected $guarded = [];\n}<\/pre>\n\n\n\n
\/\/ app\/Connectors\/InvoiceConnector.php\n<?php\n\nnamespace App\\Connectors;\n\nclass InvoiceConnector\n{\n private $client;\n\n public function __construct() \n {\n $arrContextOptions = array(\n \"ssl\"=>array( \n \"verify_peer\"=>false, \n \"verify_peer_name\"=>false,\n 'crypto_method' => STREAM_CRYPTO_METHOD_TLS_CLIENT)\n );\n \n $options = array(\n 'soap_version'=>SOAP_1_2,\n 'exceptions'=>true,\n 'trace'=>1,\n 'cache_wsdl'=>WSDL_CACHE_NONE,\n 'stream_context' => stream_context_create($arrContextOptions)\n );\n \n $this->client = new \\SoapClient('https:\/\/url\/dir\/file.wsdl', $options);\n }\n public function submitInvoice($order)\n {\n $result = $this->client->uploadInvoce([\n 'InvoiceID' => $order->source_id,\n 'TaxID' => $order->customer_id,\n 'Total' => $order->total, \n 'TaxTotal' => $order->tax_total\n ]);\n\n return $result;\n }\n}<\/pre>\n\n\n\n
\/\/ app\/Connectors\/ConnectorManager.php\n<?php\n\nnamespace App\\Connectors;\n\nclass ConnectorManager\n{\n public function createInvoiceRecord($order)\n {\n \n }\n}<\/pre>\n\n\n\n
Testing<\/h3>\n\n\n\n
\/\/ tests\/Unit\/ConnectorManagerTest.php\n<?php\n\nnamespace Tests\\Unit;\n\nuse Tests\\TestCase;\nuse Illuminate\\Foundation\\Testing\\RefreshDatabase;\nuse App\\Order;\nuse App\\Connectors\\ConnectorManager;\n\nclass ConnectorManagerTest extends TestCase\n{\n use RefreshDatabase;\n\n \/** @test *\/\n public function it_creates_invoice_record()\n {\n $order = factory(Order::class)->create();\n\n $connectorManager = new ConnectorManager;\n $connectorManager->createInvoiceRecord($order);\n \n $this->assertDatabaseHas('invoice_records', [\n 'order_id' => $order->id,\n 'uuid' => 'ABCD'\n ]);\n }\n}\n<\/pre>\n\n\n\n
\/\/ database\/factories\/OrderFactory.php\n<?php\n\nuse Faker\\Generator as Faker;\n\n$factory->define(App\\Order::class, function (Faker $faker) {\n return [\n 'source_id' => $faker->randomNumber(8),\n 'customer_id' => $faker->randomNumber(6),\n 'customer_email' => $faker->email,\n 'total' => $faker->randomFloat(2, 100, 1000),\n 'tax_total' => $faker->randomFloat(2, 5, 20)\n ];\n});\n<\/pre>\n\n\n\n
\/\/ app\/Connectors\/ConnectorManager.php\n....\npublic function createInvoiceRecord($order)\n {\n $connector = new InvoiceConnector;\n\n $result = $connector->submitInvoice($order);\n\n if ($result->response->status == 200) {\n $order->invoiceRecord()->create([\n 'uuid' => $result->response->uuid\n ]);\n }\n }<\/pre>\n\n\n\n
1) Tests\\Unit\\ConnectorManagerTest::it_creates_invoice_record\nSoapFault: SOAP-ERROR: Parsing WSDL: Couldn't load from 'https:\/\/url\/dir\/file.wsdl' : failed to load external entity \"https:\/\/url\/dir\/file.wsdl\"<\/pre>\n\n\n\n
\/\/ app\/Connectors\/ConnectorManager.php\n<?php\n\nnamespace App\\Connectors;\n\nclass ConnectorManager\n{\n private $connector;\n\n public function __construct(InvoiceConnector $connector)\n {\n $this->connector = $connector;\n }\n\n public function createInvoiceRecord($order)\n {\n $result = $this->connector->submitInvoice($order);\n\n if ($result->response->status == 200) {\n $order->invoiceRecord()->create([\n 'uuid' => $result->response->uuid\n ]);\n }\n }\n}<\/pre>\n\n\n\n
\/\/ tests\/Unit\/ConnectorManagerTest.php\n<?php\n\nnamespace Tests\\Unit;\n\nuse Tests\\TestCase;\nuse Illuminate\\Foundation\\Testing\\RefreshDatabase;\nuse App\\Order;\nuse App\\Connectors\\ConnectorManager;\nuse App\\Connectors\\InvoiceConnector;\n\nclass ConnectorManagerTest extends TestCase\n{\n use RefreshDatabase;\n\n \/** @test *\/\n public function it_creates_invoice_record()\n {\n $order = factory(Order::class)->create();\n\n $connector = new InvoiceConnector;\n\n $connectorManager = new ConnectorManager($connector);\n $connectorManager->createInvoiceRecord($order);\n \n $this->assertDatabaseHas('invoice_records', [\n 'order_id' => $order->id,\n 'uuid' => 'ABCD'\n ]);\n }\n}\n<\/pre>\n\n\n\n
\/\/ app\/Contracts\/InvoiceConnectorInterface.php\n<?php\n\nnamespace App\\Contracts;\n\ninterface InvoiceConnectorInterface\n{\n public function submitInvoice($order);\n}<\/pre>\n\n\n\n
\/\/ app\/Connectors\/ConnectorManager.php\n<?php\n\nnamespace App\\Connectors;\n\nuse App\\Contracts\\InvoiceConnectorInterface;\n\nclass ConnectorManager\n{\n private $connector;\n\n public function __construct(InvoiceConnectorInterface $connector)\n {\n $this->connector = $connector;\n }\n\n\/\/ rest of code...\n<\/pre>\n\n\n\n
\/\/ app\/Connectors\/InvoiceConnector.php\n<?php\n\nnamespace App\\Connectors;\n\nuse App\\Contracts\\InvoiceConnectorInterface;\n\nclass InvoiceConnector implements InvoiceConnectorInterface\n{\n\n\/\/ rest of code ...<\/pre>\n\n\n\n
\/\/ tests\/Doubles\/InvoiceConnectorDouble.php\n<?php\n\nnamespace Tests\\Doubles;\n\nuse App\\Contracts\\InvoiceConnectorInterface;\n\nclass InvoiceConnectorDouble implements InvoiceConnectorInterface\n{\n public function submitInvoice($order)\n {\n return (object) [\n 'response' => (object) [\n 'status' => 200, \n 'message' => 'OK',\n 'uuid' => 'ABCD'\n ]\n ];\n }\n}<\/pre>\n\n\n\n
\/\/ tests\/Unit\/ConnectorManagerTest.php\n<?php\n\nnamespace Tests\\Unit;\n\nuse Tests\\TestCase;\nuse Illuminate\\Foundation\\Testing\\RefreshDatabase;\nuse App\\Order;\nuse App\\Connectors\\ConnectorManager;\nuse Tests\\Doubles\\InvoiceConnectorDouble;\n\nclass ConnectorManagerTest extends TestCase\n{\n use RefreshDatabase;\n\n \/** @test *\/\n public function it_creates_invoice_record()\n {\n $order = factory(Order::class)->create();\n\n $connector = new InvoiceConnectorDouble;\n\n $connectorManager = new ConnectorManager($connector);\n $connectorManager->createInvoiceRecord($order);\n \n $this->assertDatabaseHas('invoice_records', [\n 'order_id' => $order->id,\n 'uuid' => 'ABCD'\n ]);\n }\n}<\/pre>\n\n\n\n
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.\n\n. 1 \/ 1 (100%)\n\nTime: 366 ms, Memory: 18.00MB\n\nOK (1 test, 1 assertion)<\/pre>\n\n\n\n
\/\/ tests\/Unit\/ConnectoManagerTest.php\n<?php\n\nnamespace Tests\\Unit;\n\nuse Mockery;\nuse Tests\\TestCase;\nuse Illuminate\\Foundation\\Testing\\RefreshDatabase;\nuse App\\Order;\nuse App\\Connectors\\ConnectorManager;\nuse Tests\\Doubles\\InvoiceConnectorDouble;\n\nclass ConnectorManagerTest extends TestCase\n{\n use RefreshDatabase;\n\n \/** @test *\/\n public function it_creates_invoice_record_with_error()\n {\n $order = factory(Order::class)->create();\n\n $connector = Mockery::mock(InvoiceConnectorDouble::class);\n $connector->shouldReceive('submitInvoice')\n ->with($order)\n ->andReturn((object) [\n 'response' => (object) [\n 'status' => 623,\n 'message' => 'Invoice invalid format'\n ]\n ]);\n\n \n $connectorManager = new ConnectorManager($connector);\n $connectorManager->createInvoiceRecord($order);\n \n $this->assertDatabaseHas('invoice_records', [\n 'order_id' => $order->id,\n 'uuid' => '',\n 'error' => true,\n 'error_message' => 'Invoice invalid format'\n ]);\n }\n}\n<\/pre>\n\n\n\n
There was 1 failure:\n\n1) Tests\\Unit\\ConnectorManagerTest::it_creates_invoice_record_with_error\nFailed asserting that a row in the table [invoice_records] matches the attributes {\n \"order_id\": 1,\n \"uuid\": \"\",\n \"error\": true,\n \"error_message\": \"Invoice invalid format\"\n}.\n\nThe table is empty.\n\nC:\\LaravelProjects\\laravel57\\vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Testing\\Concerns\\InteractsWithDatabase.php:23\nC:\\LaravelProjects\\laravel57\\tests\\Unit\\ConnectorManagerTest.php:55\n\nFAILURES!\nTests: 1, Assertions: 2, Failures: 1.<\/pre>\n\n\n\n
\/\/ app\/Connectors\/ConnectorManager.php\n\n\/\/ previous code ....\n\n public function createInvoiceRecord($order)\n {\n $result = $this->connector->submitInvoice($order);\n\n if ($result->response->status == 200) {\n $order->invoiceRecord()->create([\n 'uuid' => $result->response->uuid\n ]);\n } else {\n $order->invoiceRecord()->create([\n 'error' => true,\n 'error_message' => 'Invoice invalid format' \n ]);\n }\n }<\/pre>\n\n\n\n
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.\n\n. 1 \/ 1 (100%)\n\nTime: 4.67 seconds, Memory: 18.00MB\n\nOK (1 test, 2 assertions)<\/pre>\n\n\n\n
\/\/ tests\/Unit\/ConnectorManagerTest.php\n<?php\n\nnamespace Tests\\Unit;\n\nuse Mockery;\nuse Tests\\TestCase;\nuse Illuminate\\Foundation\\Testing\\RefreshDatabase;\nuse App\\Order;\nuse App\\Connectors\\ConnectorManager;\nuse Tests\\Doubles\\InvoiceConnectorDouble;\n\nclass ConnectorManagerTest extends TestCase\n{\n use RefreshDatabase;\n\n public function setUp()\n {\n parent::setUp();\n $this->order = factory(Order::class)->create();\n $this->connector = Mockery::mock(InvoiceConnectorDouble::class);\n }\n\n private function createInvoiceRecord()\n {\n $connectorManager = new ConnectorManager($this->connector);\n $connectorManager->createInvoiceRecord($this->order);\n }\n\n \/** @test *\/\n public function it_creates_invoice_record()\n {\n $this->connector->shouldReceive('submitInvoice')\n ->with($this->order)\n ->andReturn((object) [\n 'response' => (object) [\n 'status' => 200,\n 'message' => 'OK',\n 'uuid' => 'ABCD'\n ]\n ]);\n\n $this->createInvoiceRecord();\n \n $this->assertDatabaseHas('invoice_records', [\n 'order_id' => $this->order->id,\n 'uuid' => 'ABCD'\n ]);\n }\n\n \/** @test *\/\n public function it_creates_invoice_record_with_error()\n {\n $this->connector->shouldReceive('submitInvoice')\n ->with($this->order)\n ->andReturn((object) [\n 'response' => (object) [\n 'status' => 623,\n 'message' => 'Invoice invalid format'\n ]\n ]);\n\n $this->createInvoiceRecord();\n \n $this->assertDatabaseHas('invoice_records', [\n 'order_id' => $this->order->id,\n 'uuid' => '',\n 'error' => true,\n 'error_message' => 'Invoice invalid format'\n ]);\n }\n\n \/** @test *\/\n public function it_throws_soap_fault()\n {\n $this->connector->shouldReceive('submitInvoice')\n ->with($this->order)\n ->andThrow(new \\SoapFault('test', 'There was an error connecting'));\n\n $this->createInvoiceRecord();\n\n $this->assertDatabaseHas('invoice_records', [\n 'order_id' => $this->order->id,\n 'uuid' => '',\n 'error' => true,\n 'error_message' => 'There was an error connecting'\n ]);\n }\n\n \/** @test *\/\n public function it_records_error_if_response_property_does_not_exist()\n {\n $this->connector->shouldReceive('submitInvoice')\n ->with($this->order)\n ->andReturn((object) []);\n\n $this->createInvoiceRecord();\n \n $this->assertDatabaseHas('invoice_records', [\n 'order_id' => $this->order->id,\n 'uuid' => '',\n 'error' => true,\n 'error_message' => 'Response is malformed'\n ]);\n }\n\n}<\/pre>\n\n\n\n
\/\/ app\/Connectors\/ConnectorManager.php\n<?php\n\nnamespace App\\Connectors;\n\nuse App\\Contracts\\InvoiceConnectorInterface;\n\nclass ConnectorManager\n{\n private $connector;\n\n public function __construct(InvoiceConnectorInterface $connector)\n {\n $this->connector = $connector;\n }\n\n public function createInvoiceRecord($order)\n {\n try {\n $result = $this->connector->submitInvoice($order);\n } catch (\\Throwable $e) {\n $order->invoiceRecord()->create([\n 'error' => true,\n 'error_message' => $e->getMessage()\n ]);\n return;\n }\n\n if (!$result || empty($result->response)) { \n $order->invoiceRecord()->create([\n 'error' => true,\n 'error_message' => 'Response is malformed'\n ]);\n\n return;\n }\n\n $result = $result->response;\n \n if ($result->status == 200) {\n $order->invoiceRecord()->create([\n 'uuid' => $result->uuid\n ]);\n } else {\n $order->invoiceRecord()->create([\n 'error' => true,\n 'error_message' => $result->message\n ]);\n }\n\n \n }\n}<\/pre>\n\n\n\n
Facades Vs. Dependency Injection <\/h3>\n\n\n\n
\/\/ in app\/Connectors\/ConnectorManager.php\n\nuse Facades\\App\\Connectors\\InvoiceConnector;\n\/\/ some code ...\npublic function createInvoiceRecord($order)\n{\n try {\n InvoiceConnector::submitInvoice($order);\n } catch (\\Throwable $e) {\n $order->invoiceRecord()->create([\n 'error' => true,\n 'error_message' => $e->getMessage()\n ]);\n return;\n }\n\/\/ rest of the code ....\n\n}<\/pre>\n\n\n\n
\/\/ tests\/Unit\/ConnectorManagerTest.php\n\n\/\/ some code ...\nuse Facades\\App\\Connectors\\InvoiceConnector;\n\/\/ some code ...\n\n InvoiceConnector::shouldReceive('submitInvoice')\n ->with($this->order)\n ->andReturn((object) [\n 'response' => (object) [\n 'status' => 200,\n 'message' => 'OK',\n 'uuid' => 'ABCD'\n ]\n ]);\n\/\/ rest of the code ...<\/pre>\n\n\n\n