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.
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.
![]() |
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_Abstractdesde el métodogetConnection(), sino un envoltorio (wrapper) específico de PHPUnit que se genera con el métodocreateZendDbConnection().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étodogetDataSet().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.
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 ;
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().
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.
![]() |
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. |
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.
Para cada prueba, PHPUnit crea una nueva instancia del TestCase y llama al método
setUp().El Database TestCase crea una instancia de un Database Tester que se encarga de preparar y limpiar la base de datos.
El database tester recopila la información sobre la conexión a la base de datos y el dataset inicial a partir de
getConnection()ygetDataSet(), que son ambos métodos abstractos y deben ser implementados por cualquier Database Testcase.Por defecto, el database tester trunca las tablas especificadas en el dataset indicado y a continuación inserta los datos proporcionados como fixture inicial.
Cuando el database tester termina de preparar la base de datos, PHPUnit ejecuta la prueba.
Después de ejecutar la prueba, se llama a
tearDown(). Dado que la base de datos se vacía ensetUp()antes de insertar el fixture inicial necesario, el database tester no ejecuta ninguna acción en esta etapa.
![]() |
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.
|
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.
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.
![[Note]](images/note.png)