Haciendo del Desarrollo y la Arquitectura Web, ciencia y pasión.

PHPunit: Testeo de bases de datos

Vamos a echar un ojo a la extensión database de PHPunit, PHPUnit_Extensions_ Database_TestCase, una de las extensiones que posee para testear operaciones contra la bbdd.

Cuando usamos esta extensión añadimos dos metodos importantes, getconnection() y getDataSet(). Con el primero obtenemos un objeto conexión a la base de datos. Con el segundo obtenemos un subconjunto de datos y lo escribimos en nuestra base de datos. Este dataset puede estar basado en un archivo estatico o en queries.

En escenarios complejos los ficheros estaticos o fixtures, nos ayudarán a establecer un escenario sobre el cual probar nuestros métodos. En ocasiones cuando es necesario probar un método que efectúa una operación muy concreta, por ejemplo efectuar el balance contable de un mes de un cliente en el presente año, una opción es crear un fixture personalizado que creará el escenario necesario previo a la prueba.

Una cosa hay que tener claro. Los tests unitarios no son test de bases de datos, un test unitario testea código y comprueba que ha hecho lo que tenia que hacer. Veamos lo que tiene que hacer un test de base de datos:

1.- Poner la base de datos en un determinado estado controlado y acotado (eso es, con un fixture).

2.- Ejecutar el código que efectúa operaciones con la base de datos.

3.- Verificar que el estado actual y lo esperado son lo mismo.

Es exactamente lo que haremos nosotros, crearemos una tabla vacía, insertaremos un registro y después comprobaremos que hay uno, si hay más o menos algo ha ido mal. Después iremos un paso mas allá, insertaremos un registro que sabemos que provocará una excepción, por ejemplo por duplicar un índice único, y esperaremos que el test provoque una excepción. Si no la provoca algo estará mal en el código y/o en la definición de la tabla.

 

Para el ejemplo disponemos de un esquema de base de datos muy sencillo:

Para el ejemplo también dispondremos de un fixture muy básico que nos valdrá para nuestros tests

<?xml version="1.0" ?>
<dataset>
    <table name="Ciudad">
        <column>id</column>
        <column>nombre</column>
        <column>provincia</column>
        
        <row>
            <value>1</value>
            <value>Pozuelo</value>
            <value>1</value>
        </row>
    </table>
    <table name="provincia">
        <column>id</column>
        <column>nombre</column>
        <row>
            <value>1</value>
            <value>Madrid</value>
        </row>
    </table>
    <table name="hoteles">
        <column>id</column>
        <column>nombre</column>
        <column>estrellas</column>
        <column>tipoHabitacion</column>
        <column>ciudad</column>
        <row>
            <value>1</value>
            <value>Melia Castilla</value>
            <value>5</value>
            <value>1</value>
            <value>1</value>     
        </row>
    </table>
</dataset>

Así quedaria nuestro test:


class HotelTest extends PHPUnit_Extensions_Database_TestCase {

    public function getConnection() {
        $pdo = new PDO("mysql:host=localhost;dbname=database_test", "root", "******");
        return $this->createDefaultDBConnection($pdo, "testconnection");
    }

    public function getDataSet() {
        return $this->createXMLDataSet("myFixtures.xml");
    }

    public function testSaveHotel() {

        $d = new persistDB();
        $d->connect();

        $result = $d->findAll();

        $this->assertEquals(count($result), 1);

        $hotel = new Hotel(array(
            'nombre' => 'Hotel Ritc Madrid',
            'estrellas' => 5,
            'tipoHabitacion' => 1,
            'ciudad' => 1
        ));
        $hotel->saveHotel($d->conn);

        $result = $d->findAll();

        $this->assertEquals(count($result), 2);
    }

El primer test como dije antes será un test muy sencillo, el test de la operación save de mi entidad cuando todo va bien. Este test dispone de dos asserts, quizá alguien pueda extrañarse 1? despues del save 2? bueno, es por que el fixture ya escribía un hotel en mi base de datos. Y no importa como deje mi base de datos el test, antes de empezar una nueva ejecución se truncará la tabla que aplique y las dejará como indique el fixture.

Al finalizar el primer test la base de datos quedará asi:


mysql> select * from hoteles;
+----+-------------------+-----------+----------------+--------+
| id | nombre            | estrellas | tipoHabitacion | ciudad |
+----+-------------------+-----------+----------------+--------+
|  1 | Melia Castilla    |         5 |              1 |      1 |
|  2 | Hotel Ritc Madrid |         5 |              1 |      1 |
+----+-------------------+-----------+----------------+--------+
2 rows in set (0.00 sec)

El segundo test tratará de salvar una entidad Hotel, igual que una que ya había definida en el fixture. La tabla hoteles tiene definida un índice único para la pareja nombre y ciudad, es decir no permite que haya dos hoteles con el mismo nombre en la misma ciudad. Nuestro test esperará que se lance una excepción y para indicar esto debemos añadir una anotación previa a la declaración, con @expectedException y a continuación el tipo de la excepción.


    /**
     * @expectedException Exception
     */
    public function testSaveHotelAndThrowAnException() {

        $d = new persistDB();
        $d->connect();

        $result = $d->findAll();

        $this->assertEquals(count($result), 1);

        $hotel = new Hotel(array(
            'nombre' => 'Melia Castilla',
            'estrellas' => 5,
            'tipoHabitacion' => 1,
            'ciudad' => 1
        ));
        $hotel->saveHotel($d->conn);

        $result = $d->findAll();

        $this->assertEquals(count($result), 2);
    }

}

Y el resultado de la ejecución de nuestro test será, dos tests, cuatro asercciones:


daniel@Hall9000:~/NetBeansProjects/pu1$ phpunit testClass.php 
PHPUnit 4.5.1 by Sebastian Bergmann and contributors.

..

Time: 735 ms, Memory: 3.75Mb

OK (2 tests, 4 assertions)

El codigo que estamos testeando seria asi:


Class persistDB {

    public $conn;

    public function connect() {
        try {
            $this->conn = new PDO('mysql:host=localhost;dbname=database_test', 'root', '*******', array(
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
        } catch (PDOException $e) {
            echo 'ERROR: ' . $e->getMessage();
        }
    }

    
    public function findAll() {
        try {
            $stmt = $this->conn->prepare('SELECT * FROM hoteles');
            $stmt->execute(array());

            $result = $stmt->fetchAll();

            return $result;
        } catch (PDOException $e) {
            echo 'ERROR: ' . $e->getMessage();
        }
    }

}

class Hotel {

    private $id;
    private $nombre;
    private $estrellas;
    private $tipoHabitacion;
    private $ciudad;

    public function __construct($data) {

        $this->nombre = $data['nombre'];
        $this->estrellas = $data['estrellas'];
        $this->tipoHabitacion = $data['tipoHabitacion'];
        $this->ciudad = $data['ciudad'];
    }

    public function saveHotel($persistDB, array $data = null) {
        try {
            $this->nombre = isset($data['nombre']) ? $data['nombre'] : $this->nombre; // para hacer updates.
            $this->estrellas = isset($data['estrellas']) ? $data['estrellas'] : $this->estrellas;
            $this->tipoHabitacion = isset($data['tipoHabitacion']) ? $data['tipoHabitacion'] : $this->tipoHabitacion;
            $this->ciudad = isset($data['ciudad']) ? $data['ciudad'] : $this->ciudad;

            $sql = "INSERT INTO hoteles (nombre, estrellas, tipoHabitacion, ciudad) VALUES (:NombreHotel, :EstrellasHotel, :TipoHabitacion, :CiudadHotel)";

            $query = $persistDB->prepare($sql);

            $r = $query->execute(array(
                ':NombreHotel' => $this->nombre,
                ':EstrellasHotel' => $this->estrellas,
                ':TipoHabitacion' => $this->tipoHabitacion,
                ':CiudadHotel' => $this->ciudad
            ));
        } catch (PDOException $e) {
            throw new Exception($e->getMessage());
        }
    }

}