Utilizando el contenedor de inyección de dependencias de Symfony y PHPUnit para hacer pruebas en Drupal

June 18, 2021

Tags: IT Staff ES 2024
Share

Table of contents

Quick Access

drupal

 

Hace algún tiempo, trabajamos en un proyecto que utilizaba Drupal 7, el cual, entre otros requisitos, demandaba pruebas automáticas para demostrar la funcionalidad del sistema. Drupal viene con un módulo de pruebas integrado llamado SimpleTest. Sin embargo, al utilizarlo nos dimos cuenta de que no sería suficiente para satisfacer los requerimientos del proyecto.

 

El Problema

El código fuente de Drupal 7 está basado en procedimientos, lo que significa que no está orientado a objetos. El estilo de programación conocido como "Drupal Way" también sigue este patrón, lo cual genera dificultades para realizar pruebas unitarias. Esto se debe a que las funciones en Drupal 7 están estrechamente interconectadas, lo que hace que sea complicado probarlas de manera aislada.

 

Nuestra Solución

Para resolver el problema, decidimos adoptar tres enfoques clave:

  1. Trabajar con código orientado a objetos dentro de los hooks de Drupal.
  2. Usar un contenedor de inyección de dependencias para gestionar los servicios requeridos por nuestros objetos.
  3. Usar PHPUnit para las pruebas, debido a su estabilidad y popularidad.

 

¿Por qué PHPUnit?

Decidimos usar PHPUnit por varias razones:

  • Es una herramienta estable, robusta, popular y bien documentada.
  • Drupal 8 también utiliza PHPUnit para sus pruebas unitarias, lo que la hace compatible con versiones más recientes de Drupal.

 

Cambio hacia la Programación Orientada a Objetos

Para realizar pruebas unitarias, necesitábamos cambiar la arquitectura del código. La programación orientada a objetos es ideal para encapsular funcionalidad en objetos que se puedan probar de manera discreta. A continuación, mostramos cómo hicimos este cambio.

 

Ejemplo Original (función sin orientación a objetos):

 
function my_module_get_properties_from_remote($person_id) {  $client = new Client();    $params = array(    'query' => array(      'owner_id' => $person_id;    )  );    $response = $client->request('GET', 'http://example.com/person/properties', $params);  return $response; } 

 

Este enfoque depende directamente de Guzzle para hacer la solicitud. Para aplicar programación orientada a objetos, transformamos esta función en un objeto:

 

Versión Orientada a Objetos:

 
use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; class Owner {  private $person_id = NULL;  private $response = NULL;  public function __construct($person_id) {    $this->person_id = $person_id;  }  public function getPropertyList() {    $client = new Client();    $params = array(      'query' => array('owner_id' => $this->person_id)    );        $response = $client->request('GET', 'http://example.com/person/properties', $params);    return $response;  } } 

Y luego reescribimos la función original para usar este objeto:

 
function my_module_get_properties_from_remote($person_id) {  $owner = new Owner($person_id);  return $owner->getPropertyList(); } 

 

Pruebas Unitarias con PHPUnit

Una de las ventajas de este enfoque es que ahora podemos escribir pruebas unitarias para verificar el comportamiento del objeto Owner. Aquí mostramos un ejemplo de cómo hacerlo:

 
class OwnerTest extends PHPUnit_Framework_TestCase {  public function testCanGetListOfProperties() {    $owner = new Owner('3345');    $properties = $owner->getPropertyList();    $this->assertCount(3, $properties);    $this->assertContains('21 JumpStreet', $properties);  } } 

 

Simulando Dependencias con Mocks

En este punto, nos enfrentamos a una nueva cuestión: la función getPropertyList depende de una conexión remota a través de Guzzle. Pero, ¿qué pasa si no podemos o no queremos usar esa conexión para las pruebas unitarias automáticas? La solución es usar mocks para simular la conexión.

 

symfony

 

Usando Mocks de Guzzle:

Inyectamos una conexión mock en el objeto Owner:

 
use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; class Owner {  private $person_id = NULL;  private $response = NULL;  private $client = NULL;  public function __construct($person_id, Client $client) {    $this->person_id = $person_id;    $this->client = $client;  }  public function getPropertyList() {    $params = array('query' => array('owner_id' => $this->person_id));    $response = $this->client->request('GET', 'http://example.com/person/properties', $params);    return $response;  } } class OwnerTest extends PHPUnit_Framework_TestCase {  private $mockResponses = NULL;  private $handler = NULL;  private $client = NULL;  function setUp() {    parent::setUp();    $this->mockResponses = new MockHandler([      new Response(200, ['Content-Type' => 'application/json'], $some_json_listing),    ]);    $this->handler = HandlerStack::create($this->mockResponses);    $this->client = new Client(['handler' => $this->handler]);  }  public function testCanGetListOfProperties() {    $owner = new Owner('3345', $this->client);    $properties = $owner->getPropertyList();    $this->assertCount(3, $properties);    $this->assertContains('21 JumpStreet', $properties);  } } 

 

Inyección de Dependencias en el Proyecto

Ahora que tenemos las pruebas unitarias funcionando, necesitamos inyectar la conexión Guzzle en el objeto Owner dentro de la aplicación. Aquí entra en juego el contenedor de inyección de dependencias.Optamos por usar el módulo inject, que utiliza el contenedor de Symfony, el cual también es utilizado en Drupal 8. El siguiente código muestra cómo implementamos este contenedor para gestionar dependencias:

 
function my_module_service_container() {  static $container;  if (is_null($container)) {    $container = new Symfony\Component\DependencyInjection\ContainerBuilder();  }  if (!$container->has('remote_service')) {    $remote_settings = variable_get('remote_settings');    $guzzle_options = array(      'uri' => $remote_settings['default']['endpoint'],      'auth' => array(        $remote_settings['default']['user'],        $remote_settings['default']['password']      ),      'connect_timeout' => 30,      'timeout' => 30,    );        $container->setParameter('remote.settings', $guzzle_options);    $container->register('remote_service', '\GuzzleHttp\Client')              ->addArgument('%remote.settings%');  }  return $container; } 

 

Luego, en nuestra función, inyectamos el servicio:

 
function my_module_get_properties_from_remote($person_id) {  $container = my_module_service_container();  $owner = new Owner($person_id, $container->get('remote_service'));  return $owner->getPropertyList(); } 

 

En resumen, nuestra estrategia consistió en utilizar PHPUnit, programación orientada a objetos, y inyección de dependencias para lograr un sistema de pruebas unitarias repetibles y automatizables. Esta solución nos permitió resolver las dificultades de realizar pruebas unitarias en un código orientado a procedimientos, asegurando que nuestro sistema sea más robusto y fácil de mantener.

 

Te recomendamos este video