È una cattiva pratica modificare il codice rigorosamente a scopo di test


77

Ho un dibattito con un collega programmatore sul fatto che sia una buona o cattiva pratica modificare un pezzo di codice funzionante solo per renderlo testabile (ad esempio tramite test unitari).

La mia opinione è che sia OK, entro i limiti del mantenimento di buone pratiche orientate agli oggetti e di ingegneria del software (non "rendere tutto pubblico", ecc.).

L'opinione del mio collega è che la modifica del codice (che funziona) solo a scopo di test sia errata.

Solo un semplice esempio, pensa a questo pezzo di codice che viene utilizzato da alcuni componenti (scritto in C #):

public void DoSomethingOnAllTypes()
{
    var types = Assembly.GetExecutingAssembly().GetTypes();

    foreach (var currentType in types)
    {
        // do something with this type (e.g: read it's attributes, process, etc).
    }
}

Ho suggerito che questo codice può essere modificato per richiamare un altro metodo che farà il lavoro effettivo:

public void DoSomething(Assembly asm)
{
    // not relying on Assembly.GetExecutingAssembly() anymore...
}

Questo metodo utilizza un oggetto Assembly su cui lavorare, rendendo possibile il passaggio del proprio assembly su cui eseguire i test. Il mio collega non pensava che questa fosse una buona pratica.

Qual è considerata una pratica buona e comune?


5
Il tuo metodo modificato dovrebbe prendere un Typecome parametro, non un Assembly.
Robert Harvey,

Hai ragione - Tipo o Assemblaggio, il punto era che il codice dovrebbe consentire di fornirlo come parametro, anche se può sembrare che questa funzionalità sia lì solo per essere utilizzata per il test ...
liortal

5
La tua auto ha una porta ODBI per "testare" - se le cose fossero perfette non sarebbe necessario. Suppongo che la tua auto sia più affidabile del suo software.
mattnz,

24
Che garanzia ha che il codice "funzioni"?
Craige,

3
Certo, fallo. Il codice testato è stabile nel tempo. Ma avrai un tempo molto più semplice per spiegare i bug appena introdotti se sono venuti con un miglioramento delle funzionalità piuttosto che una correzione per la testabilità
Petter Nordlander

Risposte:


135

La modifica del codice per renderlo più testabile comporta vantaggi oltre alla testabilità. In generale, codice più testabile

  • È più facile da mantenere,
  • È più facile ragionare,
  • È più liberamente accoppiato, e
  • Ha un design complessivo migliore, dal punto di vista architettonico.

3
Non ho menzionato queste cose nella mia risposta, ma sono totalmente d'accordo. Rielaborare il codice per renderlo più testabile tende ad avere diversi effetti collaterali felici.
Jason Swett,

2
+1: generalmente sono d'accordo. Vi sono certamente casi chiari in cui rendere testabile il codice danneggia questi altri obiettivi benefici, e in questi casi la testabilità ha la priorità più bassa. Ma in generale, se il tuo codice non è abbastanza flessibile / estendibile da testare, non sarà abbastanza flessibile / estensibile da usare o invecchiare con grazia.
Telastyn

6
Il codice verificabile è un codice migliore. Esiste sempre un rischio intrinseco nella modifica del codice che non prevede test al suo interno, e il modo più semplice ed economico per mitigare il rischio a brevissimo termine è lasciarlo da solo, il che va bene ... fino a quando non sarà effettivamente necessario per cambiare il codice. Quello che devi fare è vendere i vantaggi del test unitario al tuo collega; se tutti sono a bordo con unit test, allora non ci può essere alcun argomento sul fatto che il codice debba essere testabile. Se non tutti sono a bordo con unit test non ha senso.
guysherman,

3
Esattamente. Ricordo che stavo scrivendo test unitari per circa 10k righe sorgente del mio codice. Sapevo che il codice funzionava perfettamente, ma i test mi hanno costretto a ripensare alcune situazioni. Ho dovuto chiedermi "Che cosa sta facendo esattamente questo metodo?". Ho trovato diversi bug nel codice di lavoro solo guardandolo da un nuovo punto di vista.
Sulthan,

2
Continuo a fare clic sul pulsante Su, ma posso darti solo un punto. Il mio attuale datore di lavoro è troppo miope per consentirci di implementare effettivamente i test unitari e sto ancora beneficiando di scrivere consapevolmente il mio codice come se stessi lavorando su test-driven.
AmericanUmlaut,

59

Ci sono forze (apparentemente) opposte in gioco.

  • Da un lato, si desidera applicare l'incapsulamento
  • D'altra parte, vuoi essere in grado di testare il software

I sostenitori di mantenere privati ​​tutti i "dettagli di implementazione" sono generalmente motivati ​​dal desiderio di mantenere l'incapsulamento. Tuttavia, mantenere tutto bloccato e non disponibile è un approccio incompreso all'incapsulamento. Se mantenere tutto non disponibile fosse l'obiettivo finale, l'unico vero codice incapsulato sarebbe questo:

static void Main(string[] args)

Il tuo collega sta proponendo di renderlo l'unico punto di accesso nel tuo codice? Tutti gli altri codici dovrebbero essere inaccessibili ai chiamanti esterni?

Quasi. Allora cosa rende giusto rendere pubblici alcuni metodi? Non è, alla fine, una decisione soggettiva di progettazione?

Non proprio. Ciò che tende a guidare i programmatori, anche a livello inconscio, è, ancora una volta, il concetto di incapsulamento. Ti senti sicuro di esporre un metodo pubblico quando protegge adeguatamente i suoi invarianti .

Non vorrei esporre un metodo privato che non protegge i suoi invarianti, ma spesso è possibile modificare in modo che esso non proteggere i suoi invarianti, e poi esporlo al pubblico (naturalmente, con TDD, si fa la viceversa).

Aprire un'API per la testabilità è una buona cosa , perché quello che stai veramente facendo è applicare il principio aperto / chiuso .

Se hai un solo chiamante della tua API, non sai quanto sia flessibile la tua API. Le probabilità sono, è abbastanza inflessibile. I test fungono da secondo client, fornendo preziosi feedback sulla flessibilità della tua API .

Quindi, se i test suggeriscono che dovresti aprire la tua API, allora fallo; ma mantenere l'incapsulamento, non nascondendo la complessità, ma esponendo la complessità in modo sicuro.


3
+1 Ti senti sicuro di esporre un metodo pubblico quando protegge adeguatamente i suoi invarianti.
shambulator,

1
Loooooove la tua risposta! :) Quante volte sento: "L'incapsulamento è il processo per rendere privati ​​alcuni metodi e proprietà". È come dire quando si programma orientato agli oggetti, questo perché si programma con oggetti. :( Non mi sorprende che una risposta così chiara arrivi dal maestro dell'iniezione di dipendenza. Leggerò sicuramente un paio di volte la tua risposta per farmi sorridere del mio povero vecchio codice legacy.
Samuel,

Ho aggiunto alcune modifiche alla tua risposta. A parte questo, ho letto il tuo post sull'incapsulamento delle proprietà e l'unica ragione convincente che abbia mai sentito per l'uso delle proprietà automatiche è che la modifica di una variabile pubblica in una proprietà pubblica interrompe la compatibilità binaria; esponendolo come una proprietà automatica dall'inizio, è possibile aggiungere successivamente la convalida o altre funzionalità interne, senza interrompere il codice client.
Robert Harvey,

21

Sembra che tu stia parlando di iniezione di dipendenza . È davvero comune e IMO, abbastanza necessario per la testabilità.

Per rispondere alla domanda più ampia se è una buona idea modificare il codice solo per renderlo testabile, pensalo in questo modo: il codice ha molteplici responsabilità, tra cui a) da eseguire, b) da leggere dagli umani, ec) da essere testato. Tutti e tre sono importanti e se il tuo codice non soddisfa tutte e tre le responsabilità, direi che non è un ottimo codice. Quindi modifica via!


DI non è la domanda principale (stavo solo cercando di fare un esempio), il punto era se sarebbe stato giusto fornire un altro metodo che originariamente non era stato progettato per essere creato, solo per motivi di test.
liortal

4
Lo so. Pensavo di aver affrontato il tuo punto principale nel mio secondo paragrafo, con un "sì".
Jason Swett,

13

È un po 'un problema con pollo e uova.

Uno dei motivi principali per cui è bene avere una buona copertura di prova del codice è che ti consente di eseguire il refactelessing senza paura. Ma ti trovi in ​​una situazione in cui devi refactificare il codice per ottenere una buona copertura del test! E il tuo collega ha paura.

Vedo il punto di vista del tuo collega. Hai un codice che (presumibilmente) funziona, e se vai e lo rifatti - per qualsiasi motivo - c'è il rischio che tu lo rompa.

Ma se si tratta di un codice che dovrebbe avere continue manutenzioni e modifiche, si corre questo rischio ogni volta che si esegue un lavoro su di esso. E il refactoring ora e ottenere un po 'di copertura dei test ora ti permetteranno di correre quel rischio, in condizioni controllate, e di mettere il codice in una forma migliore per future modifiche.

Quindi direi, a meno che questa particolare base di codice non sia abbastanza statica e non ci si aspetti che faccia un lavoro significativo in futuro, che ciò che vuoi fare è una buona pratica tecnica.

Certo, se si tratta di una buona pratica commerciale è un 'altra lattina di vermi ..


Una preoccupazione importante. Di solito, eseguo liberamente refactoring supportati da IDE poiché la possibilità di rompere qualsiasi cosa sia molto bassa. Se il refactoring è più coinvolto, lo farei solo quando devo cambiare il codice comunque. Per cambiare le cose sono necessari dei test per ridurre i rischi, in modo da ottenere anche valore aziendale.
Hans-Peter Störr,

Il punto è: vogliamo mantenere il vecchio codice perché vogliamo dare la responsabilità all'oggetto di interrogare l'assembly corrente o perché non vogliamo aggiungere alcune proprietà pubbliche o cambiare la firma del metodo? Penso che il codice violi l'SRP e, per questo motivo, dovrebbe essere riformulato, indipendentemente dalla paura dei colleghi. Sicuramente se si tratta di un'API pubblica utilizzata da molti utenti, dovrai pensare a una strategia come l'implementazione di una facciata o qualsiasi altra cosa che ti aiuti a garantire al vecchio codice un'interfaccia che impedisce troppe modifiche.
Samuel,

7

Questa potrebbe essere solo una differenza di enfasi rispetto alle altre risposte, ma direi che il codice non dovrebbe essere riformulato rigorosamente per migliorare la testabilità. La testabilità è estremamente importante per la manutenzione, ma la testabilità non è fine a se stessa. In quanto tale, rimanderei qualsiasi refactoring fino a quando non è possibile prevedere che questo codice avrà bisogno di manutenzione per favorire la fine dell'attività.

Al punto che si stabilisce che il codice richiede una certa manutenzione, che sarebbe un buon momento di refactoring per testabilità. A seconda del caso aziendale, potrebbe essere un presupposto valido che tutto il codice richiederà eventualmente un po 'di manutenzione, nel qual caso scompare la distinzione che traccio qui con le altre risposte ( ad esempio la risposta di Jason Swett ).

Per riassumere: la testabilità da sola non è (IMO) un motivo sufficiente per riformattare una base di codice. La testabilità ha un ruolo prezioso nel consentire la manutenzione su base di codice, ma è un requisito aziendale modificare la funzione del codice che dovrebbe guidare il refactoring. Se non ci sono tali requisiti aziendali, probabilmente sarebbe meglio lavorare su qualcosa di cui i tuoi clienti si preoccuperanno.

(Il nuovo codice, ovviamente, viene mantenuto attivamente, quindi dovrebbe essere scritto per essere testabile.)


2

Penso che il tuo collega abbia torto.

Altri hanno menzionato i motivi per cui questa è già una buona cosa, ma fintanto che ti viene dato il via libera per farlo, dovresti andare bene.

Il motivo di questo avvertimento è che apportare qualsiasi modifica al codice ha il costo di dover essere nuovamente testato. A seconda di ciò che fai, questo lavoro di test può effettivamente essere un grande sforzo da solo.

Non è necessariamente il tuo posto di prendere la decisione di refactoring invece di lavorare su nuove funzionalità che andranno a beneficio della tua azienda / cliente.


2

Ho usato strumenti di copertura del codice come parte del test unitario per verificare se tutti i percorsi attraverso il codice sono esercitati. Come programmatore / tester piuttosto bravo da solo, di solito copro l'80-90% dei percorsi del codice.

Quando studio i percorsi scoperti e faccio uno sforzo per alcuni di essi, è allora che scopro bug come casi di errore che "non accadranno mai". Quindi sì, la modifica del codice e il controllo della copertura del test rendono il codice migliore.


2

Il tuo problema qui è che i tuoi strumenti di test sono merda. Dovresti essere in grado di deridere quell'oggetto e chiamare il tuo metodo di prova senza cambiarlo - perché mentre questo semplice esempio è davvero semplice e facile da modificare, cosa succede quando hai qualcosa di molto più complicato.

Molte persone hanno modificato il loro codice per introdurre IoC, DI e classi basate sull'interfaccia semplicemente per abilitare i test unitari usando gli strumenti di derisione e unit test che richiedono queste modifiche al codice. Non mi rendo conto che sono salutari, non quando vedi un codice abbastanza diretto e semplice che si trasforma in un incubo di interazioni complesse guidate interamente dalla necessità di rendere ogni metodo di classe totalmente disaccoppiato da tutto il resto . E per aggiungere la beffa al danno, abbiamo quindi molti argomenti per stabilire se i metodi privati ​​debbano essere testati o meno! (ovviamente dovrebbero, cosa "

Il problema, ovviamente, è nella natura degli strumenti di test allora.

Ci sono strumenti migliori disponibili ora che potrebbero mettere queste modifiche di progettazione a letto per sempre. Microsoft ha Fakes (nee Moles) che ti consente di stub oggetti concreti, compresi quelli statici, quindi non è più necessario modificare il codice per adattarlo allo strumento. Nel tuo caso, se hai usato Fakes, sostituiresti la chiamata GetTypes con la tua che ha restituito dati di test validi e non validi - il che è abbastanza importante, la modifica suggerita non lo prevede affatto.

Per rispondere: il tuo collega ha ragione, ma forse per ragioni sbagliate. Non modificare il codice per testare, cambiare lo strumento di test (o l'intera strategia di test per avere più unit test in stile integrazione invece di test così dettagliati).

Martin Fowler ha discusso di quest'area nel suo articolo Mock not Stubs


1

Una buona pratica comune è quella di utilizzare i test di unità e i log di debug . I test unitari assicurano che se si apportano ulteriori modifiche al programma, la vecchia funzionalità non si interrompe. I registri di debug possono aiutarti a tracciare il programma in fase di esecuzione.
A volte capita che anche oltre ciò dobbiamo avere qualcosa solo a scopo di test. Non è insolito cambiare il codice per questo. Tuttavia, è necessario prestare attenzione affinché il codice di produzione non sia interessato da ciò. In C ++ e C questo si ottiene usando MACRO , che è un'entità del tempo di compilazione. Quindi il codice di test non viene affatto visualizzato nell'ambiente di produzione. Non so se tale disposizione è presente in C #.
Inoltre, quando aggiungi il codice di test nel tuo programma, dovrebbe essere chiaramente visibileche questa parte di codice viene aggiunta a scopo di test. Altrimenti lo sviluppatore che cerca di capire il codice sta semplicemente sudando per quella parte di codice.


1

Ci sono alcune gravi differenze tra i tuoi esempi. Nel caso di DoSomethingOnAllTypes(), esiste un'implicazione do somethingapplicabile ai tipi nell'assieme corrente. Ma DoSomething(Assembly asm)indica esplicitamente che è possibile passare qualsiasi montaggio ad esso.

Il motivo per cui lo sottolineo è che molti passaggi di dipendenza-iniezione-per-test al di fuori dei limiti dell'oggetto originale. So che hai detto " non 'rendere tutto pubblico' ", ma questo è uno dei più grandi errori di quel modello, seguito da vicino da questo: aprire i metodi dell'oggetto fino agli usi a cui non sono destinati.


0

La tua domanda non ha dato molto contesto in cui il tuo collega ha discusso, quindi c'è spazio per le speculazioni

"cattiva pratica" dipende o meno da come e quando vengono apportate le modifiche.

Nella mia opzione il tuo esempio per estrarre un metodo DoSomething(types)è ok.

Ma ho visto un codice che non va bene così:

public void DoSomethingOnAllTypes()
{
  var types = (!testmode) 
      ? Assembly.GetExecutingAssembly().GetTypes() 
      : getTypesFromTestgenerator();

  foreach (var currentType in types)
  {
     if (!testmode)
     {
        // do something with this type that made the unittest fail and should be skipped.
     }
     // do something with this type (e.g: read it's attributes, process, etc).
  }
}

Queste modifiche hanno reso il codice più difficile da capire perché hai aumentato il numero di possibili percorsi di codice.

Cosa intendo con come e quando :

se disponi di un'implementazione funzionante e per motivi di "implementazione delle funzionalità di test" hai apportato le modifiche, devi ripetere il test della tua applicazione perché potresti aver rotto il tuo DoSomething()metodo.

È if (!testmode)più difficile da capire e testare rispetto al metodo estratto.

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.