unitarias test que pruebas probar example datos curso conexion php unit-testing pdo phpunit zend-framework3

que - test phpunit



¿Cómo reducir el número de conexiones de base de datos en las pruebas en PHPUnit y ZF3? (3)

Estoy escribiendo pruebas de integración / base de datos para una aplicación Zend Framework 3 usando

  • zendframework / zend-test 3.1.0 ,
  • phpunit / phpunit 6.2.2 , y
  • phpunit / dbunit 3.0.0

Mis pruebas están fallando debido a la

Connect Error: SQLSTATE[HY000] [1040] Too many connections

Establecí algunos puntos de interrupción y eché un vistazo a la base de datos:

SHOW STATUS WHERE `variable_name` = ''Threads_connected'';

Y he visto más de 100 conexiones abiertas.

Los he reducido desconectándolos en tearDown() :

protected function tearDown() { parent::tearDown(); if ($this->dbAdapter && $this->dbAdapter instanceof Adapter) { $this->dbAdapter->getDriver()->getConnection()->disconnect(); } }

Pero todavía tengo más de 80 conexiones abiertas.

¿Cómo reducir el número de conexiones de base de datos en las pruebas a un mínimo posible?

más información

(1) Tengo un montón de pruebas, donde dispatch un URI. Cada solicitud de este tipo provoca al menos una solicitud de base de datos, lo que provoca una nueva conexión de base de datos. Estas conexiones parecen no estar cerradas. Esto podría causar la mayoría de las conexiones. (Pero todavía no he encontrado una manera de hacer que la aplicación cierre las conexiones después de que se procese la solicitud).

(2) Uno de los problemas podría ser mi prueba contra la base de datos:

protected function retrieveActualData($table, $idColumn, $idValue) { $sql = new Sql($this->dbAdapter); $select = $sql->select($table); $select->where([$table . ''.'' . $idColumn . '' = ?'' => $idValue]); $statement = $sql->prepareStatementForSqlObject($select); $result = $statement->execute(); $data = $result->current(); return $data; }

Pero la llamada de $this->dbAdapter->getDriver()->getConnection()->disconnect() antes de la return no dio nada.

Ejemplo de uso en un método de prueba:

public function testInputDataActionSaving() { // The getFormParams(...) returns an array with the needed input. $formParams = $this->getFormParams(self::FORM_CREATE_CLUSTER); $createWhateverUrl = ''/whatever/create''; $this->dispatch($createWhateverUrl, Request::METHOD_POST, $formParams); $this->assertEquals( $formParams[''whatever''][''some_param''], $this->retrieveActualData(''whatever'', ''id'', 2)[''some_param''] ); }

(3) Otro problema podría estar en la unidad PHP (¿o mi configuración?). (No haga nada, porque "PHPUnit no hace nada relacionado con las conexiones de base de datos", vea this comentario). De todos modos, incluso si no es un problema de PHPUnit, el hecho es que después de la línea

$testSuite = $configuration->getTestSuiteConfiguration($this->arguments[''testsuite''] ?? null);

en PHPUnit/TextUI/Command obtengo 31 conexiones nuevas.


El enfoque limpio y adecuado

Esto parece ser un problema si "su código está escrito de una manera que es difícil de probar" . La conexión DB debe ser manejada por DIC o (en el caso de algún grupo de conexiones) alguna clase especializada. Básicamente, la clase, que contiene retrieveActualData() debe tener la instancia de Sql se pasa como una dependencia en un constructor.

En cambio, parece que su clase Sql es un contenedor PDO dañino, que (lo más probable) estableció una conexión de base de datos cada vez que crea una instancia. En su lugar, debería compartir la misma instancia de PDO entre varias clases. De esa manera, ambos pueden controlar la cantidad de conexiones establecidas y tener una manera de probar su código en (algunos) aislamiento.

Entonces, la solución principal es: su código es malo, pero puede limpiarlo.

En lugar de incluir new fragmentos de código en el árbol de ejecución, pase la conexión como una dependencia y compártala.

De esta manera, las pruebas pueden avanzar hacia el uso de varios simulacros y talones, que le ayudan a aislar las estructuras probadas.

En caso de lógica de enlace DB y gremlins

Pero también hay un aspecto más práctico, que debes considerar. Use SQLite en lugar de una base de datos real en sus pruebas de integración. DOP admite esa opción (solo tiene que proporcionar un DSN diferente para su código de prueba).

Si cambia a usar SQLite como su "base de datos de prueba", podrá tener un estado de base de datos bien definido (múltiple) contra el cual puede probar su código.

Tiene algo como la integration-002.db archivos integration-002.db , que contiene el estado de la base de datos preparada. En el arranque de sus pruebas de integración, simplemente copie los archivos de base de datos sqlite preparados de integration-0902.db a live-002.db y ejecute todas las pruebas.

use PHPUnit/Framework/TestCase; final class CombinedTest extends TestCase { public static function setUpBeforeClass() { copy(FIXTURE_PATH . ''/integration-02.db'', FIXTURE_PATH . ''/live-02.db''); } // your test go here }

De esa manera, obtendrás un mejor control sobre tu estado de persistencia y tus pruebas se ejecutarán mucho más rápido, ya que no hay una pila de red involucrada.

También puede preparar cualquier número de bases de datos de prueba y agregar nuevas, cuando se descubra un nuevo error. Este enfoque le permitirá recrear escenarios más complejos en su base de datos e incluso simular la corrupción de datos.

Puedes ver este enfoque en la práctica en this proyecto.

PS desde la experiencia personal: el uso de SQLite en las pruebas de integración también mejora la calidad general de los códigos SQL (si no está utilizando constructores de consultas, sino que está escribiendo mapeadores de datos personalizados). Porque lo obliga a considerar las diferencias entre la funcionalidad disponible en SQLite contra MariaDB o PostgreSQL. Pero es una de esas cosas de "su millaje puede variar".

PPS puede utilizar ambos enfoques sugeridos al mismo tiempo, ya que solo se realzarán mutuamente.


Parece que está inyectando a través de su prueba en lugar de la aplicación. El código de su aplicación debe estar manejando esto correctamente, en lugar de su código de prueba. Se conecta y se cierra una vez por cada ejecución de la aplicación.

Su función tearDown() sugiere que la conectividad de su base de datos está realmente dentro de su función setUp() , que la conectará una vez por prueba. Si su código de conexión utiliza PDO::ATTR_PERSISTENT y está configurando como en el ejemplo anterior, PDO::ATTR_PERSISTENT , quiere que las conexiones no PDO::ATTR_PERSISTENT se PDO::ATTR_PERSISTENT .

Puede intentar colocarlo en su bootstrap global, de modo que se conecte una vez para siempre y eliminar su desmontaje si ese no es el caso.


Probablemente haya configurado su PHP / DB para usar conexiones persistentes. Esta es la única manera en que esas conexiones permanecen allí después de que la prueba termina su ejecución. No está tan mal.

Desde el manual : las conexiones persistentes son enlaces que no se cierran cuando finaliza la ejecución de su script. Cuando se solicita una conexión persistente, PHP comprueba si ya existe una conexión persistente idéntica (que permaneció abierta desde antes) y, si existe, la utiliza.

Una vez que tenga una conexión con el username@host:port de username@host:port establecido, hizo su cosa y se desconectó (ejecución final del chip), luego conecte nuevamente con el mismo username@host:port , sin importar las tablas que se estén utilizando, se conectará a través de la misma conexión enchufe.

Cuatro posibles razones para su problema

  1. porque estás ejecutando diferentes usuarios para conectarse al servidor db
  2. porque estás entregando nombres de tablas en conexión
  3. porque estás ejecutando múltiples pruebas a la vez
  4. porque estas construyendo conexiones múltiples

y el más posible es el 4-rth, porque es tentador crear una función frabric para crear el manejador de bases de datos cada vez que necesite una base de datos, es decir, crear una nueva conexión:

function getConnection() { // This is an example to test, that it do leave behind a non closed connection. // Skip "p:", to reduce connections left unless you are configured // globally for persistency, eg. by mysqlnd. // p: forced persistency $link = mysqli_connect("p:127.0.0.1", "my_user", "my_password", "my_db"); if (!$link) return false; return $link; }

El caso es que para cada llamada de un método ejemplar a lo largo del mismo hilo, se abrirá una conexión completamente nueva, porque realmente están pidiendo esto. Los sockets persistentes se reutilizan solo si no se utilizan más (el script del creador finaliza su ejecución anteriormente). (Al menos así fue como aprendí a usarlos hace unos años)

Para evitar crear demasiadas conexiones, debe reconstruir su fábrica de conexiones para almacenar todas las conexiones distintas y entregar aquellos enlaces que desee a pedido, sin tener que llamar al creador de conexiones una y otra vez. De esta manera, para un usuario en particular en un servidor particular, finalmente ejecutará una vez, por ejemplo. mysqli_connect para recuperar una conexión persistente del servidor y seguir reutilizándola hasta el final de la ejecución de su script.

class db { static $storage = array(); public static function getConnection($username = ''username'') { if (!array_key_exists($username, self::$storage) { $link = mysqli_connect("p:127.0.0.1", $username, "my_password", "my_db"); if (!$link) return false; self::$storage[$username] = $link; } return self::$storage[$username]; } } // --- $a = db::getConnection(); $b = db::getConnection(); // both $a and $b are the same connection, using the same socket on your server var_dump($a, $b);

Volviendo a los ejemplos entregados, probablemente se deba a una línea:

$sql = new Sql($this->dbAdapter);

ser ejecutado una y otra vez a lo largo de sus pruebas, o por el mismo conductor haciendo algo extraordinario cuando se reutiliza con frecuencia. Mi pregunta sería si el controlador no crea una nueva conexión cada vez que se ejecuta en getConnection() , o si el constructor de Sql() no crea una nueva conexión en cada llamada con un new Sql .

edit 1 - después de ver el código zf3:

Intente buscar si el código no está haciendo algo como en el ejemplo persistente . Pero a partir del uso de ZF3, preferiría suponer que está usando alguna extensión como mysqlnd, lo que hace que no use el controlador mysql nativo en favor de Streams con sus propios tiempos de espera.

edit 2 - db prueba uno tras otro:

A pesar de la persistencia del socket, no puede usarlos: el servidor SQL necesita tiempo para desconectar al usuario por completo y liberar un socket para una nueva conexión. Si está ejecutando las pruebas rápidamente una tras otra, aquí se explica que todas las pruebas se ejecutan y destruyen, lo que puede llevar a la creación de una nueva conexión en cada setUp() un archivo setUp() o en un archivo de inicio. Al ejecutar una carga de pruebas que están instanciando el servicio de base de datos (cualquier cosa que se llame Adapter/PDO/Conncetion::connect() puede producir una enorme cola de conexión para ser cerrada en la parte inferior de su uno para ser abierto Ahí es donde la configuración para la persistencia de socket debería resolver su problema.