Dovremmo progettare il nostro codice fin dall'inizio per abilitare i test unitari?


91

Al momento c'è un dibattito nel nostro team sul fatto che modificare la progettazione del codice per consentire il testing di unità sia un odore di codice o fino a che punto può essere fatto senza essere un odore di codice. Ciò è avvenuto perché abbiamo appena iniziato a mettere in atto pratiche che sono presenti in quasi tutte le altre società di sviluppo software.

In particolare, avremo un servizio API Web che sarà molto sottile. La sua principale responsabilità sarà il marshalling di richieste / risposte Web e la chiamata di un'API sottostante che contenga la logica aziendale.

Un esempio è che intendiamo creare una fabbrica che restituirà un tipo di metodo di autenticazione. Non abbiamo bisogno che erediti un'interfaccia in quanto non prevediamo che sia mai qualcosa di diverso dal tipo concreto che sarà. Tuttavia, per testare l'unità del servizio API Web dovremo prendere in giro questa fabbrica.

Ciò significa essenzialmente che progettiamo la classe di controller API Web per accettare DI (tramite il suo costruttore o setter), il che significa che stiamo progettando parte del controller solo per consentire DI e implementare un'interfaccia che altrimenti non ci serve, o che usiamo un framework di terze parti come Ninject per evitare di dover progettare il controller in questo modo, ma dovremo comunque creare un'interfaccia.

Alcuni membri del team sembrano riluttanti a progettare il codice solo per motivi di test. Mi sembra che ci debba essere qualche compromesso se speri di testare l'unità, ma non sono sicuro di come alleggerire le loro preoccupazioni.

Giusto per essere chiari, questo è un progetto nuovo di zecca, quindi non si tratta in realtà di modificare il codice per abilitare i test unitari; si tratta di progettare il codice che scriveremo per essere testabile in unità.


33
Consentitemi di ripeterlo: voi colleghi volete test unitari per il nuovo codice, ma si rifiutano di scrivere il codice in un modo che sia testabile dall'unità, sebbene non vi sia alcun rischio nel rompere qualcosa esistente? Se questo è vero, dovresti accettare la risposta di KilianFoth e chiedergli di evidenziare la prima frase nella sua risposta in grassetto! Apparentemente i tuoi colleghi hanno un grosso malinteso su quale sia il loro lavoro.
Doc Brown,

20
@Lee: chi dice che il disaccoppiamento sia sempre una buona idea? Hai mai visto una base di codice in cui tutto viene passato come un'interfaccia creata da una fabbrica di interfacce usando qualche interfaccia di configurazione? Io ho; è stato scritto in Java, ed è stato un disastro completo, non mantenibile, con errori. Il disaccoppiamento estremo è l'offuscamento del codice.
Christian Hackl,

8
Lavorare efficacemente con il codice legacy di Michael Feathers si occupa molto bene di questo problema e dovrebbe darti una buona idea dei vantaggi del test anche in una nuova base di codice.
l0b0

8
@ l0b0 È praticamente la bibbia per questo. Su stackexchange non sarebbe una risposta alla domanda, ma in RL direi a OP di leggere questo libro (almeno in parte). OP, ottenere lavorando in modo efficace con il codice legacy e lo lesse, almeno in parte (o dire al vostro capo per farlo). Affronta domande come queste. Soprattutto se non hai fatto i test e ora ti stai impegnando, potresti avere 20 anni di esperienza, ma ora farai cose con cui non hai esperienza . È molto più facile da leggere su di loro che apprendere scrupolosamente tutto da prove ed errori.
R. Schmitz,

4
Grazie per la raccomandazione del libro di Michael Feathers, ne prenderò sicuramente una copia.
Lee

Risposte:


204

La riluttanza a modificare il codice per motivi di test mostra che uno sviluppatore non ha capito il ruolo dei test e, di conseguenza, il proprio ruolo nell'organizzazione.

Il business del software ruota attorno alla fornitura di una base di codice che crea valore aziendale. Abbiamo scoperto, attraverso una lunga e amara esperienza, che non possiamo creare basi di codice di dimensioni non banali senza test. Pertanto, le suite di test sono parte integrante del business.

Molti programmatori prestano servizio a questo principio ma inconsciamente non lo accettano mai. È facile capire perché questo sia; la consapevolezza che la nostra capacità mentale non è infinita, ed è di fatto sorprendentemente limitata di fronte all'enorme complessità di una base di codice moderna, è sgradita e facilmente soppressa o razionalizzata. Il fatto che il codice di prova non venga consegnato al cliente rende facile credere che sia un cittadino di seconda classe e non essenziale rispetto al codice aziendale "essenziale". E l'idea di aggiungere un codice di prova al codice aziendale sembra doppiamente offensiva per molti.

Il problema di giustificare questa pratica ha a che fare con il fatto che l'intero quadro di come viene creato il valore in un'impresa di software è spesso compreso solo dai vertici della gerarchia aziendale, ma queste persone non hanno la comprensione tecnica dettagliata di il flusso di lavoro di codifica necessario per capire perché non è possibile eliminare i test. Pertanto sono troppo spesso pacificati dai professionisti che assicurano loro che i test possono essere una buona idea in generale, ma "siamo programmatori d'élite che non hanno bisogno di stampelle del genere", o che "non abbiamo tempo per quello in questo momento", ecc. ecc. Il fatto che il successo aziendale sia un gioco di numeri e che eviti il ​​debito tecnico, assicurando la qualità ecc. mostra il suo valore solo a lungo termine significa che spesso sono abbastanza sinceri in quella convinzione.

Per farla breve: rendere il codice testabile è una parte essenziale del processo di sviluppo, non diverso da quello di altri campi (molti microchip sono progettati con una parte sostanziale di elementi solo a scopo di test), ma è molto facile trascurare le ottime ragioni per quello. Non cadere in quella trappola.


39
Direi che dipende dal tipo di cambiamento. C'è una differenza tra rendere il codice più facile da testare e introdurre hook specifici per i test che NON devono MAI essere utilizzati in produzione. Personalmente diffido di quest'ultimo, perché Murphy ...
Matthieu M.

61
I test unitari spesso interrompono l'incapsulamento e rendono il codice in esame più complesso di quanto sarebbe altrimenti necessario (ad esempio introducendo tipi di interfaccia aggiuntivi o aggiungendo flag). Come sempre nell'ingegneria del software, ogni buona pratica e ogni buona regola ha la sua parte di responsabilità. Produrre ciecamente molti test unitari può avere un effetto dannoso sul valore aziendale, per non parlare del fatto che scrivere e mantenere i test costa già tempo e fatica. Nella mia esperienza, i test di integrazione hanno un ROI molto maggiore e tendono a migliorare le architetture software con meno compromessi.
Christian Hackl,

20
@Vedi Certo, ma devi considerare se avere un tipo specifico di test giustifica l'aumento della complessità del codice. La mia esperienza personale è che i test unitari sono un ottimo strumento fino al punto in cui richiedono cambiamenti di progettazione fondamentali per far fronte al derisione. È qui che passo a un diverso tipo di test. Scrivere unit test a spese di rendere l'architettura sostanzialmente più complessa, al solo scopo di avere unit test, è guardare l'ombelico.
Konrad Rudolph,

21
@ChristianHackl perché un test unitario rompe l'incapsulamento? Ho scoperto che per il codice su cui ho lavorato, se si avverte la necessità di aggiungere funzionalità extra per abilitare i test, il vero problema è che la funzione che si desidera testare necessita di refactoring, quindi tutte le funzionalità sono uguali livello di astrazione (sono le differenze nel livello di astrazione che di solito creano questa "necessità" di codice extra), con il codice di livello inferiore spostato nelle proprie funzioni (verificabili).
Baldrickk,

29
@ChristianHackl I test unitari non devono mai interrompere l'incapsulamento, se si sta tentando di accedere a variabili private, protette o locali da un unit test, lo si sta facendo male. Se stai testando la funzionalità foo, stai testando solo se ha effettivamente funzionato, non se la variabile locale x è la radice quadrata dell'input y nella terza iterazione del secondo ciclo. Se alcune funzionalità sono private, allora così sia, le testerai comunque in modo transitorio. se è davvero grande e privato? Questo è un difetto di progettazione, ma probabilmente non è nemmeno possibile al di fuori di C e C ++ con la separazione dell'implementazione dell'intestazione.
opa

75

Non è così semplice come potresti pensare. Analizziamolo.

  • Scrivere unit test è sicuramente una buona cosa.

MA!

  • Qualsiasi modifica al tuo codice può introdurre un bug. Quindi cambiare il codice senza una buona ragione commerciale non è una buona idea.

  • Il tuo webapi "molto sottile" non sembra il caso migliore per i test unitari.

  • Cambiare codice e test allo stesso tempo è una cosa negativa.

Suggerirei il seguente approccio:

  1. Scrivi test di integrazione . Ciò non dovrebbe richiedere alcuna modifica del codice. Ti fornirà i tuoi casi di test di base e ti consentirà di verificare che eventuali ulteriori modifiche al codice apportate non introducano alcun bug.

  2. Assicurarsi che il nuovo codice sia testabile e che abbia test di unità e integrazione.

  3. Assicurati che la tua catena CI esegua test dopo build e distribuzioni.

Quando hai impostato queste cose, solo allora inizia a pensare al refactoring dei progetti legacy per la testabilità.

Si spera che tutti abbiano imparato le lezioni dal processo e abbiano una buona idea di dove i test sono maggiormente necessari, come si desidera strutturarlo e il valore che apporta al business.

EDIT : Da quando ho scritto questa risposta, l'OP ha chiarito la domanda per dimostrare che stanno parlando di nuovo codice, non di modifiche al codice esistente. Forse ho pensato ingenuamente "Il test unitario è buono?" l'argomento è stato risolto alcuni anni fa.

È difficile immaginare quali modifiche al codice sarebbero richieste dai test unitari ma non essere una buona pratica generale che si vorrebbe in ogni caso. Probabilmente sarebbe saggio esaminare le vere obiezioni, forse è lo stile di unit testing a cui si sta obiettando.


12
Questa è una risposta molto migliore di quella accettata. Lo squilibrio nei voti è sconcertante.
Konrad Rudolph,

4
@Lee Un test unitario dovrebbe testare un'unità di funzionalità , che può o meno corrispondere a una classe. Un'unità di funzionalità dovrebbe essere testata alla sua interfaccia (che potrebbe essere l'API in questo caso). I test possono evidenziare gli odori del design e la necessità di applicare livelli diversi / più di livellamento. Costruisci i tuoi sistemi da piccoli pezzi componibili, saranno più facili da ragionare e testare.
Wes Toleman,

2
@KonradRudolph: Immagino che il punto in cui l'OP abbia aggiunto che questa domanda riguardasse la progettazione di un nuovo codice, non la modifica di uno esistente, mi sia sfuggito. Quindi non c'è nulla da rompere, il che rende la maggior parte di questa risposta non applicabile.
Doc Brown,

1
Non sono assolutamente d'accordo con l'affermazione secondo cui scrivere unit test è sempre una buona cosa. I test unitari sono buoni solo in alcuni casi. È sciocco usare unit test per testare il codice frontend (UI), sono fatti per testare la logica aziendale. Inoltre, è utile scrivere unit test per sostituire i controlli di compilazione mancanti (ad es. In Javascript). La maggior parte del codice solo frontend dovrebbe scrivere esclusivamente test end-to-end, non test unitari.
Sulthan,

1
I disegni possono sicuramente risentire del "test dei danni indotti". Di solito la testabilità migliora il design: durante la scrittura dei test si nota che qualcosa non può essere recuperato ma deve essere passato, rendendo le interfacce più chiare e così via. Ma a volte ti imbatti in qualcosa che richiede un design scomodo solo per i test. Un esempio potrebbe essere un costruttore di soli test richiesto nel nuovo codice a causa del codice di terze parti esistente che utilizza ad esempio un singleton. Quando ciò accade: fai un passo indietro e fai solo un test di integrazione, invece di danneggiare il tuo design in nome della testabilità.
Anders Forsgren,

18

Progettare il codice per essere intrinsecamente testabile non è un odore di codice; al contrario, è il segno di un buon design. Esistono diversi modelli di progettazione noti e ampiamente utilizzati basati su questo (ad esempio Model-View-Presenter) che offrono test facili (più facili) come un grande vantaggio.

Quindi, se hai bisogno di scrivere un'interfaccia per la tua classe concreta per testarla più facilmente, questa è una buona cosa. Se hai già la classe concreta, la maggior parte degli IDE può estrarre un'interfaccia da essa, riducendo al minimo lo sforzo richiesto. È un po 'più di lavoro per mantenere i due in sincronia, ma un'interfaccia non dovrebbe cambiare molto in ogni caso, e i benefici dei test possono superare quello sforzo extra.

D'altra parte, come @MatthieuM. menzionato in un commento, se si aggiungono punti di ingresso specifici nel codice che non dovrebbero mai essere utilizzati in produzione, solo a scopo di test, ciò potrebbe costituire un problema.


Tale problema può essere risolto tramite l'analisi statica del codice: contrassegnare i metodi (ad es. Devono essere nominati _ForTest) e controllare la base di codice per le chiamate da codice non di prova.
Riking

13

È molto semplice capire che per la creazione di unit test il codice da testare deve avere almeno determinate proprietà. Ad esempio, se il codice non è costituito da singole unità che possono essere testate separatamente, la parola "unit testing" non ha nemmeno senso. Se il codice non ha queste proprietà, è necessario prima modificarlo, è abbastanza ovvio.

Ha detto che, in teoria, si può provare a scrivere prima qualche unità di codice verificabile, applicando tutti i principi SOLIDI, e quindi provare a scrivere un test per essa in seguito, senza modificare ulteriormente il codice originale. Sfortunatamente, scrivere un codice che è realmente testabile dall'unità non è sempre completamente semplice, quindi è molto probabile che ci siano alcune modifiche necessarie che si rileveranno solo quando si tenta di creare i test. Questo è vero per il codice anche quando è stato scritto con l'idea di unit test in mente, ed è sicuramente più vero per il codice che è stato scritto in cui "testabilità dell'unità" non era all'ordine del giorno all'inizio.

Esiste un approccio ben noto che cerca di risolvere il problema scrivendo prima i test unitari - si chiama Test Driven Development (TDD) e può sicuramente aiutare a rendere il codice più unitamente testabile fin dall'inizio.

Ovviamente, la riluttanza a cambiare il codice in seguito per renderlo testabile sorge spesso in una situazione in cui il codice è stato testato manualmente per primo e / o funziona bene nella produzione, quindi cambiarlo potrebbe effettivamente introdurre nuovi bug, è vero. L'approccio migliore per mitigarlo è quello di creare prima una suite di test di regressione (che spesso può essere implementata con solo minime modifiche alla base di codice), così come altre misure di accompagnamento come revisioni del codice o nuove sessioni di test manuali. Che dovresti dare abbastanza fiducia per assicurarti che la riprogettazione di alcuni interni non rompa nulla di importante.


Interessante che tu menzioni TDD. Stiamo cercando di introdurre BDD / TDD, che ha anche incontrato una certa resistenza, vale a dire cosa significa "il codice minimo da passare".
Lee

2
@Lee: portare cambiamenti in un'organizzazione provoca sempre un po 'di resistenza, e ha sempre bisogno di un po' di tempo per adattare nuove cose, questa non è nuova saggezza. Questo è un problema di persone.
Doc Brown,

Assolutamente. Vorrei solo che ci fosse stato concesso più tempo!
Lee

Spesso si tratta di mostrare alle persone che farlo in questo modo farà risparmiare loro tempo (e, si spera, anche rapidamente). Perché fare qualcosa che non ti gioverà?
Thorbjørn Ravn Andersen,

@ ThorbjørnRavnAndersen: la squadra può anche mostrare all'OP che il loro approccio farà risparmiare tempo. Chissà? Ma mi chiedo se non stiamo effettivamente affrontando problemi di natura meno tecnica qui; l'OP continua a venire qui per dirci cosa fa la sua squadra (secondo la sua opinione), come se stesse cercando di trovare alleati per la sua causa. Potrebbe essere più utile discutere effettivamente del progetto insieme al team, non con estranei su Stack Exchange.
Christian Hackl,

11

Metto in dubbio l'affermazione (non comprovata) che fai:

per testare l'unità del servizio API Web avremo bisogno di deridere questa fabbrica

Questo non è necessariamente vero. Esistono molti modi per scrivere test e ci sono modi per scrivere test unit che non implicano simulazioni. Ancora più importante, ci sono altri tipi di test, come test funzionali o di integrazione. Molte volte è possibile trovare una "cucitura di prova" in una "interfaccia" che non sia un linguaggio di programmazione OOP interface.

Alcune domande per aiutarti a trovare una cucitura di prova alternativa, che potrebbe essere più naturale:

  • Vorrò mai scrivere un'API Web sottile su un'API diversa ?
  • Posso ridurre la duplicazione del codice tra l'API Web e l'API sottostante? Uno può essere generato in termini di altro?
  • Posso trattare l'intera API Web e l'API sottostante come un'unica unità "scatola nera" e fare affermazioni significative su come si comporta tutto?
  • Se in futuro l'API Web dovesse essere sostituita con una nuova implementazione, come potremmo procedere?
  • Se l'API Web fosse sostituita con una nuova implementazione in futuro, i clienti dell'API Web sarebbero in grado di notare? Se é cosi, come?

Un'altra affermazione non comprovata che fai riguarda DI:

progettiamo la classe di controller API Web per accettare DI (tramite il suo costruttore o setter), il che significa che stiamo progettando parte del controller solo per consentire DI e implementare un'interfaccia che altrimenti non ci serve, oppure utilizziamo una terza parte framework come Ninject per evitare di dover progettare il controller in questo modo, ma dovremo comunque creare un'interfaccia.

Iniezione di dipendenza non significa necessariamente creare un nuovo interface. Ad esempio, nella causa di un token di autenticazione: puoi semplicemente creare un vero token di autenticazione a livello di codice? Quindi il test può creare tali token e iniettarli. Il processo di convalida di un token dipende da un segreto crittografico di qualche tipo? Spero che tu non abbia codificato un segreto - mi aspetto che tu possa leggerlo dalla memoria in qualche modo, e in quel caso puoi semplicemente usare un segreto diverso (ben noto) nei tuoi casi di test.

Questo non vuol dire che non dovresti mai crearne uno nuovo interface. Ma non ti fissare perché esiste solo un modo per scrivere un test o un modo per simulare un comportamento. Se pensi fuori dagli schemi, di solito puoi trovare una soluzione che richiederà un minimo di contorsioni del tuo codice e che comunque ti darà l'effetto che desideri.


Punto preso in considerazione le asserzioni relative alle interfacce, ma anche se non le usassimo dovremmo comunque iniettare oggetti in qualche modo, questa è la preoccupazione del resto della squadra. vale a dire che alcuni membri del team sarebbero contenti di un CTR senza parametri che istanzia l'implementazione concreta e la lascia a quello. In effetti un membro ha abbandonato l'idea di usare la riflessione per iniettare beffe, quindi non dobbiamo progettare il codice per accettarli. Che è un odore di codice puzzolente imo
Lee

9

Sei fortunato perché questo è un nuovo progetto. Ho scoperto che Test Driven Design funziona molto bene per scrivere un buon codice (motivo per cui lo facciamo in primo luogo).

Capendo in anticipo come invocare un determinato pezzo di codice con dati di input realistici e quindi ottenere dati di output realistici che è possibile verificare è come previsto, si esegue la progettazione dell'API molto presto nel processo e si hanno buone probabilità di ottenere un design utile perché non sei ostacolato dal codice esistente che deve essere riscritto per adattarlo. Inoltre, è più facile da capire per i tuoi colleghi, in modo da poter avere di nuovo buone discussioni all'inizio del processo.

Si noti che "utile" nella frase sopra significa non solo che i metodi risultanti sono facili da invocare, ma anche che si tende a ottenere interfacce pulite che sono facili da sistemare nei test di integrazione e per cui scrivere mockup.

Tieni conto di questo. Soprattutto con la revisione tra pari. Nella mia esperienza, l'investimento di tempo e fatica verrà restituito molto rapidamente.


Abbiamo anche un problema con TDD, ovvero ciò che costituisce "codice minimo da passare". Ho dimostrato al team questo processo e hanno fatto eccezione non solo per scrivere ciò che abbiamo già progettato - che posso capire. Il "minimo" non sembra essere definito. Se scriviamo un test e abbiamo piani e progetti chiari, perché non scriverlo per superare il test?
Lee

@Lee "codice minimo da passare" ... beh, potrebbe sembrare un po 'stupido, ma è letteralmente quello che dice. Ad esempio, se si dispone di un test UserCanChangeTheirPassword, nel test si chiama la funzione (non ancora esistente) per modificare la password e quindi si afferma che la password è effettivamente cambiata. Quindi scrivi la funzione, fino a quando non puoi eseguire il test e non genera eccezioni né ha un'asserzione sbagliata. Se a quel punto hai un motivo per aggiungere qualsiasi codice, allora quel motivo va in un altro test, ad es UserCantChangePasswordToEmptyString.
R. Schmitz,

@Lee In definitiva, i tuoi test finiranno per essere la documentazione di ciò che fa il tuo codice, tranne la documentazione che controlla se si è realizzato da solo, invece di essere solo inchiostro su carta. Confronta anche con questa domanda : un metodo CalculateFactorialche restituisce solo 120 e il test ha esito positivo. Questo è il minimo. Ovviamente non è anche quello che era previsto, ma ciò significa solo che hai bisogno di un altro test per esprimere ciò che era previsto.
R. Schmitz,

1
@Lee Piccoli passi. Il minimo indispensabile può essere maggiore di quanto si pensi quando il codice supera banale. Anche il design che esegui quando implementi tutto in una volta può essere di nuovo meno ottimale perché fai ipotesi su come dovrebbe essere fatto senza aver ancora scritto i test che lo dimostrano. Ricorda ancora, all'inizio il codice dovrebbe fallire.
Thorbjørn Ravn Andersen,

1
Inoltre, i test di regressione sono molto importanti. Sono in campo per la squadra?
Thorbjørn Ravn Andersen,

8

Se devi modificare il codice, questo è l'odore del codice.

Per esperienza personale, se il mio codice è difficile da scrivere per i test, è un codice errato. Non è un cattivo codice perché non funziona o funziona come previsto, è cattivo perché non riesco a capire rapidamente perché funzioni. Se riscontro un bug, so che sarà un lavoro lungo e doloroso risolverlo. Il codice è anche difficile / impossibile da riutilizzare.

Un buon codice (pulito) suddivide le attività in sezioni più piccole che sono facilmente comprensibili a colpo d'occhio (o almeno un buon aspetto). Testare queste sezioni più piccole è facile. Posso anche scrivere test che testano solo una parte della base di codice con una facilità simile se sono abbastanza sicuro delle sottosezioni (il riutilizzo aiuta anche perché è già stato testato).

Mantieni il codice facile da testare, facile da refactoring e facile da riutilizzare dall'inizio e non ti ucciderai ogni volta che dovrai apportare modifiche.

Sto scrivendo questo mentre ricostruisco completamente un progetto che avrebbe dovuto essere un prototipo usa e getta in un codice più pulito. È molto meglio farlo dall'inizio e refactoring il codice errato il più presto possibile piuttosto che fissare uno schermo per ore e ore, avendo paura di toccare qualcosa per paura di rompere qualcosa che funziona parzialmente.


3
"Throwaway prototype" - ogni singolo progetto inizia la vita come uno di quelli ... meglio pensare a cose come non essere mai così. digitando questo come sto .. indovina un po '? ... refactoring di un prototipo usa e getta che si è rivelato non essere;)
Algy Taylor il

4
Se vuoi essere sicuro che un prototipo buttato via verrà buttato via, scrivilo in un linguaggio prototipo che non sarà mai permesso in produzione. Clojure e Python sono buone scelte.
Thorbjørn Ravn Andersen,

2
@ ThorbjørnRavnAndersen Mi ha fatto ridere. Doveva essere uno scavo in quelle lingue? :)
Lee

@Lee. No, solo esempi di lingue che potrebbero non essere accettabili per la produzione, in genere perché nessuno nell'organizzazione può mantenerle perché non hanno familiarità con esse e le loro curve di apprendimento sono ripide. Se quelli sono accettabili, scegli un altro che non lo è.
Thorbjørn Ravn Andersen,

4

Direi che scrivere codice che non può essere testato in unità è un odore di codice. In generale, se il tuo codice non può essere testato in unità, non è modulare, il che rende difficile da comprendere, mantenere o migliorare. Forse se il codice è un codice colla che ha davvero senso solo in termini di test di integrazione, puoi sostituire i test di integrazione con i test unitari, ma anche in questo caso quando l'integrazione fallisce dovrai isolare il problema e i test unitari sono un ottimo modo per fallo.

Tu dici

Abbiamo in programma di creare una fabbrica che restituirà un tipo di metodo di autenticazione. Non abbiamo bisogno che erediti un'interfaccia in quanto non prevediamo che sia mai qualcosa di diverso dal tipo concreto che sarà. Tuttavia, per testare l'unità del servizio API Web dovremo prendere in giro questa fabbrica.

Non lo seguo davvero. Il motivo per avere una fabbrica che crea qualcosa è per permetterti di cambiare fabbriche o cambiare ciò che la fabbrica crea facilmente, quindi non è necessario cambiare altre parti del codice. Se il tuo metodo di autenticazione non cambierà mai, la fabbrica è inutile gonfiare il codice. Tuttavia, se si desidera avere un metodo di autenticazione diverso nel test rispetto alla produzione, avere una fabbrica che restituisce un metodo di autenticazione diverso nel test rispetto alla produzione è un'ottima soluzione.

Non hai bisogno di DI o Mock per questo. Hai solo bisogno che la tua fabbrica supporti i diversi tipi di autenticazione e che sia configurabile in qualche modo, ad esempio da un file di configurazione o da una variabile d'ambiente.


2

In ogni disciplina ingegneristica che mi viene in mente, esiste un solo modo per raggiungere livelli di qualità decenti o più elevati:

Per tenere conto dell'ispezione / test nella progettazione.

Questo è vero nella costruzione, nella progettazione di chip, nello sviluppo di software e nella produzione. Ora, questo non significa che i test siano il pilastro su cui ogni progetto deve essere costruito, per niente. Ma con ogni decisione di progettazione, i progettisti devono essere chiari sugli impatti sui costi dei test e prendere decisioni consapevoli in merito al trade off.

In alcuni casi, i test manuali o automatizzati (ad esempio il selenio) saranno più convenienti dei test unitari, fornendo anche una copertura dei test accettabile per conto proprio. In rari casi è anche accettabile lanciare qualcosa che è quasi del tutto non testato. Ma queste devono essere decisioni consapevoli caso per caso. Chiamare un progetto che rappresenta il test di un "odore di codice" indica una grave mancanza di esperienza.


1

Ho scoperto che i test unitari (e altri tipi di test automatizzati) hanno la tendenza a ridurre gli odori di codice e non riesco a pensare a un singolo esempio in cui introducono odori di codice. I test unitari di solito ti costringono a scrivere codice migliore. Se non è possibile utilizzare facilmente un metodo in fase di test, perché dovrebbe essere più semplice nel codice?

Test unitari ben scritti mostrano come si intende utilizzare il codice. Sono una forma di documentazione eseguibile. Ho visto test di unità scritti in modo orribile e troppo lunghi che semplicemente non potevano essere compresi. Non scrivere quelli! Se hai bisogno di scrivere lunghi test per impostare le tue classi, le tue classi necessitano di refactoring.

I test unitari evidenzieranno dove si trovano alcuni dei tuoi odori di codice. Consiglierei di leggere Michael C. Feathers ' Lavorando efficacemente con il codice legacy . Anche se il tuo progetto è nuovo, se non ha già (o molti) test unitari, potresti aver bisogno di alcune tecniche non ovvie per far testare bene il tuo codice.


3
Puoi essere tentato di introdurre molti livelli di indiretta per poterlo testare e quindi non usarli mai come previsto.
Thorbjørn Ravn Andersen,

1

In breve:

Il codice testabile è (di solito) un codice gestibile - o meglio, il codice che è difficile da testare è di solito difficile da mantenere. Progettare un codice che non è testabile è simile a progettare una macchina che non è riparabile: peccato per il povero shmuck che alla fine verrà assegnato per ripararlo (potresti essere tu).

Un esempio è che intendiamo creare una fabbrica che restituirà un tipo di metodo di autenticazione. Non abbiamo bisogno che erediti un'interfaccia in quanto non prevediamo che sia mai qualcosa di diverso dal tipo concreto che sarà.

Sai che avrai bisogno di cinque diversi tipi di metodi di autenticazione tra tre anni, ora che l'hai detto, giusto? I requisiti cambiano e, mentre dovresti evitare di progettare eccessivamente il tuo design, avere un design testabile significa che il tuo design ha (appena) giunture sufficienti per essere modificato senza (troppo) dolore - e che i test del modulo ti forniranno mezzi automatizzati per vedere che le tue modifiche non rompono nulla.


1

Progettare intorno all'iniezione di dipendenza non è un odore di codice: è la migliore pratica. L'uso di DI non è solo per testabilità. Costruire i componenti attorno a DI aiuta la modularità e la riusabilità, consente più facilmente lo scambio di componenti principali (come un livello di interfaccia del database). Mentre aggiunge un certo grado di complessità, fatto nel modo giusto consente una migliore separazione dei livelli e un isolamento delle funzionalità che semplifica la gestione e la navigazione della complessità. Ciò semplifica la validazione corretta del comportamento di ciascun componente, riducendo i bug e facilitando anche la ricerca dei bug.


1
"fatto bene" è un problema. Devo mantenere due progetti in cui DI è stato fatto male (anche se mirava a farlo "bene"). Questo rende il codice chiaramente orribile e molto peggio dei progetti legacy senza DI e unit test. Ottenere DI nel modo giusto non è facile.
Gen

@Jan è interessante. Come hanno fatto a sbagliare?
Lee,

1
@Lee Un progetto è un servizio che ha bisogno di un orario di avvio rapido ma è orribilmente lento all'avvio perché tutta l'inizializzazione della classe viene eseguita in anticipo dal framework DI (Castle Windsor in C #). Un altro problema che vedo in questi progetti è mescolare DI con la creazione di oggetti con "nuovo", evitando il DI. Ciò rende nuovamente difficili i test e ha portato ad alcune brutte condizioni di gara.
Gen

1

Ciò significa essenzialmente che progettiamo la classe di controller API Web per accettare DI (tramite il suo costruttore o setter), il che significa che stiamo progettando parte del controller solo per consentire DI e implementare un'interfaccia che altrimenti non ci serve, o che usiamo un framework di terze parti come Ninject per evitare di dover progettare il controller in questo modo, ma dovremo comunque creare un'interfaccia.

Diamo un'occhiata alla differenza tra un testabile:

public class MyController : Controller
{
    private readonly IMyDependency _thing;

    public MyController(IMyDependency thing)
    {
        _thing = thing;
    }
}

e controller non testabile:

public class MyController : Controller
{
}

La prima opzione ha letteralmente 5 righe di codice extra, due delle quali possono essere generate automaticamente da Visual Studio. Dopo aver impostato il framework di iniezione delle dipendenze per sostituire un tipo concreto IMyDependencyin fase di runtime - che per qualsiasi framework DI decente, è un'altra singola riga di codice - tutto funziona, tranne ora puoi deridere e testare il controller in base al tuo cuore .

6 righe di codice extra per consentire la testabilità ... e i tuoi colleghi stanno sostenendo che è "troppo lavoro"? Questa discussione non vola con me e non dovrebbe volare con te.

E non è necessario creare e implementare un'interfaccia per i test: Moq , ad esempio, consente di simulare il comportamento di un tipo concreto a scopo di unit test. Naturalmente, non ti sarà molto utile se non riesci a iniettare quei tipi nelle classi che stai testando.

L'iniezione di dipendenza è una di quelle cose che una volta capito, ti chiedi "come ho lavorato senza questo?". È semplice, efficace e ha senso. Per favore, non permettere alla mancanza di comprensione da parte dei tuoi colleghi di nuove cose per ostacolare la verifica del tuo progetto.


1
Ciò che sei così veloce a respingere come "la mancanza di comprensione di cose nuove" può rivelarsi una buona comprensione delle cose vecchie. L'iniezione di dipendenza non è certamente nuova. L'idea, e probabilmente le prime implementazioni, sono vecchie di decenni. E sì, credo che la tua risposta sia un esempio di codice che diventa più complicato a causa di unit test, e forse un esempio di unit test che interrompe l'incapsulamento (perché chi dice che la classe ha un costruttore pubblico in primo luogo?). Ho spesso rimosso l'iniezione di dipendenza da basi di codice che avevo ereditato da qualcun altro, a causa dei compromessi.
Christian Hackl,

I controller hanno sempre un costruttore pubblico, implicito o no, perché MVC lo richiede. "Complicato" - forse, se non capisci come funzionano i costruttori. Incapsulamento: sì, in alcuni casi, ma il dibattito DI vs incapsulamento è in corso, altamente soggettivo, che non sarà di aiuto in questo caso, e in particolare per la maggior parte delle applicazioni, DI ti servirà meglio dell'incapsulamento IMO.
Ian Kemp,

Per quanto riguarda i costruttori pubblici: in effetti, questa è una particolarità del quadro utilizzato. Stavo pensando al caso più generale di una classe ordinaria che non è istanziata da un quadro. Perché credi che vedere parametri di metodo aggiuntivi come complessità aggiunta equivalga a una mancanza di comprensione su come funzionano i costruttori? Tuttavia, apprezzo il fatto che tu riconosca l'esistenza di un compromesso tra DI e incapsulamento.
Christian Hackl,

0

Quando scrivo unit test, comincio a pensare a cosa potrebbe andare storto nel mio codice. Mi aiuta a migliorare la progettazione del codice e ad applicare il principio della responsabilità singola (SRP). Inoltre, quando torno a modificare lo stesso codice qualche mese dopo, mi aiuta a confermare che la funzionalità esistente non è danneggiata.

C'è una tendenza a usare le funzioni pure il più possibile (app senza server). Il test unitario mi aiuta a isolare lo stato e scrivere funzioni pure.

In particolare, avremo un servizio API Web che sarà molto sottile. La sua principale responsabilità sarà il marshalling di richieste / risposte Web e la chiamata di un'API sottostante che contenga la logica aziendale.

Scrivi prima i test unitari per l'API sottostante e se hai tempo di sviluppo sufficiente, devi scrivere anche i test per il servizio API Web sottile.

TL; DR, unit testing consente di migliorare la qualità del codice e di apportare modifiche future al codice senza rischi. Migliora anche la leggibilità del codice. Usa i test anziché i commenti per esprimere il tuo punto.


0

La linea di fondo, e quale dovrebbe essere la tua argomentazione con il lotto riluttante, è che non c'è conflitto. Il grande errore sembra essere stato che qualcuno ha coniato l'idea di "progettare per i test" per le persone che odiano i test. Avrebbero dovuto semplicemente chiudere la bocca o dirlo in modo diverso, come "prendiamoci il tempo per farlo bene".

L'idea che "devi implementare un'interfaccia" per rendere qualcosa di testabile è sbagliata. L'interfaccia è già implementata, non è ancora dichiarata nella dichiarazione di classe. Si tratta di riconoscere i metodi pubblici esistenti, copiare le loro firme su un'interfaccia e dichiarare tale interfaccia nella dichiarazione della classe. Nessuna programmazione, nessuna modifica alla logica esistente.

Apparentemente alcune persone hanno un'idea diversa a riguardo. Ti suggerisco di provare a risolvere prima questo.

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.