8

I have a service the consumes data from an external source. I would like to unit test the service and I am not sure how to get the test set up. To start with I would like to just test 1 method. Here is my service:

<?php

namespace Drupal\data_provider;

use Drupal\data_provider\Security\DataProviderCrypt;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\ClientInterface;
use Drupal\Core\Cache\CacheBackendInterface;



/**
 * DataProviderServic.
 *
 * @ingroup data_provider
 *
 * @group data_provider
 */
class DataProviderService {


  public $baseUri;


  protected $username;


  protected $password;

  /**
   * The HTTP client to fetch the feed data with.
   *
   * @var \GuzzleHttp\ClientInterface
   */
  protected $httpClient;

  /**
   * The cache.default cache backend.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $cacheBackend;

  /**
   * Constructs a database object.
   *
   * @param \GuzzleHttp\ClientInterface $http_client
   *   The Guzzle HTTP client.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
   *   The cache object associated with the default bin.
   */
  public function __construct(ClientInterface $http_client, CacheBackendInterface $cache_backend) {
    $this->httpClient = $http_client;
    $this->cacheBackend = $cache_backend;
    $this->baseUri = \Drupal::config('data_provider.settings')->get('base_uri');
    $this->username = \Drupal::config('data_provider.settings')
      ->get('username');
    $this->password = \Drupal::config('data_provider.settings')
      ->get('password');
  }


  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    // Forms that require a Drupal service or a custom service should access
    // the service using dependency injection.
    // @link https://www.drupal.org/node/2203931.
    // Those services are passed in the $container through the static create
    // method.
    return new static(
      $container->get('http_client'),
      $container->get('cache.default')
    );
  }


  /**
   * Create a formatted request based on options provided
   *
   * @param $url
   *   Requested URl.
   * @param array $options
   *  Query parameter options.
   * @param bool $reset
   *   Rest the cache.
   *
   * @return bool|string
   */
  public function doRequest($url, $options = array(), $reset = FALSE) {
    // request from api
    return $content;
  }

  /**
   * Return the data from the API in json format.
   *
   * @return bool|mixed
   */
  public function getSubscriptions() {
    // do_request and from the api and return info
    return $subscriptions;
  }



}

and here is my test:

<?php

namespace Drupal\data_provider\Tests\Unit;

use Drupal\Tests\UnitTestCase;
use Drupal\data_provider\DataProviderService;

/**
 * DataProviderServic unit test.
 *
 * @ingroup data_provider
 *
 * @group data_provider
 */
class DataProviderServiceTest extends UnitTestCase {

  /**
   * Very simple test of DataProviderService::getSubscriptions().
   * @todo write dataprovider tests.
   */
  public function testGetSubscriptions() {
    $dp = new DataProviderService();
    $subs = $dp->getSubscriptions();
    $this->assertEquals(TRUE, TRUE);
  }

}

when I run the tests I get these errors:

PHPUnit 4.8.36 by Sebastian Bergmann and contributors.

Testing 
E

Time: 42.35 seconds, Memory: 610.25MB

There was 1 error:

1) Drupal\data_provider\Tests\Unit\DataProviderServiceTest::testGetSubscriptions
Argument 1 passed to Drupal\data_provider\DataProviderService::__construct() must be an instance of GuzzleHttp\ClientInterface, none given, called in /var/www/drupal8/docroot/modules/custom/data_provider/tests/src/Unit/DataProviderServiceTest.php on line 22 and defined

/var/www/drupal8/docroot/vendor/symfony/phpunit-bridge/DeprecationErrorHandler.php:73
/var/www/drupal8/docroot/modules/custom/data_provider/src/DataProviderService.php:63
/var/www/drupal8/docroot/modules/custom/data_provider/tests/src/Unit/DataProviderServiceTest.php:22

I believe the issue has to do with DI and how to properly do it. So my questions:
1. How do I start with testing methods on the service?
2. Should my unit tests make actual api requests or mock them?

3
  • Well the immediate error is because you are doing a new Class() and passing no arguments to it. The constructor is expecting arguments. If this is a service registered to Drupal (w a services.yml file) you can bring in this service already constructed for you.
    – Kevin
    Commented Nov 11, 2017 at 0:01
  • Also in your class, you should pass in the config service to retrieve configuration objects instead of use the global \Drupal.
    – Kevin
    Commented Nov 11, 2017 at 0:04
  • Try this method to register the class for a service. Then you can access with \Drupal:: service('id_of_service'); in your unit test case.
    – OPTASY
    Commented Nov 14, 2017 at 18:47

1 Answer 1

9

You are on the right track!

Your tests should also never depend on calling out to external services. You should build a list of responses and scenarios you expect from the external service and ensure your tests are using your deterministic source data every time.

In your test you need to either provide instances of your dependencies or mocked versions of them through the constructor. PHPUnit ships with a few ways to mock dependencies, prophecy is a popular option (used below). It allows you to create mocks in the same fashion you expect them to be used in your code. Here is an example of creating a mock HTTP client with a response:

use GuzzleHttp\ClientInterface;
use Prophecy\Argument;
use GuzzleHttp\Psr7\Response;

$client  = $this->prophesize(ClientInterface::class);
// When the request method is called on the HTTP client, with "GET", a specific
// URL of our choosing and any third argument, make sure the following response
// is sent.
$client->request('GET', 'http://example.com/api-endpoint', Argument::any())->will(function () {
  return new Response(200, [], '[{"valid api response"}]');
});

$dp = new DataProviderService($client->reveal());

You can create a folder called "fixtures" and start creating files containing responses from your API. Start imagining scenarios where your API starts misbehaving, maybe sending some malformed JSON, create a test case and see how your code responds. A few

Note: this is just a starting point. You will need to handle the cache dependency and eliminate your calls to the "\Drupal" static methods in order to unit test your class. Click through to each method on the \Drupal class to see how you might inject it as a dependency. For example \Drupal::config is defined as:

  public static function config($name) {
    return static::getContainer()->get('config.factory')->get($name);
  }

So you might inject your settings as $container->get('config.factory')->get('data_provider.settings').

Getting this stuff setup will always be a bit of a learning curve, but the stuff you'll pick up along the way will be invaluable, keep persisting!

I presented this session at DrupalCon Vienna which might have some useful information on testing in general: https://www.youtube.com/watch?v=MquqAplUXFY

2
  • 1
    This all makes sense for Unit testing but what about when a Controller calls a service and you are running a Function Unit test. I have a question about that: drupal.stackexchange.com/questions/290889/…
    – albertski
    Commented Feb 8, 2020 at 16:17
  • Hey @albertski, I've answered your question :)
    – Sam Becker
    Commented Feb 10, 2020 at 0:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.