Fino a che punto devo andare con l'iniezione delle dipendenze e il mocking?

6

Dire che ho una classe seguente:

public class A {

  public void execute() {
    if (something) 
      ThirdPartyApi.method();   
  }
}

Ora vorrei testare in particolare il metodo execute() . Voglio assicurare che tutti i percorsi sono coperti e funzionano come previsto. Tuttavia, il problema che vorrei eseguire qui non è in grado di prendere in giro il ThirdPartyApi perché non è fornito come dipendenza. Una facile soluzione a questo è di fornire ThirdPartyApi come una dipendenza come questa.

public class A {

  private ThirdPartyApi api;

  public A(ThirdPartyApi api) {
    this.api = api;
  }

  public void execute() {
    if (something) 
      api.method();   
  }
}

Ora vorrei ottenere la funzionalità che voglio e tutti sarebbero felici. Tuttavia, ciò che non mi piace qui è il fatto che ho bisogno di fornire tutte le dipendenze interne attraverso un metodo costruttore / set per rendere la mia classe completamente testabile e indipendente da qualsiasi dipendenza esterna (API Android, librerie, utility, ecc.) .

Questo diventa un problema per le dipendenze che non sono direttamente necessarie per realizzare il caso d'uso della classe. Si tratta di un semplice strumento, ad esempio lo strumento di conversione delle unità. In questo modo, chiedo all'utente di questa classe di fornirmi una dipendenza che posso creare internamente per conto mio. Rende l'API più difficile da usare, perché l'utente deve sapere di più sulla classe di quanto dovrebbe / necessario.

Per rendere l'esempio più specifico, dì che ThidPartyApi è un'enorme libreria di conversione di unità esterne. Perché un utente dovrebbe fornirmi una classe da questa libreria attraverso un costruttore? È qualcosa che è un semplice strumento secondario utilizzato per realizzare il caso d'uso. Non è direttamente correlato al caso d'uso stesso. Richiedere questa dipendenza in un costruttore renderebbe l'API più confusa. Vedi la seguente lezione:

public class Storage {

  private ConversionTool api;

  public Storage (ConversionTool api) {
    this.api = api;
  }

  public void storeInteger(int a) {
    storeString(api.convertToString(a));
  }
}

vs

public class Storage {

  private ConversionTool api;

  public Storage () {
    this.api = new ConversionTool();
  }

  public void storeInteger(int a) {
    storeString(api.convertToString(a));
  }
}

Quale sarebbe l'approccio preferito? Fornire dipendenze secondarie tramite il costruttore rende più verificabile e controllabile, ma l'API perde il suo significato. Non fornire dipendenze secondarie attraverso il costruttore mantiene il significato in una classe, ma perdo la capacità di prendere in giro tutto all'interno della classe e testare completamente la classe.

    
posta aarnaut 13.08.2018 - 12:09
fonte

5 risposte

4

Why would a user need to provide me a class from this library through a constructor? It's something that's a mere side tool used to accomplish the use case.

IMHO questa non è la domanda veramente importante. Se ThirdPartyApi è uno "strumento secondario" non ha importanza. Invece, consiglierei di chiedere

  • non prende in giro ThirdPartyApi ti consente di scrivere ancora test semplici, stabili e veloci?

Se la risposta è "sì", non è necessario inserire ThirdPartyApi nella classe (almeno non per il test), se la risposta è "no", DI sarà l'approccio migliore.

Il tuo esempio di "libreria di conversione" sembra uno in cui la risposta potrebbe essere "sì", proprio come in genere non avresti dovuto iniziare a simulare le funzioni di conversione incorporate del tuo linguaggio di programmazione o della libreria standard. Ma se la costruzione di un oggetto ThirdPartyApi richiede qualcosa come una connessione al database attraverso la rete, poiché diverse funzioni di conversione richiedono dati da un database, quindi l'iniezione potrebbe essere l'approccio migliore.

Si noti inoltre che potrebbero esserci altri motivi per rendere iniezioni di ThirdPartyApi rispetto ai test di justs. Ad esempio, per evitare troppi accoppiamenti specifici del fornitore o perché desideri avere "strategie di conversione" differenti.

DI non è fine a se stesso, è un mezzo per finire. Usalo quando ne hai bisogno, non "just in case".

    
risposta data 13.08.2018 - 13:39
fonte
4

How far should I go with dependency injection and mocking?

La risposta volutamente vaga è "per quanto è necessario per produrre un buon design".

Nel 1972 , Parna ha descritto un principio interessante per il design del modulo

at one begins with a list of difficult design decisions or design decisions which are likely to change. Each module is then designed to hide such a decision from the others.

Un'implementazione di terze parti che vogliamo elidere durante i test è un buon esempio di una decisione che è "probabile che cambi".

Introducendo la capacità di fornire un'implementazione specifica non necessariamente significa che le affordances API per composizione root deve esporre complessità aggiuntiva

public Storage () {
    this(new ConversionTool());
}

Storage (ConversionTool api) {
    this.api = api;
}

Questo tipo di approccio introduce percorsi di codice non testati, ma il percorso non testato soddisfa questo importante vincolo: sono "così semplici che ovviamente non ci sono carenze".

La domanda correlata è "vogliamo elidere questa libreria di terze parti durante i test?" Ci sono proprietà che vogliamo che i nostri test abbiano - vogliamo essere in grado di eseguirli spesso, quindi rapidamente, quindi in parallelo, e senza effetti collaterali lenti. Se la libreria di terze parti rende queste proprietà costose o impossibili, ovviamente vogliamo essere in grado di sostituire la libreria con un doppio di prova.

Quando la libreria di terze parti è testabile ... potremmo comunque sostituirla. Rainsberger lo spiega nel suo talk i test integrati sono una truffa . TL; DR: puoi testare il tuo design in modo più efficiente se non devi considerare le varianti del percorso del codice nel codice di terze parti.

    
risposta data 13.08.2018 - 14:05
fonte
3

So che arrivo tardi a questa festa ma ho qualcosa da dire:

Smetti di usare TDD come unica considerazione nel tuo progetto!

Non è mai stato pensato per questo. È un terribile fraintendimento di quello che dicevano i sostenitori del TDD quando sostenevano che la riprogettazione per rendere testabile il codice migliorasse il suo design. Lo fa. Se non lo rovini.

Sì, è possibile girare altrimenti in modo semplice per costruire grafici di oggetti in un incubo da costruire. Ma non è colpa di TDD. Ci sono alcuni semplici principi che attenuano questa complessità.

Convenzione sulla configurazione è una soluzione molto semplice a questo problema. Fondamentalmente dice che va bene per i valori predefiniti del codice duro, purché siano sovrascrivibili. Questo consiglio è gloriosamente semplice da seguire in linguaggi che hanno parametri con nome e quindi argomenti predefiniti. Se sei bloccato senza di loro, come in Java, finisci per doverli simulare con il modello di build di Joshua Blochs se essere immutabile è importante per te. Se mutabile va bene allora i setter risolvono questo facilmente. Se sei in C # o Python o simili hai già chiamato i parametri, quindi smetti di inventare scuse.

Un'altra soluzione è la costruzione astratta con le fabbriche. Fare tutto direttamente in main può diventare poco maneggevole. Finché riesci a pensare ad un buon nome, puoi impacchettare una fabbrica con valori hardcoded che semplificano la costruzione pur non imponendo che questo è l'unico modo in cui la costruzione può accadere. Mi piace questo metodo perché i costruttori lunghi sono molto meno dolorosi se non si devono mai toccarli.

Se stai pensando che questo sia molto utile solo per i test hai assolutamente ragione. Non lo faccio per i test. Lo faccio perché non voglio mantenere il mio diritto di modificare la mia API di terze parti ogni volta che accidenti, per favore.

Per farlo devo essere sicuro di non lasciare che i test prendano in giro la libreria. Devo esprimere attentamente solo ciò che i miei oggetti di dominio hanno veramente bisogno di loro. Niente di più. ThirdPartyLibrary potrebbe avere 35 metodi utili che se ne stanno appesi, ma se ho bisogno di due ho intenzione di chiarire che ho bisogno di questi due.

Perché? Perché se in seguito ThirdPartyLibrary vuole improvvisamente cambiare il suo accordo di licenza su di me, io non verrò tenuto in ostaggio. Ho intenzione di scrivere due stupidi metodi e andare avanti con la mia vita.

Se lasci che le librerie si infiltrino nel tuo spazio linguistico così tanto che non puoi fare pubblicità per un lavoro che lavora nella tua base di codice senza menzionare la libreria. Tienilo a mente quando decidi come usarlo.

    
risposta data 08.12.2018 - 20:49
fonte
2

Una buona regola empirica è che si dovrebbe evitare di prendere in giro le pure funzioni , es. quelli le cui uscite dipendono esclusivamente dagli input. Non c'è bisogno di prendere in giro una libreria di conversione di unità per lo stesso motivo per cui non prendiamo in giro la libreria matematica standard o le routine di elaborazione delle stringhe.

Al contrario, c'è una buona ragione per prendere in giro l'I / O del disco, il traffico di rete, le query sui database, ecc .: dipendono dal mondo esterno e hanno un sacco di stato mutabile. Non è garantito che la stessa sequenza di chiamate produca gli stessi risultati (un altro programma potrebbe sovrascrivere il file, l'utente potrebbe abilitare la modalità aereo, ecc.) / Consente inoltre di testare la robustezza in condizioni rare, come guasti intermittenti.

Tuttavia, una ragione per prendere in giro una pura funzione sarebbe la prestazione. Ad esempio, se stai testando un gioco, potrebbe essere utile prendere in giro il path-path in modo da dedicare meno tempo all'esecuzione dell'algoritmo A * quando non viene testato direttamente.

    
risposta data 09.12.2018 - 04:26
fonte
0

L'approccio preferito è generalmente quello di creare un'interfaccia per "qualcosa da convertire", aggiungerla come dipendenza e fornire un'implementazione che usi la libreria di terze parti. Ciò ti consente flessibilità nel caso in cui smettano di esistere, ti addebitino troppo o altrimenti diventino impraticabili.

O a seconda di dove viene utilizzato, ( Storage qui) potresti considerare quella classe come l'implementazione specifica della tua libreria di terze parti. Ma allora non puoi testare l'unità. A volte va bene perché la classe non sta facendo molto.

In generale, però, vuoi disaccoppiare le cose in modo tale che la lib di terze parti tocchi solo una classe che hai bisogno di test di integrazione.

    
risposta data 13.08.2018 - 13:41
fonte

Leggi altre domande sui tag