Stefano Alletti

Home » Code » PHP » Test double with webservices, Symfony and PHPUnit

Test double with webservices, Symfony and PHPUnit

Often, applications that i have developed was interfaced with external web services. Sometimes the services that my application would have to consume were themselves under development. Other times did not respond as they should have. What to do in these cases? Wait for the resolution of problems or the conclusion of the development of the API’s could stop the development of our application. But in a scenario of continuous development that is not acceptable.

So, the solution are mocking web services. But how to mock web services?

Case 1) Linking to internal routes instead of real webservice

Suppose we have to replace a web-service defined on config_dev.yml as well:

web_services:
  service1:
    api_info:
      endpoint: 'http://api-services/service1'
      api_user: 'service1'
      api_pass: 'service1'

1 – Replace the real config with:

web_services:
  service1:
    api_info:
      endpoint: 'http://salletti.myapplication.dev/api-services-mocked/service1'
      api_user: ''
      api_pass: ''

2 – Create new route in routing.yml:

service1-mocked:
    path:     /api-services-mocked/service1
    defaults: { _controller: 'MyApplicationMockBundle:Mock:service1' }
    methods: [POST]

3 – Create Action in MockController of MoclBundle:

<?php

namespace MyApplication\MockBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * @codeCoverageIgnore
 * No need to be cover
 */
class MockController extends Controller
{
    public function servive1Action(Request $request)
    {
        //Your logic here
       $response = new Response($this->container->get('mock_service')->getGenericResultWithoutError());
       $response->headers->set('Content-Type', 'application/json');

       return $response;
    }
}

It’s all, with this strategy you can mock every external web service in a easy way. You can as well use the mock for yours unit tests, simply changing in config_test.yml urls pointing to the external serviceswith your internal urls.

 

Case 2) Injecting mocked class instead real classes

Imagine having clients interfacing with external web services. Clients are injected inside others classes. For example:

#MyApplication/MyBundle/Resources/services.yml
services:
  service1_client:
    class: MyApplication\MyBundle\Client\Service1Client
    calls:
      - [ setLogger, ['@my_logger']]
  service1_consumer:
    class: MyApplication\MyBundle\Client\Service1Consumer
    calls:
      - [ setClient, ['@service1_client']]

And the classes:

<?php

namespace MyApplication\MyBundle\Client
class Service1Client {

   public function getFoo(array $params)
   {
      $result = $this->callService('getFoo', $params);
      return isset($result->foo) ? $result->foo : false;
   } 
}
<?php

namespace MyApplication\MyBundle\Service1Consumer
class Service1Consumer {

   public function getFoo()
   {
      $params = ...;
      return $this->client->getFoo($params);
   }

   public function setClient($client)
   {
      $this->client = $client; 
   } 
}

 

The idea is to change service to inject:

1 – Create a Mock class

<?php

namespace MyApplication\MockBundle\Client
class Service1Client {

   public function getFoo(array $params)
   {
      //return what you want. Your logic here
      return array('foo' => 'bar');
   } 
}

2 – Create a services_test.yml

services:
  service1__mock_client:
    class: MyApplication\MockBundle\Client\Service1Client
    calls:
      - [ setLogger, ['@my_logger']]
 service1_consumer:
    class: MyApplication\MyBundle\Client\Service1Consumer
    calls:
      - [ setClient, ['@service1_mock_client']]

3 – Load services_test.yml file if you are in a test environment

namespace MyApplication\MyBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
class MyApplicationMyBundleExtension extends Extension
{
 /**
 * {@inheritDoc}
 */
 public function load(array $configs, ContainerBuilder $container)
 {
     $configuration = new Configuration();
     $config = $this->processConfiguration($configuration, $configs);
     $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
     $loader->load('services.yml');
    
    //If we are on test env we load classes mocked defined on service_test.yml
    if ($container->getParameter('kernel.environment') == 'test') {
      $loader->load('services_test.yml');
    }
 }

 

Case 3) Useing Test Doubles of PHPUnit

1 – Using getMockBuilder

<?php
use PHPUnit\Framework\TestCase;

class ConsumerTest extends TestCase
{
    public function testConsumer()
    {
        // Create a stub for the SomeClass class.
        $stubClient = $this->getMockBuilder(Service1Client:class) 
       ->disableOriginalConstructor() 
       ->getMock(); 

       // Configure the stub. 
      $stubClient->method('getFoo')->willReturn(array('foo' => 'bar'));
      $consumer = new Service1Consumer();
      $consumer->setClient($stubClient)
      $this->assertArrayHasKey('foo', $consumer->getFoo()); 
   } 
} 
?>

2 – Using Prophecy

<?php
use PHPUnit\Framework\TestCase;

class ConsumerTest extends TestCase
{
    public function testConsumer()
    {
        $consumer = new Service1Consumer();

        // Create a prophecy for the Observer class.
        $client = $this->prophesize(Service1Client::class);

        // Set up the expectation for the update() method
        // to be called only once
        // as its parameter.
        $client->getFoo(Argument::type('array'))->shouldBeCalled();
        $client->willReturn(array('foo' => 'bar'));        
        // Reveal the prophecy and attach the mock object
        // to the Consumer.
        $consumer->attach($client->reveal());
        $this->assertArrayHasKey('foo', $consumer->getFoo());
    }
}
?>

Conclusion

Sometimes it is just plain hard to test the system under test (SUT) because it depends on other components that cannot be used in the test environment. This could be because they aren’t available, they will not return the results needed for the test or because executing them would have undesirable side effects. In other cases, our test strategy requires us to have more control or visibility of the internal behavior of the SUT. I have listed three strategies that i used, hoping that can be help to someone.

References:


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: