Vraag Hoe het aantal databaseverbindingen in tests in PHPUnit en ZF3 verminderen?


Ik schrijf integratie- / databasetests voor een Zend Framework 3-toepassing met behulp van

  • ZendFramework / zend-toets 3.1.0,
  • PHPUnit / PHPUnit 6.2.2, en
  • PHPUnit / dbunit 3.0.0

Mijn tests mislukken als gevolg van de

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

Ik heb enkele breekpunten ingesteld en heb de database bekeken:

SHOW STATUS WHERE `variable_name` = 'Threads_connected';

En ik heb het echt gezien 100 geopende verbindingen.

Ik heb ze gereduceerd door de verbinding te verbreken in de tearDown():

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

Maar ik ben nog steeds over 80 geopende verbindingen.

Hoe het aantal databaseverbindingen in tests tot een minimum te verlagen?


meer informatie

(1) Ik heb veel tests, waarbij ik dispatch een URI. Elk dergelijk verzoek veroorzaakt ten minste één databaseaanvraag, die een nieuwe databaseverbinding veroorzaakt. Deze verbindingen lijken niet te zijn gesloten. Dit kan de meeste verbindingen veroorzaken. (Maar ik heb nog geen manier gevonden om de toepassing de verbindingen te laten sluiten nadat het verzoek is verwerkt.)

(2) Een van de problemen kan mijn testen tegen de database zijn:

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;
}

Maar de roep van de $this->dbAdapter->getDriver()->getConnection()->disconnect() voor de return gaf niets.

Voorbeeld van gebruik in een testmethode:

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) Een ander probleem kan zijn in de PHPUnit (of mijn configuratie ervan?). (Opstarten, omdat "PHPUnit niets doet met betrekking tot databaseverbindingen.", Zie deze opmerking.) Hoe dan ook, zelfs als het geen PHPUnit-probleem is, is het feit dat na de regel

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

in de PHPUnit\TextUI\Command ik krijg 31 nieuwe verbindingen.


16
2017-08-12 14:05


oorsprong


antwoorden:


De schone en juiste aanpak

Dit lijkt een probleem te zijn als "uw code is geschreven op een manier die moeilijk te testen is". De DB-verbinding moet worden afgehandeld door DIC of (in het geval van een of andere verbindingspool) enige gespecialiseerde klasse. Kortom, de klasse, die bevat retrieveActualData() zou het moeten hebben Sql bijvoorbeeld worden doorgegeven als een afhankelijkheid in een constructor.

In plaats daarvan lijkt het op jou Sql klas is schadelijk PDO wrapper, dat (waarschijnlijk) een DB-verbinding tot stand heeft gebracht telkens wanneer u een exemplaar maakt. In plaats daarvan zou je dezelfde PDO-instantie moeten delen tussen meerdere klassen. Op die manier kun je zowel het aantal verbindingen bepalen dat je hebt ingesteld en een manier hebben om je code in (enige) isolatie te testen.

De primaire oplossing is dus - je code is slecht, maar je kunt het opruimen.

In plaats van hebben new snippets die diep in uw uitvoeringsboom zijn gesprenkeld, geven de verbinding door als een afhankelijkheid en delen deze.

Op deze manier kun je testen naar het gebruik van verschillende moppen en stubs, die je helpen de geteste structuren te isoleren.

In het geval van DB-gebonden logica en gremlins

Maar er is ook een meer praktisch aspect dat je zou moeten overwegen. Gebruik SQLite in plaats van echte database in uw integratietests. PDO ondersteunt die optie (je hoeft alleen maar een andere DSN op te geven voor je testcode).

Als u overschakelt naar het gebruik van SQLite als uw "test-DB", kunt u een duidelijk gedefinieerde DB-status (meerdere) hebben waarmee u uw code kunt testen.

Je hebt zoiets als een bestand integration-002.db, die de voorbereide database-status bevat. In de bootstrap van uw integratietests kopieert u alleen die voorbereide sqlite-databasebestanden van integration-0902.db naar live-002.db en voer alle tests uit.

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

}

Op die manier verkrijgt u zowel betere controle over uw persistentiestaat en uw tests zullen veel sneller verlopen, omdat er geen netwerkstack bij betrokken is.

U kunt ook een onbeperkt aantal test-databases voorbereiden en nieuwe toevoegen, wanneer een nieuwe bug wordt ontdekt. Met deze aanpak kunt u complexere scenario's opnieuw creëren in uw DB en zelfs gegevensbeschadiging simuleren.

U kunt deze aanpak in de praktijk inzien deze project.


Postscriptum uit persoonlijke ervaring - het gebruik van SQLite in de integratietests verbetert ook de algemene kwaliteit van die SQL-code (als u geen gebruik maakt van query-builders, maar in plaats daarvan aangepaste gegevens-mappers schrijft). Omdat het je dwingt om na te denken over de verschillen tussen de beschikbare functionaliteit in SQLite tegen MariaDB of PostgreSQL. Maar het is een van die dingen die "uw kilometerstand kan variëren".

P.P.S. je kunt beide voorgestelde benaderingen tegelijkertijd gebruiken, omdat ze elkaar alleen maar zullen verbeteren.


8
2017-08-20 23:09



U hebt waarschijnlijk uw PHP / DB geconfigureerd om permanente verbindingen te gebruiken. Alleen zo blijven die verbindingen daar nadat de test de uitvoering heeft beëindigd. Het valt wel mee.

Van handleiding: Persistente verbindingen zijn links die niet worden gesloten wanneer de uitvoering van uw script eindigt. Wanneer een persistente verbinding wordt aangevraagd, controleert PHP of er al een identieke persistente verbinding is (die eerder is geopend) - en als deze bestaat, wordt deze gebruikt.

Zodra u verbinding hebt met username@host:port vastgesteld, deed je ding en verbrak (scipt einde uitvoering), daarna opnieuw verbinden met hetzelfde username@host:portongeacht de tafels die worden gebruikt, wordt u verbonden via dezelfde aansluitbus.

Vier mogelijke redenen voor uw probleem

  1. omdat u verschillende gebruikers gebruikt om verbinding te maken met de db-server
  2. omdat je tabelnamen in verbinding brengt
  3. omdat u meerdere tests tegelijkertijd uitvoert
  4. omdat je meerdere verbindingen bouwt

en de meest mogelijke is de 4-ste, omdat het verleidelijk is om een ​​frabric-functie te maken om db-handle aan te maken elke keer dat je een database nodig hebt, dat is het creëren van een nieuwe verbinding:

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;
}

Geval is, dat er voor elke aanroep van een voorbeeldachtige methode langs dezelfde thread een heel nieuwe verbinding zal worden geopend, omdat je hier echt om vraagt. Persistente sockets worden alleen opnieuw gebruikt als ze niet meer worden gebruikt (het script van de maker beëindigt eerder de uitvoering). (het was tenminste de manier waarop ik een paar jaar geleden werd geleerd om ze te gebruiken)

Om te voorkomen dat er te veel verbindingen ontstaan, moet u uw verbindingsfabriek opnieuw opbouwen om alles op te slaan onderscheiden verbindingen maken en die links op verzoek leveren, zonder verbindingsbouwer steeds maar opnieuw te bellen. Op deze manier voor een bepaalde gebruiker naar een partticular server die u uiteindelijk eenmaal eenmaal zult gebruiken. mysqli_connectom een ​​blijvende verbinding van de server op te halen en te blijven gebruiken tot het einde van uw scriptuitvoering.

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);

Terugkomend op uw geleverde voorbeelden, komt dit waarschijnlijk door een regel:

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

steeds opnieuw worden uitgevoerd tijdens uw tests, of doordat de bestuurder zelf iets bijzonders doet als hij vaak wordt hergebruikt. Mijn vraag zou zijn als de bestuurder niet telkens een nieuwe verbinding maakt getConnection() wordt gerund, of als de constructor van Sql() geen nieuwe verbinding maken bij elk gesprek met new Sql.

bewerk 1 - na een kijkje in de zf3-code:

Probeer te zoeken of code niet zoiets doet als in persistent voorbeeld. Maar vanaf het gebruik van ZF3 zou ik liever raden dat je een extensie zoals mysqlnd gebruikt, waardoor je geen native mysql-stuurprogramma gebruikt in het voordeel van streams met hun eigen time-outs.

edit 2 - db test de een na de ander:

Ondanks de persistentie van de socket - je mag ze helemaal niet gebruiken: SQL-server heeft tijd nodig om de gebruiker volledig te ontkoppelen en een socket vrij te maken voor een nieuwe verbinding. Als u snel achter elkaar een test uitvoert, is er iets dat elke test uitvoert en vernietigt, wat kan leiden tot het maken van een nieuwe verbinding setUp() aanroep van call of bootsrap. Door een hele reeks tests uit te voeren die DB-services ondersteunen (alles wat ze zullen bellen) Adapter/PDO/Conncetion::connect() je kunt een enorme wachtrij van verbinding maken om te worden afgesloten aan de onderkant van je te openen eentje. Dat zou zijn waar het configureren van socket-persistentie uw probleem zou moeten oplossen.


3
2017-08-21 08:37



Het lijkt erop dat u door uw test injecteert in plaats van de toepassing. Uw toepassingscode moet hier correct mee omgaan, in plaats van uw testcode. U sluit en sluit eenmaal per toepassingsrun.

Jouw tearDown() functie suggereert dat de connectiviteit van uw database zich daadwerkelijk in uw bevindt setUp() functie, die het eenmaal per test zal verbinden. Als uw verbindingscode gebruikt PDO::ATTR_PERSISTENT en je gaat zoals hierboven op, haalt het eruit, je wilt onverwerkte verbindingen om te sterven.

Je kunt proberen het in je globale bootstrap te plaatsen, zodat het voor altijd één keer verbinding maakt en je demontage verwijdert als dat niet het geval is.


2
2017-08-21 08:49