TigerZF
🌐Español

68.3. Zend_Test_PHPUnit_Db

El acoplamiento entre el acceso a datos y el modelo de dominio a menudo requiere el uso de una base de datos con fines de prueba. Pero la base de datos persiste entre distintas pruebas, lo que lleva a resultados de prueba que pueden afectarse entre sí. Además, preparar la base de datos para poder ejecutar una prueba requiere bastante trabajo. La extensión Database de PHPUnit simplifica las pruebas con una base de datos ofreciendo un mecanismo muy simple para preparar y limpiar la base de datos entre distintas pruebas. Este componente extiende la extensión Database de PHPUnit con código específico de Zend Framework, de modo que escribir pruebas de base de datos contra una aplicación de Zend Framework se simplifica.

Las pruebas de base de datos se pueden explicar mediante dos entidades conceptuales, DataSets y DataTables. Internamente, la extensión Database de PHPUnit puede construir una estructura de objetos de una base de datos, sus tablas y las filas que contienen a partir de archivos de configuración o del contenido real de la base de datos. Este grafo de objetos abstracto se puede comparar entonces mediante aserciones. Un caso de uso habitual en las pruebas de base de datos consiste en preparar algunas tablas con datos semilla, realizar después ciertas operaciones y, finalmente, afirmar que el estado de la base de datos tras la operación es igual a algún estado esperado predefinido. Zend_Test_PHPUnit_Db simplifica esta tarea al permitir generar DataSets y DataTables a partir de instancias existentes de Zend_Db_Table_Abstract o Zend_Db_Table_Rowset_Abstract.

Además, este componente permite integrar cualquier Zend_Db_Adapter_Abstract para las pruebas, mientras que la extensión original solo funciona con PDO. Este componente también incluye una implementación de Test Adapter para Zend_Db_Adapter_Abstract. Esta permite instanciar un Db Adapter que no requiere base de datos alguna y actúa como una pila de SQL y de resultados que es utilizada por los métodos de la API.

68.3.1. Inicio rápido

68.3.1.1. Configurar un TestCase de base de datos

Vamos ahora a escribir algunas pruebas de base de datos para el ejemplo de la base de datos de errores (Bug Database) de la documentación de Zend_Db_Table. Primero empezamos comprobando que al insertar un nuevo bug este se guarda correctamente en la base de datos. Primero tenemos que configurar una clase de prueba que extienda Zend_Test_PHPUnit_DatabaseTestCase. Esta clase extiende la extensión Database de PHPUnit, la cual a su vez extiende la clase básica PHPUnit_Framework_TestCase. Un testcase de base de datos contiene dos métodos abstractos que deben implementarse, uno para la conexión a la base de datos y otro para el dataset inicial que debe usarse como semilla o fixture.

[Note] Nota

Debería estar familiarizado con la extensión Database de PHPUnit para seguir este inicio rápido con facilidad. Aunque todos los conceptos se explican en esta documentación, puede resultar útil leer primero la documentación de PHPUnit.

class BugsTest extends Zend_Test_PHPUnit_DatabaseTestCase
{
    private $_connectionMock;

    /**
     * Returns the test database connection.
     *
     * @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
     */
    protected function getConnection()
    {
        if($this->_connectionMock == null) {
            $connection = Zend_Db::factory(...);
            $this->_connectionMock = $this->createZendDbConnection(
                $connection, 'zfunittests'
            );
            Zend_Db_Table_Abstract::setDefaultAdapter($connection);
        }
        return $this->_connectionMock;
    }

    /**
     * @return PHPUnit_Extensions_Database_DataSet_IDataSet
     */
    protected function getDataSet()
    {
        return $this->createFlatXmlDataSet(
            dirname(__FILE__) . '/_files/bugsSeed.xml'
        );
    }
}

Aquí creamos la conexión a la base de datos e insertamos algunos datos semilla en la base de datos. Es importante prestar atención a algunos detalles de este código:

  • No se puede devolver directamente un Zend_Db_Adapter_Abstract desde el método getConnection(), sino un envoltorio (wrapper) específico de PHPUnit que se genera con el método createZendDbConnection().

  • El esquema de la base de datos (tablas y base de datos) no se vuelve a crear en cada ejecución de la prueba. La base de datos y las tablas deben crearse manualmente antes de ejecutar las pruebas.

  • Por defecto, las pruebas de base de datos truncan los datos durante setUp() y a continuación insertan los datos semilla que devuelve el método getDataSet().

  • Los DataSets deben implementar la interfaz PHPUnit_Extensions_Database_DataSet_IDataSet. PHPUnit incluye una amplia gama de tipos de archivos de configuración XML y YAML que permiten especificar el aspecto que deben tener las tablas y los datasets, y debería consultar la documentación de PHPUnit para obtener la información más reciente sobre estas especificaciones de datasets.

68.3.1.2. Especificar un dataset semilla

En la configuración anterior del testcase de base de datos hemos especificado un archivo semilla para el fixture de base de datos. Ahora creamos este archivo especificado en formato Flat XML:

<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
    <zfbugs bug_id="1" bug_description="system needs electricity to run"
        bug_status="NEW" created_on="2007-04-01 00:00:00"
        updated_on="2007-04-01 00:00:00" reported_by="goofy"
        assigned_to="mmouse" verified_by="dduck" />
    <zfbugs bug_id="2" bug_description="Implement Do What I Mean function"
        bug_status="VERIFIED" created_on="2007-04-02 00:00:00"
        updated_on="2007-04-02 00:00:00" reported_by="goofy"
        assigned_to="mmouse" verified_by="dduck" />
    <zfbugs bug_id="3" bug_description="Where are my keys?" bug_status="FIXED"
        created_on="2007-04-03 00:00:00" updated_on="2007-04-03 00:00:00"
        reported_by="dduck" assigned_to="mmouse" verified_by="dduck" />
    <zfbugs bug_id="4" bug_description="Bug no product" bug_status="INCOMPLETE"
        created_on="2007-04-04 00:00:00" updated_on="2007-04-04 00:00:00"
        reported_by="mmouse" assigned_to="goofy" verified_by="dduck" />
</dataset>

Trabajaremos con estas cuatro entradas en la tabla de base de datos "zfbugs" en los siguientes ejemplos. El esquema MySQL necesario para este ejemplo es:

CREATE TABLE IF NOT EXISTS `zfbugs` (
    `bug_id` int(11) NOT NULL auto_increment,
    `bug_description` varchar(100) default NULL,
    `bug_status` varchar(20) default NULL,
    `created_on` datetime default NULL,
    `updated_on` datetime default NULL,
    `reported_by` varchar(100) default NULL,
    `assigned_to` varchar(100) default NULL,
    `verified_by` varchar(100) default NULL,
PRIMARY KEY  (`bug_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 ;

68.3.1.3. Algunas pruebas iniciales de base de datos

Ahora que hemos implementado los dos métodos abstractos requeridos por Zend_Test_PHPUnit_DatabaseTestCase y especificado el contenido de la base de datos semilla, que se volverá a crear para cada nueva prueba, podemos pasar a hacer nuestra primera aserción. Esta será una prueba para insertar un nuevo bug.

class BugsTest extends Zend_Test_PHPUnit_DatabaseTestCase
{
    public function testBugInsertedIntoDatabase()
    {
        $bugsTable = new Bugs();

        $data = array(
            'created_on'      => '2007-03-22 00:00:00',
            'updated_on'      => '2007-03-22 00:00:00',
            'bug_description' => 'Something wrong',
            'bug_status'      => 'NEW',
            'reported_by'     => 'garfield',
            'verified_by'     => 'garfield',
            'assigned_to'     => 'mmouse',
        );

        $bugsTable->insert($data);

        $ds = new Zend_Test_PHPUnit_Db_DataSet_QueryDataSet(
            $this->getConnection()
        );
        $ds->addTable('zfbugs', 'SELECT * FROM zfbugs');

        $this->assertDataSetsEqual(
            $this->createFlatXmlDataSet(dirname(__FILE__)
                                      . "/_files/bugsInsertIntoAssertion.xml"),
            $ds
        );
    }
}

Hasta $bugsTable->insert($data); todo resulta familiar. Las líneas siguientes contienen el nombre del método de aserción. Queremos verificar que, después de insertar el nuevo bug, la base de datos se ha actualizado correctamente con los datos indicados. Para ello creamos una instancia de Zend_Test_PHPUnit_Db_DataSet_QueryDataSet y le proporcionamos una conexión a la base de datos. A continuación le indicaremos a este dataset que contiene una tabla "zfbugs" que viene dada por una sentencia SQL. Este estado actual/real de la base de datos se compara con el estado esperado de la base de datos, contenido en otro archivo XML "bugsInsertIntoAssertions.xml". Este archivo XML es una ligera variación del mostrado anteriormente y contiene otra fila con los datos esperados:

<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
    <!-- previous 4 rows -->
    <zfbugs bug_id="5" bug_description="Something wrong" bug_status="NEW"
        created_on="2007-03-22 00:00:00" updated_on="2007-03-22 00:00:00"
        reported_by="garfield" assigned_to="mmouse" verified_by="garfield" />
</dataset>

Existen otras formas de comprobar que el estado actual de la base de datos es igual a un estado esperado. La tabla "Bugs" del ejemplo ya conoce mucho sobre su estado interno, así que ¿por qué no aprovecharlo? El siguiente ejemplo comprobará que es posible eliminar de la base de datos:

class BugsTest extends Zend_Test_PHPUnit_DatabaseTestCase
{
    public function testBugDelete()
    {
        $bugsTable = new Bugs();

        $bugsTable->delete(
            $bugsTable->getAdapter()->quoteInto("bug_id = ?", 4)
        );

        $ds = new Zend_Test_PHPUnit_Db_DataSet_DbTableDataSet();
        $ds->addTable($bugsTable);

        $this->assertDataSetsEqual(
            $this->createFlatXmlDataSet(dirname(__FILE__)
                                      . "/_files/bugsDeleteAssertion.xml"),
            $ds
        );
    }
}

Aquí hemos creado un dataset Zend_Test_PHPUnit_Db_DataSet_DbTableDataSet, que toma cualquier instancia de Zend_Db_Table_Abstract y la añade al dataset con su nombre de tabla, en este ejemplo "zfbugs". Podría añadir varias tablas más si quisiera usando el método addTable(), en caso de querer comprobar el estado esperado de la base de datos en más de una tabla.

Aquí solo tenemos una tabla y comprobamos frente a un estado esperado de la base de datos en "bugsDeleteAssertion.xml", que es el dataset semilla original sin la fila con id 4.

Dado que en los ejemplos anteriores solo hemos comprobado que dos tablas específicas (no datasets) son iguales, también deberíamos ver cómo afirmar que dos tablas son iguales. Por ello, añadiremos otra prueba a nuestro TestCase que verifique el comportamiento de actualización de un dataset.

class BugsTest extends Zend_Test_PHPUnit_DatabaseTestCase
{
    public function testBugUpdate()
    {
        $bugsTable = new Bugs();

        $data = array(
            'updated_on'      => '2007-05-23',
            'bug_status'      => 'FIXED'
        );

        $where = $bugsTable->getAdapter()->quoteInto('bug_id = ?', 1);

        $bugsTable->update($data, $where);

        $rowset = $bugsTable->fetchAll();

        $ds        = new Zend_Test_PHPUnit_Db_DataSet_DbRowset($rowset);
        $assertion = $this->createFlatXmlDataSet(
            dirname(__FILE__) . '/_files/bugsUpdateAssertion.xml'
        );
        $expectedRowsets = $assertion->getTable('zfbugs');

        $this->assertTablesEqual(
            $expectedRowsets, $ds
        );
    }
}

Aquí creamos el estado actual de la base de datos a partir de una instancia de Zend_Db_Table_Rowset_Abstract junto con la instancia Zend_Test_PHPUnit_Db_DataSet_DbRowset($rowset), que crea una representación interna de datos del rowset. Esto se puede comparar de nuevo con otra tabla de datos usando la aserción $this->assertTablesEqual().

68.3.2. Uso, API y puntos de extensión

El inicio rápido ya ha proporcionado una buena introducción a cómo se pueden realizar pruebas de base de datos usando PHPUnit y Zend Framework. Esta sección ofrece una visión general de la API que incluye el componente Zend_Test_PHPUnit_Db y de su funcionamiento interno.

[Note] Algunas observaciones sobre las pruebas de base de datos

Al igual que el Controller TestCase prueba una aplicación a nivel de integración, el Database TestCase es un método de prueba de integración. Utiliza varias capas distintas de la aplicación con fines de prueba y, por lo tanto, debe usarse con precaución.

Cabe destacar que probar el dominio y la lógica de negocio con pruebas de integración como los TestCases de Controller y Database de Zend Framework es una mala práctica. El propósito de una prueba de integración es comprobar que varias partes de una aplicación funcionan bien cuando se conectan entre sí. Estas pruebas de integración no sustituyen la necesidad de un conjunto de pruebas unitarias que comprueben el dominio y la lógica de negocio a un nivel mucho más pequeño, la clase aislada.

68.3.2.1. La clase Zend_Test_PHPUnit_DatabaseTestCase

La clase Zend_Test_PHPUnit_DatabaseTestCase deriva de PHPUnit_Extensions_Database_TestCase, que permite configurar pruebas con un fixture de base de datos nuevo en cada ejecución de forma sencilla. La implementación de Zend ofrece algunas funciones adicionales de conveniencia respecto a la extensión Database de PHPUnit a la hora de usar recursos de Zend_Db dentro de las pruebas. El flujo de trabajo de un testcase de base de datos puede describirse de la siguiente manera.

  1. Para cada prueba, PHPUnit crea una nueva instancia del TestCase y llama al método setUp().

  2. El Database TestCase crea una instancia de un Database Tester que se encarga de preparar y limpiar la base de datos.

  3. El database tester recopila la información sobre la conexión a la base de datos y el dataset inicial a partir de getConnection() y getDataSet(), que son ambos métodos abstractos y deben ser implementados por cualquier Database Testcase.

  4. Por defecto, el database tester trunca las tablas especificadas en el dataset indicado y a continuación inserta los datos proporcionados como fixture inicial.

  5. Cuando el database tester termina de preparar la base de datos, PHPUnit ejecuta la prueba.

  6. Después de ejecutar la prueba, se llama a tearDown(). Dado que la base de datos se vacía en setUp() antes de insertar el fixture inicial necesario, el database tester no ejecuta ninguna acción en esta etapa.

[Note] Nota

El Database TestCase espera que el esquema de la base de datos y las tablas estén configurados correctamente para ejecutar las pruebas. No existe ningún mecanismo para crear ni eliminar tablas de base de datos.

La clase Zend_Test_PHPUnit_DatabaseTestCase dispone de algunas funciones de conveniencia que pueden ayudar a escribir pruebas que interactúan con la base de datos y con la extensión de pruebas de base de datos.

La siguiente tabla enumera únicamente los métodos nuevos en comparación con PHPUnit_Extensions_Database_TestCase, cuya API está documentada en la documentación de PHPUnit.

Tabla 68.1. Métodos de la API de Zend_Test_PHPUnit_DatabaseTestCase

Método Descripción
createZendDbConnection(Zend_Db_Adapter_Abstract $connection, $schema) Crea una instancia de conexión compatible con la extensión Database de PHPUnit a partir de una instancia de Zend_Db_Adapter_Abstract. Este método debería usarse en la configuración del testcase al implementar el método abstracto getConnection() del testcase de base de datos.
getAdapter() Método de conveniencia para acceder a la instancia subyacente de Zend_Db_Adapter_Abstract que se encuentra anidada dentro de la conexión de base de datos de PHPUnit creada con getConnection().
createDbRowset(Zend_Db_Table_Rowset_Abstract $rowset, $tableName = null) Crea un objeto DataTable relleno con los datos de una instancia dada de Zend_Db_Table_Rowset_Abstract. La tabla a la que está conectado el rowset se elige cuando $tableName es NULL.
createDbTable(Zend_Db_Table_Abstract $table, $where = null, $order = null, $count = null, $offset = null) Crea un objeto DataTable que representa los datos contenidos en una instancia de Zend_Db_Table_Abstract. Para obtener los datos se utiliza fetchAll(), donde los parámetros opcionales pueden usarse para restringir la tabla de datos a un determinado subconjunto.
createDbTableDataSet(array $tables=array()) Crea un DataSet que contiene las $tables indicadas, un array de instancias de Zend_Db_Table_Abstract.

68.3.2.2. Integrar las pruebas de base de datos con ControllerTestCase

Dado que PHP no admite herencia múltiple, no es posible usar los testcases de Controller y de Database de forma conjunta. Sin embargo, puede usar el database tester Zend_Test_PHPUnit_Db_SimpleTester en su testcase de controlador para configurar un fixture del entorno de base de datos en cada nueva prueba de controlador. El Database TestCase en general no es más que un conjunto de funciones de conveniencia a las que también se puede acceder y utilizar sin el testcase.

Ejemplo 68.3. Ejemplo de integración con la base de datos

Este ejemplo extiende la prueba User Controller Test de la documentación de Zend_Test_PHPUnit_ControllerTestCase para incluir una configuración de base de datos.

class UserControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{
    public function setUp()
    {
        $this->setupDatabase();
        $this->bootstrap = array($this, 'appBootstrap');
        parent::setUp();
    }

    public function setupDatabase()
    {
        $db = Zend_Db::factory(...);
        $connection = new Zend_Test_PHPUnit_Db_Connection($db,
                                                      'database_schema_name');
        $databaseTester = new Zend_Test_PHPUnit_Db_SimpleTester($connection);

        $databaseFixture =
                    new PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet(
                        dirname(__FILE__) . '/_files/initialUserFixture.xml'
                    );

        $databaseTester->setupDatabase($databaseFixture);
    }
}

Ahora se utiliza el dataset Flat XML "initialUserFixture.xml" para poner la base de datos en un estado inicial antes de cada prueba, exactamente igual a como funciona internamente el DatabaseTestCase.


68.3.3. Uso del adaptador de pruebas de base de datos

Hay ocasiones en las que no se desea probar partes de la aplicación con una base de datos real, pero se ve obligado a ello debido al acoplamiento. Zend_Test_DbAdapter ofrece una forma cómoda de usar una implementación de Zend_Db_Adapter_Abstract sin necesidad de abrir una conexión a la base de datos. Además, este Adapter es muy fácil de simular (mock) dentro de su conjunto de pruebas de PHPUnit, ya que no requiere argumentos en el constructor.

El Test Adapter actúa como una pila de diversos resultados de base de datos. El orden de los resultados debe ser implementado por el usuario, lo cual puede ser una tarea tediosa en pruebas que invocan muchas consultas de base de datos distintas, pero es justo el ayudante adecuado para pruebas en las que solo se ejecutan un puñado de consultas y se conoce el orden exacto de los resultados que deben devolverse al código del usuario.

$adapter   = new Zend_Test_DbAdapter();
$stmt1Rows = array(array('foo' => 'bar'), array('foo' => 'baz'));
$stmt1     = Zend_Test_DbStatement::createSelectStatement($stmt1Rows);
$adapter->appendStatementToStack($stmt1);

$stmt2Rows = array(array('foo' => 'bar'), array('foo' => 'baz'));
$stmt2     = Zend_Test_DbStatement::createSelectStatement($stmt2Rows);
$adapter->appendStatementToStack($stmt2);

$rs = $adapter->query('SELECT ...'); // Returns Statement 2
while ($row = $rs->fetch()) {
    echo $rs['foo']; // Prints "Bar", "Baz"
}
$rs = $adapter->query('SELECT ...'); // Returns Statement 1

El comportamiento de cualquier adaptador de base de datos real se simula en la mayor medida posible, de modo que métodos como fetchAll(), fetchObject(), fetchColumn y otros funcionan con el test adapter.

También puede colocar sentencias INSERT, UPDATE y DELETE en la pila de resultados; sin embargo, estas solo devuelven una sentencia que permite especificar el resultado de $stmt->rowCount().

$adapter = new Zend_Test_DbAdapter();
$adapter->appendStatementToStack(
    Zend_Test_DbStatement::createInsertStatement(1)
);
$adapter->appendStatementToStack(
    Zend_Test_DbStatement::createUpdateStatement(2)
);
$adapter->appendStatementToStack(
    Zend_Test_DbStatement::createDeleteStatement(10
));

Por defecto, el generador de perfiles (profiler) de consultas está activado, de modo que se pueden obtener las sentencias SQL ejecutadas y sus parámetros asociados para comprobar la corrección de la ejecución.

$adapter = new Zend_Test_DbAdapter();
$stmt = $adapter->query("SELECT * FROM bugs");

$qp = $adapter->getProfiler()->getLastQueryProfile();

echo $qp->getQuerY(); // SELECT * FROM bugs

El test adapter nunca comprueba si la consulta especificada es realmente del tipo SELECT, DELETE, INSERT o UPDATE que se devuelve a continuación desde la pila. El orden correcto de devolución de los datos debe ser implementado por el usuario del test adapter.

El Test adapter también especifica métodos para simular el uso de los métodos listTables(), describeTables() y lastInsertId(). Además, usando setQuoteIdentifierSymbol() puede especificar qué símbolo debe usarse para el entrecomillado; por defecto no se utiliza ninguno.