Pasar al contenido principal

Mockeando una API de terceros en entornos de desarrollo y test

Buena parte de nuestro trabajo consiste en integrar las herramientas que construimos con servicios de terceros de la forma más eficiente y confiable posible, lo cual implica mantener un código robusto y cubierto por tests. En uno de nuestros proyectos, desarrollamos una funcionalidad que se encargaba de añadir o eliminar usuarios a listas de correos gestionadas por un servicio externo, con el cual nos comunicamos mediante una API.

En esta ocasión integramos la API utilizando la librería proporcionada por el mismo proveedor, desarrollando además un servicio custom a modo de interface para interactuar con la librería.

La lógica que teníamos que aplicar para decidir cuándo añadir o eliminar usuarios de las múltiples listas era algo compleja, ya que dependía de los diferentes estatus de un usuario, su configuración de cuenta y los roles. 

Para el desarrollo y la cobertura de tests automatizados decidimos duplicar las listas en el servicio externo para tener versiones distintas de las de producción. El problema surgió cuando, en un determinado momento, los tests de la gestión de las listas empezaron a fallar aleatoriamente. Entonces nos percatamos de que la inconsistencia la generaba el lanzamiento de tests en paralelo desde diferentes Merge Requests, ya que estaban actuando sobre las mismas listas.

La solución consistió en mockear la API. En nuestro caso, lo que debíamos mockear no era la API como tal, sino el servicio custom. Con esta aproximación desacoplamos el desarrollo de la integración de la API propiamente dicha del desarrollo de funcionalidades específicas que dependen del consumo de dicha API.

 

Beneficios

  • En caso de bugs y con una cobertura de tests adecuada, nos permite identificar rápidamente si el problema proviene de la integración de la API o de otra parte del código.
  • Poder desarrollar en paralelo la integración y otras funcionalidades dependientes de esta.
  • Evitar problemas de inconsistencia en el lanzamiento de tests automatizados.

Cómo 

Necesitábamos sobreescribir los métodos de nuestro servicio para que realizara las acciones que precisamos en los entornos de test, así que lo que hicimos fue crear un módulo custom y “decorar” el servicio original. Este patrón consiste en añadir o sobrescribir lógica de una clase previamente existente, y que esto sea completamente transparente para el resto del sistema:

     1. Configuramos el nuevo servicio en el archivo module.services.yml de nuestro módulo:

services: 
  acumbamail_tests.acumbamamil_decorator: 
     class: Drupal\acumbamail_tests\AcumbamailDecorator 
     public: false 
     decorates: acumbamail.client 
     arguments: ["@config.factory", "@logger.factory", "@tempstore.private"]

Los parámetros, explicados brevemente, son los siguientes:

  • class: especificamos la clase.  
  • public: lo seteamos a “false” para que solo se pueda llamar al servicio extendido mediante el servicio decorado.
  • decorates: el servicio que queremos decorar.
  • arguments: los argumentos que necesita el servicio decorado, y, si fuera necesario, dependencias que necesitemos en nuestro servicio decorador.

Con esta configuración, cuando se llama al servicio original, se carga el servicio decorador, sin que las clases que lo invoquen tengan que ser modificadas.

    2. A continuación, generamos la clase decoradora que extiende la clase decorada, es decir, el servicio original:

<?php

namespace Drupal\acumbamail_tests;

use Drupal\acumbamail\Client\AcumbamailService;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactory;
use Drupal\Core\TempStore\PrivateTempStore;

/**
 * Decorator for the Acumbamail API.
 */

class AcumbamailDecorator extends AcumbamailService {

  /**
   * Cache id.
   *
   * @var string
   */

  protected $storeId = 'acumbamail_tests';


  /**
   * Temp Store.
   *
   * @var \Drupal\Core\TempStore\PrivateTempStore
   */
  protected $tempStorePrivate;

  /**
   * AcumbamailService constructor.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   Config factory.
   * @param \Drupal\Core\Logger\LoggerChannelFactory $logger
   *   Logger.
   * @param \Drupal\Core\TempStore\PrivateTempStore $temp_store_private
   *   Store for subscription data.
   */
  public function __construct(ConfigFactoryInterface $config_factory, LoggerChannelFactory $logger, PrivateTempStore $temp_store_private) {
    parent::__construct($config_factory, $logger);
    $this->tempStorePrivate = $temp_store_private;
  }

  /**
   * Initialize Acumbamail Client.
   */
  protected function initializeClient() {
    if (empty($this->getAuthtoken())) {
      throw new \Exception('Authtoken is not set, please review Acumbamail settings form.');
    }
  }

  /**
   * {@inheritdoc}
   */
  public function subscribeUser(string $list_id, string $user_name, string $user_mail, string $user_verified) {
    $this->initializeClient();
    $data = $this->getData();
    if (!isset($data[$user_mail])) {
      $data[$user_mail] = [];
    }
    $data[$user_mail][$list_id] = TRUE;
    $this->saveData($data);
  }

  /**
   * {@inheritdoc}
   */
  public function unsubscribeUser(string $list_id, string $user_mail) {
    $this->initializeClient();
    $data = $this->getData();
    unset($data[$user_mail][$list_id]);
    $this->saveData($data);
  }

  /**
   * {@inheritdoc}
   */
  public function userIsSubscribed(string $list_id, string $user_mail) {
    $this->initializeClient();
    $data = $this->getData();
    return (isset($data[$user_mail][$list_id]));
  }

  /**
   * Save the data.
   *
   * @param array $data
   *   Data.
   */
  protected function saveData(array $data) {
    $temp_store = $this->tempStorePrivate->get($this->storeId);
    $temp_store->set($this->storeId, $data);
  }

  /**
   * Return data.
   *
   * @return array
   *   Data.
   */
  protected function getData() {
    $temp_store = $this->tempStorePrivate->get($this->storeId);
    $data = $temp_store->get($this->storeId);
    if (empty($data)) {
      $data = [];
    }
    return $data;
  }

}



Cabe destacar que en nuestro caso debíamos persistir la asociación de usuarios y listas a través de diferentes peticiones, para ello utilizamos el servicio “tempstore.private”.

Con esto, tendríamos terminado el mock y podríamos generar nuestros test apoyándonos en él.
Tip 1: Si la integración con la API se realiza directamente, sin utilizar una librería, y se utiliza el servicio http_client para la gestión de peticiones, puedes aplicar la estrategia explicada por Daniel Sipos.

Tip 2: Si quieres que este módulo sea visible solo en ciertos entornos, puedes ubicarlo bajo la carpeta “tests” del módulo principal. Así, solo será visible para Drupal cuando el parámetro ‘extension_discovery_scan_tests’ del settings esté a “true”.

$settings['extension_discovery_scan_tests'] = TRUE;

 

Image
Luis Ruiz

Luis Ruiz

Senior Drupal developer