Come riconciliare "non prendere in giro ciò che non possiedi" con "aspettative" nei test unitari?

5

Sto assemblando una serie di modelli da utilizzare in un'applicazione Zend Framework 2. Quindi, ogni modello ha una classe di tabella che funge da interfaccia tra il modello e il database per l'interrogazione. Utilizzando lo ZF2% diTableGateway significa che anche una query abbastanza semplice, come ottenere il numero di risultati da un log di pagina tra due date, finisce con alcune chiamate complesse:

public function getDailyHits($start, $end)
{
    $select = $this->tableGateway->getSql()->select();

    $select->columns(array(
            "date" => new Expression("DATE_FORMAT(timestamp, '%Y-%m-%d')"),
            "hits" => new Expression("COUNT(timestamp)"),
        ));

    $select->where->between("timestamp", $start, $end);

    $select->group("date");

    $results = $this->tableGateway->selectWith($select);



    $dailyHits = array();
    foreach ($results as $result) {
        $dailyHits[$result->date] = $result->hits;
    }

    return $dailyHits;
}

Quando si è trattato di sviluppare il test unitario, ho scritto un sacco di aspettative:

public function testGetDailySearches()
{
    // Setup
    $result = new \stdClass();
    $result->date = "2015-01-01";
    $result->searches = 42;

    $resultSet = array($result);

    $sql = $this->getMockBuilder("Zend\Db\Sql\Sql")
        ->disableOriginalConstructor()
        ->getMock();

    $select = $this->getMockBuilder("Zend\Db\Sql\Select")
        ->disableOriginalConstructor()
        ->getMock();

    $where = $this->getMockBuilder("Zend\Db\Sql\Where")
        ->disableOriginalConstructor()
        ->getMock();

    $tableGateway = $this->getMockBuilder("Zend\Db\TableGateway\TableGateway")
        ->disableOriginalConstructor()
        ->getMock();

    $searchLogTable = $this->getMockBuilder("Usage\Model\Table\SearchLog")
        ->setConstructorArgs(array($tableGateway))
        ->setMethods(null)
        ->getMock();



    // Expectations
    $tableGateway->expects($this->once())
        ->method("getSql")
        ->will($this->returnValue($sql));

    $tableGateway->expects($this->once())
        ->method("selectWith")
        ->with($this->equalTo($select))
        ->will($this->returnValue($resultSet));



    $sql->expects($this->once())
        ->method("select")
        ->will($this->returnValue($select));

    $select->expects($this->once())
        ->method("columns")
        ->will($this->returnValue($select)); // TODO: need to assert parameters

    $select->expects($this->once())
        ->method("__get")
        ->with($this->equalTo("where"))
        ->will($this->returnValue($where));

    $where->expects($this->once())
        ->method("between")
        ->with(
            $this->equalTo("time"),
            $this->equalTo("2015-10-01 00:00:00"),
            $this->equalTo("2015-10-07 23:59:59")
        )
        ->will($this->returnValue($select));

    $select->expects($this->once())
        ->method("group")
        ->with($this->equalTo("date"));



    // Assertions
    $this->assertEquals(
        array("2015-01-01" => 42),
        $searchLogTable->getDailySearches("2015-10-01 00:00:00", "2015-10-07 23:59:59")
    );
}

Ho provato a usare Prophecy per ridurre la verbosità in questi test, ma sono stato inciampato con loro mag% __get metodo necessario per leggere la proprietà $ dove di Select oggetto.

Dopo aver letto un paio di thread sul sito di Prophecy, ho scoperto un link a > That not Notours" . Ha molto senso. Inoltre, in alcuni test ridurrebbe un sacco di file standard se eseguo la sottoclasse di TableGateway (o forse anche solo la derisione) in modo che tutto il comportamento di Zend \ Db venga mantenuto.

Ma allora cosa succede alle mie aspettative? È un caso di test eccessivamente ingegnerizzati? Devo avere queste aspettative, o dovrei fare affidamento sul test di una scatola per lo più nera di un metodo?

Se rimuovevo quelle aspettative, e facevo affidamento su un risultato deriso da selectWith sottoclasse / deriso, potevo scrivere un test di passaggio che non specificava nessuno dei criteri di selezione, e non funzionava come richiesto nel reale mondo.

    
posta HorusKol 03.11.2015 - 06:58
fonte

3 risposte

5

Il punto del test dovrebbe essere quello di verificare se getDailyHits restituisce i risultati attesi. Non lo sta facendo adesso. È un test privo di significato che controlla semplicemente che l'implementazione faccia un sacco di chiamate, invece di testare il comportamento stesso. Quando per qualche ragione l'implementazione o la struttura del database cambia, non dovresti modificare questo test, che è quello che farai adesso.

Questo è un tipo di test in cui verrei effettivamente eseguito contro il database stesso. Secondo alcuni standard questo non lo renderebbe un 'unittest', ma ha molto senso testarlo contro il database.

Pensa in questo modo: se tu potessi, scriveresti un pezzo di codice per verificare che l'SQL generato da questa dichiarazione corrisponda ad un SQL predefinito che pensi possa funzionare? Oppure preferiresti testare che il risultato di quella query corrisponda a quanto ti aspetti e che non manchi di dati o restituisca cose che non dovrebbero?

    
risposta data 03.11.2015 - 08:55
fonte
4

L'idea alla base del consiglio che citi è quella in cui hai un pezzo di codice che:

  • interagisce con alcuni servizi esterni (ad es. il tuo database) e
  • contiene anche la logica dell'applicazione (ad esempio i dati dei processi recuperati dal database)

è meglio dividere il codice in due, perché queste due diverse responsabilità devono essere testate in diversi modi:

  • crei un modulo che presenta l'interfaccia più semplice possibile al tuo codice e che incapsula tutta la complessità dell'interazione con il servizio esterno: questo modulo è stato testato con test di integrazione

  • quindi sviluppi un modulo con la tua logica applicativa che utilizza il modulo di interfaccia semplificato. Questo modulo semplificato dovrebbe essere più facile da sostituire con una simulazione, ed è più probabile che tu comprenda pienamente il suo comportamento, quindi è meno probabile che tu scriva un mock che si comporta in modo errato. È quindi possibile testare la logica complessa più facilmente utilizzando test unitari più semplici.

Nota che il tuo modulo di interfaccia non dovrebbe sottoclasse una classe esterna - per trarre il massimo vantaggio dal principio, dovresti includere tutta la complessità della sua interfaccia come assolutamente necessario. Preferisci l'aggregazione sull'ereditarietà è la massima da applicare qui.

    
risposta data 03.11.2015 - 10:56
fonte
2

La raccomandazione in "Non scherzare su ciò che non possiedi" è racchiudere un componente di terze parti in un tuo oggetto. L'hai già fatto . Qualunque oggetto ha il metodo getDailyHits() è già un wrapper attorno a TableGateway . Ed è un buon wrapper poiché descrive il suo contratto non in termini di brutto livello basso dal mondo ZF2, ma in termini che hanno senso nel tuo sistema ( getDailyHits , start , end ).

Tutto ciò che devi fare è dargli un'interfaccia e cambiare il modo di testare:

  • Provare l'oggetto wrapper concreto (chiamiamolo ZF2DAO, per l'oggetto di accesso ai dati) in un test di integrazione contro un TableGateway reale, se possibile. Basta testare percorsi felici. Questi test non sono qui per smantellare i casi d'angolo, solo per verificare che tutto sia impostato in modo ordinato. Puoi spostarli in una suite di test separata se sono troppo lenti.

  • Durante il test degli oggetti che collaborano con il DAO, prendilo in giro. Ne possiedi questo.

risposta data 03.11.2015 - 17:39
fonte

Leggi altre domande sui tag