È corretto introdurre metodi utilizzati solo durante i test unitari?


12

Recentemente stavo TDDing un metodo di fabbrica. Il metodo consisteva nel creare un oggetto semplice o un oggetto avvolto in un decoratore. L'oggetto decorato potrebbe essere di uno dei diversi tipi, tutti estendendo la StrategyClass.

Nel mio test volevo verificare se la classe dell'oggetto restituito è come previsto. È facile quando viene restituito un semplice oggetto, ma cosa fare quando è racchiuso in un decoratore?

Scrivo codice in PHP in modo da poter usare ext/Reflectionper scoprire una classe di oggetti avvolti, ma mi è sembrato di complicare troppo le cose, e in qualche modo di nuovo le regole del TDD.

Invece ho deciso di introdurre getClassName()che restituirebbe il nome della classe dell'oggetto quando chiamato da StrategyClass. Tuttavia, quando viene chiamato dal decoratore, restituisce il valore restituito con lo stesso metodo nell'oggetto decorato.

Qualche codice per renderlo più chiaro:

interface StrategyInterface {
  public function getClassName();
}

abstract class StrategyClass implements StrategyInterface {
  public function getClassName() {
    return \get_class($this);
  }
}

abstract class StrategyDecorator implements StrategyInterface {

  private $decorated;

  public function __construct(StrategyClass $decorated) {
    $this->decorated = $decorated;
  }

  public function getClassName() {
    return $this->decorated->getClassName();
  }

}

E un test PHPUnit

 /**
   * @dataProvider providerForTestGetStrategy
   * @param array $arguments
   * @param string $expected
   */
  public function testGetStrategy($arguments, $expected) {

    $this->assertEquals(
      __NAMESPACE__.'\\'.$expected,  
      $this->object->getStrategy($arguments)->getClassName()
    )
  }

//below there's another test to check if proper decorator is being used

Il mio punto qui è: va bene introdurre tali metodi, che non hanno altro uso che rendere più semplici i test unitari? In qualche modo non mi sembra giusto.


Forse questa domanda fornirà maggiori informazioni sulla tua domanda, programmers.stackexchange.com/questions/231237/… , credo che dipenda dall'uso e se i metodi aiuteranno notevolmente nel test unitario e nel debug di qualsiasi applicazione che stai sviluppando. .
Clement Mark-Aaba

Risposte:


13

Il mio pensiero è no, non dovresti fare nulla solo perché è necessario per la testabilità. Molte decisioni che le persone prendono possono trarre vantaggio dalla testabilità e la testabilità può anche essere il vantaggio principale, ma dovrebbe essere una buona decisione di progettazione per altri meriti. Ciò significa che alcune proprietà desiderate non sono verificabili. Un altro esempio è quando è necessario sapere quanto sia efficiente una routine, ad esempio Hashmap utilizza un intervallo uniformemente distribuito di valori hash: nulla nell'interfaccia esterna sarebbe in grado di dirlo.

Invece di pensare "sto ottenendo la giusta classe di strategia" pensa "la classe che ottengo esegue ciò che questa specifica sta cercando di testare?" È bello quando è possibile testare l'impianto idraulico interno ma non è necessario, basta provare a girare la manopola e vedere se si ottiene acqua calda o fredda.


+1 OP sta descrivendo il test unitario 'clear box', non il test delle funzionalità TDD
Steven A. Lowe,

1
Vedo la poesia prima, anche se sono un po 'riluttante ad aggiungere test sugli algoritmi StrategyClass quando voglio testare se il metodo di fabbrica sta facendo il suo lavoro. Questo tipo di interruzione dell'ISHO di isolamento. Un altro motivo che voglio evitare è che queste particolari classi operano su database, quindi testarle richiede ulteriori beffe / stub.
Mchl

D'altra parte e alla luce di questa domanda: programmers.stackexchange.com/questions/86656/… quando distinguiamo i "test TDD" dai "test unitari", questo diventa perfettamente perfetto (comunque l'impianto idraulico del database è: P)
Mchl

Se aggiungi i metodi, questi diventano parte del contratto con i tuoi utenti. Ti ritroverai con programmatori che chiamano le tue funzioni di test e il ramo in base ai risultati. In generale, preferisco esporre il meno possibile della classe.
BillThor,

5

La mia opinione su questo è: a volte devi rielaborare un po 'il tuo codice sorgente per renderlo più testabile. Non è l'ideale e non dovrebbe essere una scusa per ingombrare l'interfaccia con funzioni che dovrebbero essere utilizzate solo per i test, quindi la moderazione è generalmente la chiave qui. Inoltre, non si desidera trovarsi nella situazione in cui gli utenti del codice utilizzano improvvisamente le funzioni dell'interfaccia di test per le normali interazioni con l'oggetto.

Il mio modo preferito di gestirlo (e devo scusarmi per non poter mostrare come farlo in PHP poiché scrivo principalmente in linguaggi in stile C) è fornire le funzioni di "test" in un modo che non sono esposto al mondo esterno dall'oggetto stesso, ma è possibile accedervi da oggetti derivati. Ai fini del test, desidero quindi derivare una classe che gestisca l'interazione con l'oggetto che voglio testare e che il test unitario utilizzi quel particolare oggetto. Un esempio in C ++ sarebbe simile al seguente:

Classe di produzione:

class ProdObj
{
  ...
  protected:
     virtual bool checkCertainState();
};

Test della classe di supporto:

class TestHelperProdObj : public ProdObj
{
  ...
  public:
     virtual bool checkCertainState() { return ProdObj::checkCertainState(); }
};

In questo modo, almeno sei in una posizione in cui non devi esporre le funzioni di tipo 'test' nel tuo oggetto principale.


Un approccio interessante Avrei bisogno di vedere come avrei potuto adattarlo
Mchl

3

Pochi mesi fa, quando ho inserito la mia lavastoviglie appena acquistata, dal tubo è uscita molta acqua, mi sono reso conto che probabilmente è dovuto al fatto che è stato testato correttamente in fabbrica da cui proviene. Non è raro vedere fori di montaggio e oggetti su macchinari che sono lì - solo - allo scopo di testare in una catena di montaggio.

Il test è importante, se necessario, basta aggiungere qualcosa per questo.

Ma prova alcune delle alternative. La tua opzione basata sulla riflessione non è poi così male. Potresti avere un accesso virtuale protetto a ciò di cui hai bisogno e creare una classe derivata da testare e da affermare. Forse puoi dividere la tua classe e testare direttamente una classe foglia risultante. Anche nascondere il metodo di test con una variabile del compilatore nel codice sorgente è un'opzione (conosco a malapena PHP, non sono sicuro che sia possibile in PHP).

Nel tuo contesto potresti decidere di non testare la composizione corretta all'interno del decoratore, ma testare il comportamento previsto che dovrebbe avere la decorazione. Ciò potrebbe forse focalizzarsi un po 'di più sul comportamento del sistema previsto e non tanto sulle specifiche tecniche (cosa ti offre il modello di decorazione da una prospettiva funzionale?).


1

Sono un principiante assoluto di TDD, ma sembra dipendere dal metodo che viene aggiunto. Da quanto ho capito di TDD, i tuoi test dovrebbero "guidare" la creazione della tua API in una certa misura.

Quando va bene:

  • Finché il metodo non rompe l'incapsulamento e serve uno scopo in linea con la responsabilità dell'oggetto.

Quando non va bene:

  • Se il metodo sembra qualcosa che non sarebbe mai utile o non ha senso in relazione ad altri metodi di interfaccia, potrebbe essere incoerente o confuso. A quel punto, confonderebbe la mia comprensione di quell'oggetto.
  • L'esempio di Jeremy, "... quando devi sapere quanto è efficiente qualche routine, ad esempio la tua Hashmap usa una gamma uniformemente distribuita di valori di hash - nulla nell'interfaccia esterna sarebbe in grado di dirtelo."
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.