Perchè la cosa che ci viene meglio scrivere sono i bachi

Giovanni Battista Lenoci / @gianiaz

PUG SONDRIO - 12 Giugno 2019

Chi sono?

  • Giovanni Battista Lenoci
  • Senior developer @
  • @gianiaz
  • @gianiaz
  • Coordinatore PUG Sondrio

Perchè scrivere test è importante

Ogni mille righe di codice
scriviamo dai 15 ai 50 bachi(*).
Il nostro obbiettivo è portare questo valore a 0. (impossibile, ma ci dobbiamo provare).
*Rif. Code Complete - Steve McConnel

Perchè scrivere test?

Un senior developer alle prese con il codice che ha scritto 2 settimane fa.

Perchè scrivere test?

No, sul serio

  • Perchè non siamo infallibili
  • Perchè individuiamo prima i "code smells"
  • Perchè documentiamo il nostro codice
  • Perchè ci liberiamo dall'ansia
L'espressione code smell (letteralmente "puzza del codice") viene usata per indicare una serie di caratteristiche che il codice sorgente può avere e che sono generalmente riconosciute come probabili indicazioni di un difetto di programmazione.[1] I code smell non sono (e non rivelano) "bug", cioè veri e propri errori, bensì debolezze di progettazione che riducono la qualità del software, a prescindere dall'effettiva correttezza del suo funzionamento. Il code smell spesso è correlato alla presenza di debito tecnico e la sua individuazione è un comune metodo euristico usato dai programmatori come guida per l'attività di refactoring, ovvero l'esecuzione di azioni di ristrutturazione del codice volte a migliorarne la struttura, abbassandone la complessità senza modificarne le funzionalità.

DISCLAIMER

Nell'ambito dei test, la nomenclatura è variegata.

Quali tipi di test esistono?

  • Test Unitari
  • Test Funzionali
  • Test di Integrazione
  • ...e mille altri..
Test di regressione (il baco bastardello che ritorna ogni tanto), Acceptance Test ecc ecc.

Test Unitari

Per unit testing (testing d'unità o testing unitario) si intende l'attività di test di singole unità software.
Per unità si intende normalmente il minimo componente di un programma dotato di funzionamento autonomo (una Classe!)

rif. https://it.wikipedia.org/wiki/Unit_testing

Cosa vuol dire?

Fare un test unitario vuol dire "ignorare" il resto della nostra applicazione.
Il test unitario controlla che il nostro codice faccia esattamente quello che ci aspettiamo a livello LOGICO.

Unit tests tell a developer that the code is doing things right; (*)

"Definizioni"

Stubs

Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.

rif. https://martinfowler.com/articles/mocksArentStubs.html

Gli stub forniscono risposte predefinite alle chiamate effettuate durante il test, di solito non rispondono affatto a nulla al di fuori di ciò che è stato programmato per il test.

Mocks

Mocks are objects pre-programmed with expectations which form a specification of the calls they are expected to receive

rif. https://martinfowler.com/articles/mocksArentStubs.html

A cosa serve fare il mocking di oggetti?

  • Rimpiazzare un comportamento non deterministico (l'ora o la temperatura ambiente).
  • Se l'oggetto ha degli stati difficili da riprodurre (simulare un eccezione).

Es. Ho un metodo di una repository (repositoryPattern) ha come dipendenza un ORM esterno per poter commporre le query. A seconda della struttura del db queste query possono lanciare alcune eccezioni (unique, vincoli di altro genere). Se dobbiamo testare il repository potrebbero esserci casi in cui non riusciamo a riprodurre in modo puntuale una delle possibili eccezioni. In alcuni casi è accettabile fare un catch generico di \Exception. In altri casi abbiamo bisogno di gestire una specifica eccezione per reagire di conseguenza. In questo caso il mocking ci permette di far reagire a determinate chiamate con un eccezione specifica.
  • ConsentsStrategy (strategy pattern definita dalla gang of four "Design Patterns: Elements of Reusable Object-Oriented Software"
  • Domanda1: C'è qualcosa da mockare qui?
  • Domanda2: Secondo voi quanti metodi avrà il nostro test?
  • Domanda3: Su cosa faremo le asserzioni?

2 chiacchiere su un caso semi reale


class ConsentsBagStrategy
{
    /** @var ConsentsStrategy */
    private $consentsStrategy;

    public function __construct(
        ConsentsStrategy $ConsentsStrategy
    ) {
        $this->consentsStrategy = $ConsentsStrategy;
    }

    public function applyStrategy(
        ?ConsentsBag $previousConsentsBag,
        ?ConsentsBag $newConsentsBag): ?ConsentsBag
    {
        if (null === $newConsentsBag) {
            return $previousConsentsBag;
        }
        if (null === $previousConsentsBag) {
            return $newConsentsBag;
        }

        $this->mergeConsents($previousConsentsBag, $newConsentsBag->getConsents());

        return $previousConsentsBag;
    }

    private function mergeConsents(ConsentsBag $consentsBag, array $Consents): void
    {
        $consents = $this->consentsStrategy->merge($consentsBag->getConsents(), $Consents);
        foreach ($consents as $businessUnit => $consent) {
            $consentsBag->setConsent($consent, $businessUnit);
        }
    }
}
                
                

Test Funzionali

I test funzionali non si chiamano in questo modo perchè testano una funzione, bensì perchè testano una funzionalità, una piccola fetta della nostra applicazione che può coinvolgere servizi esterni (il web server o il database ad esempio).

functional tests tell a developer that the code is doing the right things.

Come individuo una funzionalità?

  • Voglio assicurarmi che una pagina web risponda correttamente, testando ad esempio il codice di ritorno http o il contenuto della pagina.
  • Voglio assicurarmi che un repository stia ritornando proprio quello che mi aspetto interrogando il database.
Introdurre concetto di fixtures.

Cosa sono le Fixtures

Le fixtures di test si riferisco ad uno stato prefissato usato come linea di partenza per testare il software.
Leggi: Se voglio testare cosa risponde il mio db in determinate condizioni, devo avere dei dati simulati ben conosciuti per poter sapere sempre da che stato parto.

class ShutdownRepository extends BaseRepository
{
    protected function findNotAccomplishedShutdownRequestsQueryBuilder(): QueryBuilder
    {
        $queryBuilder = $this->createQueryBuilderWithDefaultAlias();
        $this->filterByActive($queryBuilder);
        return $queryBuilder;
    }

    /**
    * @return AbstractShutdown[]
    */
    public function findNotAccomplishedShutdownRequests(): array
    {
        return $this->findNotAccomplishedShutdownRequestsQueryBuilder()
                    ->getQuery()
                    ->getResult();
    }

    /**
    * @throws \Doctrine\ORM\NonUniqueResultException
    */
    public function existsShutDownRequests(): bool
    {
        $queryBuilder = $this->findNotAccomplishedShutdownRequestsQueryBuilder();
        $queryBuilder->select('count(' . $this->getAlias() . '.id)');
        return $queryBuilder->getQuery()->getSingleScalarResult() > 0;
    }

    protected function filterByActive(QueryBuilder $queryBuilder): void
    {
        $queryBuilder->andWhere($this->getAlias() . '.restartRequestedAt IS NULL');
    }
}
                
                

Test integrazione

Il test di integrazione è un tipo di test che verifica l'integrazione tra più sistemi.
Questo tipo di test è molto oneroso, perchè richiede di avere un'ambiente di test che sia il più possibile simile (l'ideale è che sia uguale) all'ambiente di produzione.
Non sempre è necessario avere test di integrazione, e generalmente si testano gli "happy path".

Esempio di sistema che richiede un test di integrazione

Ma cosa vuol dire testare?


Vuol dire scrivere codice PHP

In quale proporzione dobbiamo testare?

Ci sono casi in cui si ha un'applicazione fantastica che però mostra il segno del tempo. All'ennesima richiesta di funzionalità in più si arriva al punto che l'inserimento costa troppo perchè il codice è diventato troppo complesso. La tentazione di riscrivere tutto da zero con il know how acquisito è forte. Però le insidie sono dietro l'angolo e si perdono funzionalità, per scelte di svariato tipo si sceglie la strategia sbagliata e si crea un mostro peggio del precedente, e intanto le vendite calano. Mettere sotto test un sistema già esistente richiede un approccio diverso, quindi in un sistema che sta già girando è perfettamente accettabile in fase di test cominciare dai test funzionali, Si crea una "gabbia" di test funzionali che si assicurino ad esempio che il bottone "compra" dell'ecommerce sia sempre presente sulla pagina di dettaglio di un prodotto, e contestualmente mi assicuro che la pagina risponda sempre ecc.

Come scriviamo i test?

Avvalendoci di librerie che ci facilitano il compito.
Di seguito un elenco degli strumenti che utilizzo quotidianamente per testare:

  • phpunit/phpunit
  • phpspec/prophecy
  • facile-it/paraunit
  • symfony/phpunit-bridge
  • facile-it/symfony-functional-testcase
  • dama/doctrine-test-bundle
  • fzaninotto/faker

  • PHPUNIT : E' il framework che ci mette a disposizione tutte le utilità per poter fare asserzioni di ogni genere, dalle più banali alle più articolate
  • PROPHECY: E' un motore di mocking, anche phpunit ha il suo, ma prophecy rende a mio parere più leggibile un mock e ti permette di usare il tuo mock come se fosse l'oggetto reale.
  • PARAUNIT: I test se fatti per bene diventano molti e cominciano ad avere un tempo di esecuzione lungo, paraunit sfrutta tutti i processori che abbiamo oggi per parallelizzare i test. OCCHIO ALLA CONCORRENZA SU DB!
  • symfony/phpunit-bridge che dire di +?
  • facile-it/symfony-functional-testcase ha sostituito il liipfunctionaltestbundle, da metodi di utilità per facilitare l'utilizzo del container in ambiente di test
  • dama/doctrine-test-bundle simula la scrittura su db velocizzando i test e riportando tutto allo stato originale.
  • fzaninotto/faker

Grazie per l'attenzione!

Contatti

  • gianiaz@gmail.com
  • giovanni.lenoci@facile.it (we are hiring!)
  • @gianiaz
  • @gianiaz

Domande?