Paraunit

Test paralleli, Doctrine e le fixture

By Alessandro Lai / @AlessandroLai

Spoiler alert

Talk in anteprima dal SymfonyDay
e (forse) dal CodeMotion Milano

Mi presento

  • Alessandro Lai
  • PHP developer ...
  • ... da meno di un anno e mezzo

Perché ho cambiato?

Non facevo e volevo provare:

  • Test automatici & TDD
  • Continuous Integration
  • Automatizzazione deploy
  • Etc. ...

I fondamentali

Prima di iniziare mi consigliarono un libro:

PHP Best Practices

“ Una build che impiega un'ora ad essere eseguita non è particolarmente utile [...] ”

Nella pratica il limite è molto più stretto:

10 minuti

Gli inizi...

La nostra build era molto lenta
(fino a 25 minuti!!) per tre motivi combinati:

Vagrant + NFS + Symfony2 (cache)

Docker! (<15 minuti)
Ma servirebbe un altro talk...

Inoltre, la dimensione (e la durata) di una suite di test
può solo aumentare...

Primi tentativi

  • brianium/paratest
    • Va configurato con precisione (batch size)
    • Non risolve problemi di concorrenza
      (solo la globale TEST_TOKEN)
  • liuggio/fastest
    • Semplice lancio di comandi paralleli
    • Diventato tool generico
    • Un processo/database per core
  • jlipps/paraunit (ouch!)
    • Abbandonato da 3 anni (v0.1.5)
    • Anche qui, solo TEST_TOKEN
    • Nessun supporto testsuite

facile-it/paraunit

Features:

  • PHP >= 5.3, PHPUnit >= 4.6
  • Creato con componenti Symfony
  • Supporto testsuite da configurazione XML
  • Parsing e aggregazione dei risultati
  • Gestione dei fatal error
  • Affidabile: controllo degli exit code
  • Utilizzo ottimizzato della memoria

Testsuite unitarie

  • Nessun problema di concorrenza
  • Ottima parallelizzazione
  • Utilizzo risorse 100%
  • Ultime versioni di PHPUnit hanno ottime performance
  • Overhead bootstrap (XDebug)
  • Migliorabile con workers?
    (v. https://dzone.com/articles/parallel-phpunit )

Testsuite funzionali

  • Overhead bootstrap poco importante
  • Utilizzo risorse 100%
  • Niente problemi di memoria
  • Problemi di concorrenza sul DB

Problemi di accesso al database

Principali cause dei problemi

  • Fixtures di dimensione ridotta
  • Test che usano sempre le stesse fixtures
  • Letture seguite da scritture: deadlocks
  • Alterazione dei dati

facile-it/paraunit-testcase

Per test funzionali di Symfony2 + Doctrine

Il trucco?

Accesso transazionale al database

Unico limite: funziona solo con l'EntityManager

setUp()


    public function setUp()
    {
        parent::setUp();

        $this
            ->getEM()
            ->getConnection()
            ->setTransactionIsolation(Connection::TRANSACTION_READ_COMMITTED);

        $this->getEM()->beginTransaction();
    }
                        

tearDown()


    public function tearDown()
    {
        parent::tearDown();

        if ($this->getEm()) {
            $this->getEm()->rollback();

            $conn = $this->getEm()->getConnection();
            $conn->close();
        }
    }
                        

Vantaggi

  • Massimo isolamento del database
  • Possibilità cambiare/creare fixtures al volo
  • Il database di test rimane sempre pulito
  • Impossibile avere test interdipendenti

Ancora fallimenti??

Si verificavano ancora fallimenti casuali...

Le cause erano due:

  • I deadlock possono sempre succedere
  • I controller non mantengono la transazione
    tra una chiamata e l'altra

Risolvere definitivamente i deadlock

Il parser di Paraunit sa individuare i fallimenti per deadlock


    SQLSTATE[HY000]:
        General error: 1205 Lock wait timeout exceeded;
        try restarting transaction
                        


Quando li individua,
riporta il test tra quelli
ancora non eseguiti,
e stampa una "A" (attempt)

Client HTTP di test

Per risolvere il problema dei controller,
abbiamo fatto 2 modifiche al TestCase:

  • Avviamo il kernel una volta sola nel costruttore
  • Passiamo la connessione all'EntityManager manualmente:

protected function doRequest($request)
{
    $em = $this->getContainer()->get('doctrine.orm.entity_manager');
    $newEm = $em->create(
                $em->getConnection(),
                $em->getConfiguration(),
                $em->getEventManager()
            );
    $this->getContainer()->set('doctrine.orm.entity_manager', $newEm);

    return $this->kernel->handle($request);
}
                        

Comparazione

Suite funzionale SharkDev: 261 classi, 1568 metodi

Live demo!

Nuova funzionalità: coverage

  • Feature ancora in sviluppo (milestone 0.5)
  • Sfrutta l'opzione --coverage-php di PHPUnit
  • Esecuzione parallelizzata
  • Merge successivo dei risultati

Prossimi sviluppi

  • Migliorare feature coverage
    • Migliorare strategia di merging
    • Profiling per trovare colli di bottiglia
  • Prioritizzazione test lenti
  • Altre opzioni di selezione test
  • Evitare overhead bootstrap
  • Supportare altri framework/ORM
  • NoSQL?
  • Milestone v1.0: PHP >= 5.6, PHPUnit >= 5.0

Contatti

In Facile.it...

Fine!

Grazie per l'attenzione!

Domande?