← Back to list

Usando TDD para desarrollar un modulo para Issabel (Parte II)

Continuando con lo que empecé hace un tiempo atrás, el siguiente paso es construir la estructura base del módulo.

Juan Almeida · 2017-10-07 23:19 · 35 claps · 7.2 min read
#issabel #asterisk #elastix #voip #php
Open on Medium ↗

Usando TDD para desarrollar un modulo para Issabel (Parte II)

Continuando con lo que empecé hace un tiempo atrás, el siguiente paso es construir la estructura base del módulo.

Estructura básica

Estructura básica

Estructura básica

La carpeta “/app” es la más importante de todas, y contiene el código principal del módulo. Tiene a su ves otras subcarpetas, que posteriormente explicaré cual es su propósito.

La carpeta “/bootstrap” es la que contiene el código que se carga para procesar cada una de las llamadas a nuestro módulo. Incluye las librerías base del framework.

Archivo: “/bootstrap/bootstrap.php”

<?php
require_once ‘/var/www/html/libs/paloSantoConfig.class.php’;
require_once ‘/var/www/html/libs/misc.lib.php’;
require_once ‘/var/www/html/libs/paloSantoDB.class.php’;
require_once ‘/var/lib/asterisk/agi-bin/phpagi.php’;
require_once ‘/var/lib/asterisk/agi-bin/phpagi-asmanager.php’;
require_once __DIR__ . ‘/Autoloader.php’;
spl_autoload_register(‘Autoloader::loader’);

Este archivo también registra un autoloader. Como su nombre indica, el autoloader está destinado a cargar de forma automática las clases utilizadas. Cada vez que se intenta inicializar una clase y la clase no existe, el nombre de esta clase se pasa al autoloader y este es ejecutado. En el autoloader se automatiza el proceso de carga sin tener que incluir manualmente cada archivo y además permite hacer el código más rápido, pues sólo se cargarán las clases que efectivamente se utilicen.

Archivo: “/bootstrap/Autoloader.php”

<?php
class Autoloader
{
  static public function loader($className)
  {
    $filename = __DIR__ . '/../';
    $filename .= lcfirst(str_replace("\\", '/', $className) . '.php');

    if (file_exists($filename)) {
      include $filename;

      if (class_exists($className)) {
        return true;
      }
    }
    return false;
  }
}

La carpeta “/configs” contiene la información básica sobre el módulo, como el nombre, entre otros valores.

Archivo: “/configs/default.conf.php”

<?php
global $arrConfModule;
$arrConfModule = [
  'module_name' => 'my_issabel_module',
  'module_title' => 'My Issabel Module',
  'templates_dir' => 'themes',
  'pagination' => 20
];

La carpeta “/lang” contiene los archivos con las traducciones de los terminos a los distintos idiomas soportados por Issabel.

La carpeta “/themes” contiene las vistas, y los archivos de estilo (css) o javascript (js) que pueda requerir el módulo.

Y la carpeta “/tests”, es decir la mas importante, es la que contiene los archivos con las pruebas.

Escribir tests

Antes de empezar a escribir las primeras pruebas, primero voy a crear el archivo que permitirá la ejecución de las pruebas.

Archivo: “/tests/TestCase.php”

<?php
namespace Tests;
require_once __DIR__ . '/../bootstrap/bootstrap.php';
require_once __DIR__ . '/../vendor/autoload.php';
abstract class TestCase extends \PHPUnit_Framework_TestCase
{
  protected $_faker;

  protected function setUp()
  {
    $this->_faker = \Faker\Factory::create();
  }
}

En este archivo, se incluye el archivo de inicio, es decir el que carga los archivos con las clases y librerias del framework, junto con las librerías que se van a usar para hacer las pruebas, como PHPUnit, faker, entre otras.

Una vez hecho esto, ya se pueden escribir las primeras pruebas. En esta ocasión, voy a crear dos pruebas.

En la primera prueba, quiero contar la cantidad de dispositivos sip que se han creado, para lo cual voy a ejecutar una sentencia del tipo count a la tabla “devices” de la base de datos “asterisk”.

En la segunda prueba, quiero traer los dispositivos sip creados, de la misma tabla del caso anterior.

Archivo: “/tests/Unit/Models/DeviceModelTest.php”

<?php
namespace Tests\Unit\Models;
require_once __DIR__ . '/../../TestCase.php';
use Tests\TestCase;
use \Mockery as m;
use App\Models\DeviceModel;
class DeviceModelTest extends TestCase
{
  protected $_paloDB;

  protected function setUp()
  {
    parent::setUp();
    $this->_paloDB = m::mock('\paloDB');
  }

  protected function tearDown()
  {
    m::close();
  }
public function testItCountsAllDevices()
  {
    $query = "SELECT COUNT(id) AS total FROM `asterisk`.`devices` WHERE tech = ?";
    $parameters = ['sip'];

    $queryResult = [
      'total' => 10,
    ];

    $this->_paloDB->shouldReceive('getFirstRowQuery')
      ->once()
      ->with($query, true, $parameters)
      ->andReturn($queryResult);

    $model = new DeviceModel($this->_paloDB);

    $this->assertEquals($queryResult, $model->count());
  }
public function testItGetsAllDevices()
  {
    $query = "SELECT user, description FROM `asterisk`.`devices` WHERE tech = ?";
    $parameters = ['sip'];

    $queryResult = [
      [
        'user' => $this->_faker->unique()->randomNumber(4, true),
        'description' => $this->_faker->name,
      ],
      [
        'user' => $this->_faker->unique()->randomNumber(4, true),
        'description' => $this->_faker->name,
      ],
      [
        'user' => $this->_faker->unique()->randomNumber(4, true),
        'description' => $this->_faker->name,
      ],
    ];

    $this->_paloDB->shouldReceive('fetchTable')
      ->once()
      ->with($query, true, $parameters)
      ->andReturn($queryResult);

    $model = new DeviceModel($this->_paloDB);

    $this->assertEquals($queryResult, $model->index());
  }
}

Voy a tratar de explicar con un poco mas de detalle el código.

  protected function setUp()
  {
    parent::setUp();
    $this->_paloDB = m::mock('\paloDB');
  }

Aquí creo un mock de la clase “paloDB” que es la que maneja la comunicación con la base de datos.

  public function testItCountsAllDevices()
  {
    $query = "SELECT COUNT(id) AS total FROM `asterisk`.`devices` WHERE tech = ?";
    $parameters = ['sip'];

    $queryResult = [
      'total' => 10,
    ];

    $this->_paloDB->shouldReceive('getFirstRowQuery')
      ->once()
      ->with($query, true, $parameters)
      ->andReturn($queryResult);

    $model = new DeviceModel($this->_paloDB);

    $this->assertEquals($queryResult, $model->count());
  }

Esta es la primera prueba, en donde defino la sentencia sql que deseo que se ejecute:

$query = "SELECT COUNT(id) AS total FROM `asterisk`.`devices` WHERE tech = ?";

Los parámetros necesarios para la búsqueda:

$parameters = ['sip'];

El resultado esperado:

$queryResult = [
   'total' => 10,
 ];

Y la creación del mock, que va a simular la consulta a la base de datos:

$this->_paloDB->shouldReceive('getFirstRowQuery')
      ->once()
      ->with($query, true, $parameters)
      ->andReturn($queryResult);

Al definir el mock, se utilizan los parámetros especificados anteriormente. Pero, ¿Por qué simular la consulta a la base de datos? Existen varias razones, la primera es que lo que se quiere probar es la clase que se esta creando, por ejemplo, el test podría fallar por que la conexión a la base está mal definida, no por que la lógica del modelo está incorrecta. Otra razón, es que se tendría que preparar la base de datos, de tal forma que siempre se encuentre en el mismo estado al ejecutar este test.

Al ejecutar por primera vez la prueba, se obtiene el siguiente resultado:

Muestra que no ha encontrado el archivo con el modelo “DeviceModel”, y tiene toda la razón. Así es como se debe empezar a escribir los componentes del módulo para Issabel, es decir, primero la prueba, luego la solución.

Solución

Archivo: “/app/Models/DeviceModel.php”

<?php
namespace App\Models;
use App\Models\Model;
class DeviceModel extends Model
{
  /**
   * Count rows of table devices
   * @return array             
   */
  public function count()
  {
    $query = "SELECT COUNT(id) AS total FROM `asterisk`.`devices` WHERE tech = ?";
    $parameters = ['sip'];
return $this->fetchSingleRow($query, $parameters);
  }
/**
   * Get sip devices    
   * @return array             
   */
  public function index()
  {
    $query = "SELECT user, description FROM `asterisk`.`devices` WHERE tech = ?";
    $parameters = ['sip'];
return $this->fetchRows($query, $parameters);
  }
}

En el archivo anterior, se crearon, los dos métodos necesarios para que las pruebas pasen.

Pero hay mucho más. Si se fijan, el código luce limpio. Uno de los principios de programación que siempre aplico es DRY (do not repeat yourself). Algo que me molesta de la estructura actual de los módulos de Issabel (heredados de Elastix), es que se repite el código una y otra vez. Para no repetir el código varias veces, he creado el siguiente archivo:

Archivo: “/app/Models/Model.php”

<?php
namespace App\Models;
use App\Contracts\ModelContract;
use App\Helpers\ExecuteQueryHelper;
abstract class Model implements ModelContract
{
  use ExecuteQueryHelper;
/**
   * Conection to database
   * @var object
   */
  protected $_paloDB;
/**
   * Create instance of object
   * @param \paloDB $paloDB
   */
  public function __construct(\paloDB $paloDB)
  {
    $this->_paloDB = $paloDB;
  }
}

En este archivo es una clase abstracta, cuya función principal es recrear un comportamiento común. En este caso, lo que hace es definir el constructor, con la instancia de paloDB, que como dije anteriormente, maneja la conexión a la base de datos, e incluye una interfaz y un trait.

Una interfaz en la vida diaria es una manera común de usar cierto tipo de objetos, por ejemplo esperamos que todos los autos, sin importar su marca, modelo, año o tipo, tengan ciertos elementos como son un volante, es como un contrato. En este caso, deseo que todos mis modelos, tengan los métodos “count” e “index”.

Archivo: “/app/Contracts/ModelContract.php”

<?php
namespace App\Contracts;
interface ModelContract
{
  /**
   * Method to to count rows
   * 
   * @return array
   */
  public function count();

  /**
   * Method to get rows
   * 
   * @return mixed
   */
  public function index();
}

En cambio los traits son mecanismos que permiten reutilizar código.

Archivo: “/app/Helpers/ExecuteQueryHelper.php”

<?php
namespace App\Helpers;
trait ExecuteQueryHelper
{
  /**
   * Get rows of database
   * @param  string $query
   * @param  array $parameters
   * @return array
   */
  public function fetchRows($query, $parameters = [])
  {
    $result = $this->_paloDB->fetchTable($query, true, $parameters);
    return is_array($result) ? $result : [];
  }

  /**
   * Get single row of database
   * @param  string $query     
   * @param  array $parameters
   * @return mixed            
   */
  public function fetchSingleRow($query, $parameters = [])
  {
    $result = $this->_paloDB->getFirstRowQuery($query, true, $parameters);
    return is_array($result) && count($result) > 0 ? $result : null;
  }

  /**
   * Execute query in database
   * @param  string $query     
   * @param  array  $parameters
   * @return bool            
   */
  public function executeQuery($query, $parameters = [])
  {
    return $this->_paloDB->genQuery($query, $parameters);
  }
}

En este caso, el trait, es una ayuda usada para ejecutar las sentencias sql usando ciertos métodos de la clase “paloDB”. También dan formato a las respuestas.

Espero no haber mareado a nadie, con tanto código y conceptos. Es un poco avanzada la temática que trato en esta serie, no es algo que se aprende de la noche a la mañana, como mencioné en la entrega anterior, es el producto de la experiencia acumulada, y el aplicar practicas para el desarrollo de software que ayudan a tener un código limpio y con el menor número de fallas.

Y algunos se preguntarán, ¿Para qué me complico la vida, con tests, interfaces, traits, etc?. Bueno en este caso, luce como innecesario, por que estoy consultando una tabla de una base de datos, las líneas de código son pocas, pero que pasa si tengo que consultar 10 tablas de la base de datos, o ejecutar acciones complejas, o si tengo un equipo haciendo cambios, como sé que el trabajo que realiza cada miembro del equipo está bien y no rompe lo que otros hicieron, pues bien, para eso son las pruebas.

Recién escuche uno de las mejores razones por las cuales se deben realizar pruebas: “Por sanidad mental”. El navegar por el browser, crear tres cosas, y ver el comportamiento, no es suficiente para garantizar que mi código está bien hecho. Recuerdo que cuando trabajaba en Palosanto Solutions, al liberar una nueva versión de Callcenter PRO, se producian errores absurdos, con la licencia, o con los reportes, que se hubiesen evitado, si se tenia una suite de pruebas.

En la siguiente entrega espero completar el tutorial, con la creación de la interfaz gráfica.

El link hacia el repositorio de github, con el contenido de los archivos es: https://github.com/juanelojga/my_issabel_module

Si tienen dudas, comentarios, sugerencias mi correo es: juanelojga@gmail.com.

Hasta la proxima.


메타데이터
post_id
b27c3dc2de2c
slug
usando-tdd-para-desarrollar-un-modulo-para-issabel-parte-ii-b27c3dc2de2c
url
https://medium.com/@juanelojga/usando-tdd-para-desarrollar-un-modulo-para-issabel-parte-ii-b27c3dc2de2c
canonical_url
https://medium.com/@juanelojga/usando-tdd-para-desarrollar-un-modulo-para-issabel-parte-ii-b27c3dc2de2c
author_url
https://medium.com/@juanelojga
status
ok
fetched_at
2026-06-27 23:56:40