Ha senso scrivere test per il codice legacy quando non c'è tempo per un refactoring completo?


72

Di solito cerco di seguire il consiglio del libro Lavorare efficacemente con Legacy Cod e . Spezzo le dipendenze, sposto parti del codice in @VisibleForTesting public staticmetodi e in nuove classi per rendere testabile il codice (o almeno una parte di esso). E scrivo dei test per assicurarmi di non interrompere nulla quando modifico o aggiungo nuove funzioni.

Un collega dice che non dovrei farlo. Il suo ragionamento:

  • Il codice originale potrebbe non funzionare correttamente in primo luogo. E scrivere test per questo rende le future correzioni e modifiche più difficili poiché gli sviluppatori devono capire e modificare anche i test.
  • Se è un codice GUI con una certa logica (~ 12 righe, 2-3 blocco if / else, per esempio), un test non vale la pena, dato che il codice è troppo banale per cominciare.
  • Simili schemi negativi potrebbero esistere anche in altre parti della base di codice (che non ho ancora visto, sono piuttosto nuovo); sarà più facile ripulirli tutti in un unico grande refactoring. L'estrazione della logica potrebbe minare questa possibilità futura.

Dovrei evitare di estrarre parti testabili e scrivere test se non abbiamo tempo per il refactoring completo? C'è qualche svantaggio da considerare?


29
Sembra che il tuo collega stia solo presentando delle scuse perché non lavora in quel modo. Le persone a volte si comportano così a causa dell'essere troppo tenaci per cambiare il loro modo di fare le cose adottato.
Doc Brown,

3
ciò che dovrebbe essere classificato come un bug può essere invocato da altre parti del codice trasformandolo in una funzionalità
maniaco del cricchetto

1
L'unico buon argomento contro cui posso pensare è che il tuo refactoring stesso potrebbe introdurre nuovi bug se hai letto male / copiato male qualcosa. Per questo motivo sono libero di rifattorizzare e correggere il contenuto del mio cuore sulla versione attualmente in fase di sviluppo, ma qualsiasi correzione sulle versioni precedenti deve affrontare un ostacolo molto più elevato e potrebbe non essere approvata se sono "solo" pulizia estetica / strutturale dal si ritiene che il rischio superi il potenziale guadagno. Conoscere la propria cultura locale - non solo l'idea di un orcaio - e avere ragioni ESTREMAMENTE forti pronte prima di fare qualsiasi altra cosa.
Keshlam,

6
Il primo punto è un po 'esilarante: "Non provarlo, potrebbe essere difettoso." Bene, sì? Quindi è bene sapere che - o vogliamo risolverlo o non vogliamo che nessuno cambi il comportamento reale in quello che hanno detto alcune specifiche di design. In ogni caso, i test (e l'esecuzione dei test in un sistema automatizzato) sono utili.
Christopher Creutzig,

3
Troppo spesso l '"unico grande refactoring" che sta per accadere e che curerà tutti i mali è un mito, inventato da coloro che vogliono semplicemente spingere le cose che considerano noiose (scrivere test) nel lontano futuro. E se mai diventasse reale, si pentiranno seriamente di averlo lasciato diventare così grande!
Julia Hayward,

Risposte:


100

Ecco la mia impressione non scientifica personale: tutte e tre le ragioni suonano come illusioni cognitive diffuse ma false.

  1. Certo, il codice esistente potrebbe essere sbagliato. Potrebbe anche essere giusto. Poiché l'applicazione nel suo insieme sembra avere un valore per te (altrimenti la scarteresti semplicemente), in assenza di informazioni più specifiche dovresti presumere che sia prevalentemente giusto. "Scrivere test rende le cose più difficili perché nel complesso c'è più codice" è un atteggiamento semplicistico e molto sbagliato.
  2. Spendi tutti i tuoi sforzi di refactoring, testing e miglioramento nei luoghi in cui aggiungono il massimo valore con il minimo sforzo. Le subroutine della GUI per la formattazione del valore spesso non sono la prima priorità. Ma non testare qualcosa perché "è semplice" è anche un atteggiamento molto sbagliato. Praticamente tutti gli errori gravi vengono commessi perché la gente pensava di aver capito qualcosa di meglio di quello che effettivamente faceva.
  3. "In futuro faremo tutto in un sol colpo" è un bel pensiero. Di solito il grande schiocco rimane saldamente in futuro, mentre nel presente non succede nulla. Io sono fermamente convinto della convinzione "lento e costante vince la gara".

23
+1 per "Praticamente tutti gli errori gravi vengono commessi perché le persone pensavano di aver capito qualcosa di meglio di quanto non facessero realmente."
rem

Per quanto riguarda il punto 1 - con BDD , i test sono auto documentanti ...
Robbie Dee,

2
Come sottolinea @ guillaume31, parte del valore della scrittura dei test sta dimostrando come funziona effettivamente il codice, che può essere o meno conforme alle specifiche. Ma potrebbe essere la specifica "sbagliata": le esigenze aziendali potrebbero essere cambiate e il codice riflette i nuovi requisiti, ma le specifiche no. Supporre semplicemente che il codice sia "sbagliato" è eccessivamente semplicistico (vedi punto 1). E di nuovo i test ti diranno cosa fa effettivamente il codice, non quello che qualcuno pensa / dice di fare (vedi punto 2).
David

anche se fai un colpo solo, devi capire il codice. I test ti aiuteranno a catturare comportamenti inaspettati anche se non refatti ma riscrivi (e se fai refactoring, ti aiutano ad assicurarti che il refactoring non rompa il comportamento legacy - o solo dove vuoi che si rompa). Sentiti libero di incorporare o meno - come desideri.
Frank Hopkins,

50

Alcuni pensieri:

Quando si esegue il refactoring del codice legacy, non importa se alcuni dei test scritti sembrano contraddire le specifiche ideali. Ciò che conta è che testano il comportamento attuale del programma . Il refactoring consiste nel prendere piccoli passi iso-funzionali per rendere il codice più pulito; non vuoi impegnarti nella correzione di bug mentre esegui il refactoring. Inoltre, se noti un bug palese, non andrà perso. Puoi sempre scrivere un test di regressione per esso e disabilitarlo temporaneamente, oppure inserire un'attività di correzione di bug nel tuo backlog per dopo. Una cosa alla volta.

Concordo sul fatto che il codice GUI puro sia difficile da testare e forse non adatto per il refactoring in stile " Lavorare efficacemente ... ". Tuttavia, ciò non significa che non dovresti estrarre comportamenti che non hanno nulla a che fare con il livello GUI e testare il codice estratto. E "12 righe, 2-3 if / else block" non è banale. Tutto il codice con almeno un po 'di logica condizionale dovrebbe essere testato.

Nella mia esperienza, i grandi refactoring non sono facili e raramente funzionano. Se non ti poni obiettivi precisi e minuscoli, c'è un rischio elevato di intraprendere una rilavorazione infinita e strabiliante in cui non finirai mai in piedi alla fine. Più grande è il cambiamento, più rischi di rompere qualcosa e più problemi avrai a scoprire dove hai fallito.

Rendere le cose progressivamente migliori con piccoli refactoring ad hoc non "indebolisce le possibilità future", le sta abilitando, consolidando il terreno paludoso in cui si trova l'applicazione. Dovresti assolutamente farlo.


5
+1 per "test che scrivi test il comportamento attuale del programma "
David

17

Inoltre: "Il codice originale potrebbe non funzionare correttamente", ciò non significa che devi semplicemente modificare il comportamento del codice senza preoccuparti dell'impatto. Un altro codice può fare affidamento su ciò che sembra essere un comportamento non funzionante o gli effetti collaterali dell'attuale implementazione. La copertura del test dell'applicazione esistente dovrebbe facilitare il refactoring in un secondo momento, perché ti aiuterà a scoprire quando hai accidentalmente rotto qualcosa. Dovresti prima provare le parti più importanti.


Tristemente vero. Abbiamo un paio di ovvi bug che si manifestano in casi limite che non possiamo correggere perché il nostro cliente preferisce la coerenza rispetto alla correttezza. (Sono causati dal codice di raccolta dei dati che consente alle cose che il codice di segnalazione non tiene conto, come lasciare vuoto un campo in una serie di campi)
Izkata

14

La risposta di Kilian copre gli aspetti più importanti, ma voglio espandere i punti 1 e 3.

Se uno sviluppatore vuole cambiare il codice (refactor, extension, debug), deve capirlo. Deve assicurarsi che i suoi cambiamenti influenzino esattamente il comportamento che desidera (niente nel caso del refactoring) e nient'altro.

Se ci sono test, allora deve capire anche i test, certo. Allo stesso tempo, i test dovrebbero aiutarla a capire il codice principale e comunque i test sono molto più facili da capire rispetto al codice funzionale (a meno che non siano test negativi). E i test aiutano a mostrare cosa è cambiato nel comportamento del vecchio codice. Anche se il codice originale è errato e il test verifica tale comportamento errato, è comunque un vantaggio.

Tuttavia, ciò richiede che i test siano documentati come test di comportamento preesistente, non una specifica.

Alcune considerazioni anche sul punto 3: oltre al fatto che il "grande colpo" raramente accade realmente, c'è anche un'altra cosa: in realtà non è più facile. Per essere più semplici, si dovrebbero applicare diverse condizioni:

  • L'antipasto da refactoring deve essere facilmente reperibile. Tutti i tuoi singoli sono chiamati XYZSingleton? La loro istanza getter viene sempre chiamata getInstance()? E come trovi le tue gerarchie troppo profonde? Come cerchi i tuoi oggetti divini? Questi richiedono l'analisi delle metriche del codice e quindi l'ispezione manuale delle metriche. Oppure inciampi su di loro mentre lavori, come hai fatto.
  • Il refactoring deve essere meccanico. Nella maggior parte dei casi, la parte difficile del refactoring è comprendere il codice esistente abbastanza bene da sapere come modificarlo. Singletons di nuovo: se il singleton è sparito, come si ottengono le informazioni richieste ai suoi utenti? Spesso significa comprendere il callgraph locale in modo da sapere da dove ottenere le informazioni. Ora, cosa è più semplice: cercare i dieci singoli nella tua app, capire gli usi di ciascuno (il che porta alla necessità di capire il 60% della base di codice) e strapparli? O prendendo il codice che già capisci (perché ci stai lavorando proprio ora) e strappando i singoli che vengono usati là fuori? Se il refactoring non è così meccanico da richiedere poca o nessuna conoscenza del codice circostante, non è utile raggrupparlo.
  • Il refactoring deve essere automatizzato. Questo è in qualche modo basato sull'opinione pubblica, ma qui va. Un po 'di refactoring è divertente e soddisfacente. Un sacco di refactoring è noioso e noioso. Lasciare il codice su cui hai appena lavorato in uno stato migliore ti dà una sensazione piacevole e calda, prima di passare a cose più interessanti. Cercare di riformattare un'intera base di codice ti lascerà frustrato e arrabbiato con i programmatori idioti che l'hanno scritto. Se vuoi fare un grande refactoring, allora deve essere ampiamente automatizzato in modo da ridurre al minimo la frustrazione. Questo è, in un certo senso, una combinazione dei primi due punti: è possibile automatizzare il refactoring solo se è possibile automatizzare la ricerca del codice errato (cioè facilmente reperibile) e automatizzare la sua modifica (cioè meccanico).
  • Il graduale miglioramento rende un caso aziendale migliore. Il grande refactoring è incredibilmente dirompente. Se rifatti un pezzo di codice, invariabilmente ti unisci in conflitti con altre persone che ci lavorano, perché hai appena diviso il metodo che stavano cambiando in cinque parti. Quando refatti un pezzo di codice di dimensioni ragionevoli, ottieni conflitti con alcune persone (1-2 quando si divide la megafunzione a 600 linee, 2-4 quando si rompe l'oggetto dio, 5 quando si estrae il singleton da un modulo ), ma avresti comunque avuto quei conflitti a causa delle tue modifiche principali. Quando esegui un refactoring a livello di codice, sei in conflitto con tutti. Per non parlare del fatto che lega alcuni sviluppatori per giorni. Il miglioramento graduale causa un po 'più di tempo per ogni modifica del codice. Questo lo rende più prevedibile e non esiste un periodo di tempo così visibile in cui non accade nulla tranne la pulizia.

12

Esiste una cultura in alcune aziende in cui sono reticenti per consentire agli sviluppatori in qualsiasi momento di migliorare il codice che non fornisce direttamente valore aggiunto, ad esempio nuove funzionalità.

Probabilmente sto predicando al convertito qui, ma questa è chiaramente falsa economia. Il codice pulito e conciso è vantaggioso per gli sviluppatori successivi. È solo che il rimborso non è immediatamente evidente.

Personalmente sottoscrivo il Boy Scout Principle, ma altri (come hai visto) no.

Detto questo, il software soffre di entropia e crea debito tecnico. Gli sviluppatori precedenti a corto di tempo (o forse solo pigri o inesperti) potrebbero aver implementato soluzioni buggy non ottimali rispetto a quelle ben progettate. Mentre può sembrare desiderabile riformattare questi, rischi di introdurre nuovi bug in ciò che è (per gli utenti comunque) codice funzionante.

Alcuni cambiamenti comportano un rischio inferiore rispetto ad altri. Ad esempio, dove lavoro, tende ad esserci un sacco di codice duplicato che può essere tranquillamente trasferito in una subroutine con un impatto minimo.

Alla fine, è necessario effettuare una valutazione del giudizio su quanto lontano si prende il refactoring ma c'è innegabilmente valore nell'aggiungere test automatici se non esistono già.


2
Sono totalmente d'accordo in linea di principio, ma in molte aziende si tratta di tempo e denaro. Se la parte "riordina" richiede solo pochi minuti, allora va bene, ma una volta che la stima per la riordino inizia a diventare più grande (per qualche definizione di grande), tu, la persona che codifica devi delegare quella decisione al tuo capo o responsabile del progetto. Non è il tuo posto a decidere il valore di quel tempo trascorso. Lavorare sulla correzione degli errori X o la nuova funzione Y potrebbe avere un valore molto più alto per il progetto / azienda / cliente.
ozz,

2
Potresti anche non essere a conoscenza di problemi più grandi come il progetto che viene demolito in 6 mesi o semplicemente che la società apprezza di più il tuo tempo (ad es. Fai qualcosa che ritengono più importante e qualcun altro può svolgere il lavoro di refeactor). Anche i lavori di refactoring possono influire sui test. Un grande refactoring determinerà una regressione completa del test? L'azienda ha risorse che può distribuire per fare questo?
ozz,

Sì, come hai toccato, ci sono una miriade di ragioni per cui la chirurgia del codice maggiore può o meno essere una buona idea: altre priorità di sviluppo, la durata del software, risorse di test, esperienza degli sviluppatori, accoppiamento, ciclo di rilascio, familiarità con il codice base, documentazione, criticità della missione, cultura aziendale, ecc. ecc. ecc. È una chiamata di giudizio
Robbie Dee,

4

Nella mia esperienza, un test di caratterizzazione di qualche tipo funziona bene. Ti offre una copertura di test ampia ma non molto specifica relativamente rapidamente, ma può essere difficile da implementare per le applicazioni GUI.

Vorrei quindi scrivere i test unitari per le parti che si desidera modificare e farlo ogni volta che si desidera apportare una modifica, aumentando così la copertura del test unitario nel tempo.

Questo approccio ti dà una buona idea se le modifiche stanno interessando altre parti del sistema e ti consentiamo di metterti in posizione per apportare le modifiche necessarie prima.


3

Ri: "Il codice originale potrebbe non funzionare correttamente":

I test non sono scritti in pietra. Possono essere cambiati. E se hai testato per una funzionalità che era sbagliata, dovrebbe essere facile riscrivere il test più correttamente. Dopo tutto, solo il risultato atteso della funzione testata dovrebbe essere cambiato.


1
IMO, i singoli test dovrebbero essere scritti in pietra, almeno fino a quando la funzionalità che stanno testando è morta. Sono ciò che verifica il comportamento del sistema esistente e aiuta a garantire ai manutentori che le loro modifiche non romperanno il codice legacy che potrebbe già fare affidamento su quel comportamento. Modifica i test per una funzione live e stai rimuovendo tali assicurazioni.
cHao,

3

Beh si. Rispondere come ingegnere di test del software. Innanzitutto dovresti testare tutto ciò che fai comunque. Perché se non lo fai, non sai se funziona o no. Questo può sembrare ovvio per noi, ma ho colleghi che la vedono diversamente. Anche se il tuo progetto è un progetto che potrebbe non essere mai consegnato, devi guardare l'utente in faccia e dire che sai che funziona perché l'hai testato.

Il codice non banale contiene sempre bug (citando un ragazzo da uni; e se non ci sono bug, è banale) e il nostro compito è quello di trovarli prima del cliente. Il codice legacy presenta bug legacy. Se il codice originale non funziona come dovrebbe, vuoi conoscerlo, credimi. I bug sono ok se li conosci, non aver paura di trovarli, ecco a cosa servono le note di rilascio.

Se ricordo giustamente che il libro Refactoring dice di testare costantemente, quindi fa parte del processo.


3

Esegui la copertura di test automatizzata.

Fai attenzione al desiderio, sia tuo che dei tuoi clienti e capi. Per quanto mi piacerebbe credere che i miei cambiamenti saranno corretti la prima volta e dovrò testarlo solo una volta, ho imparato a trattare quel tipo di pensiero nello stesso modo in cui tratto le e-mail truffe nigeriane. Bene, principalmente; Non ho mai cercato un'e-mail di truffa ma di recente (quando urlato a) ho rinunciato a non usare le migliori pratiche. È stata un'esperienza dolorosa che si trascinava (a caro prezzo) all'infinito. Mai più!

Ho una citazione preferita del fumetto di Freefall: "Hai mai lavorato in un campo complesso in cui il supervisore ha solo una vaga idea dei dettagli tecnici? ... Allora sai che il modo più sicuro per far fallire il tuo supervisore è quello di segui ogni suo ordine senza dubbio ".

Probabilmente è appropriato limitare il tempo investito.


1

Se hai a che fare con grandi quantità di codice legacy che non è attualmente in fase di test, ottenere la copertura del test ora invece di aspettare un'ipotetica grande riscrittura in futuro è la mossa giusta. Iniziare scrivendo unit test non lo è.

Senza test automatizzati, dopo aver apportato modifiche al codice è necessario eseguire alcuni test manuali end-to-end dell'app per assicurarsi che funzioni. Inizia scrivendo test di integrazione di alto livello per sostituirlo. Se la tua app legge i file, li convalida, elabora i dati in qualche modo e visualizza i risultati desiderati per i test che catturano tutto ciò.

Idealmente, avrai i dati di un piano di test manuale o sarai in grado di ottenere un campione di dati di produzione effettivi da utilizzare. Altrimenti, poiché l'app è in produzione, nella maggior parte dei casi sta facendo quello che dovrebbe essere, quindi basta inventare dati che colpiranno tutti i punti alti e supporre che l'output sia corretto per ora. Non è peggio che assumere una piccola funzione, supporre che stia facendo quello che è il suo nome o qualsiasi commento suggerisca che dovrebbe fare, e scrivere test supponendo che funzioni correttamente.

IntegrationTestCase1()
{
    var input = ReadDataFile("path\to\test\data\case1in.ext");
    bool validInput = ValidateData(input);
    Assert.IsTrue(validInput);

    var processedData = ProcessData(input);
    Assert.AreEqual(0, processedData.Errors.Count);

    bool writeError = WriteFile(processedData, "temp\file.ext");
    Assert.IsFalse(writeError);

    bool filesAreEqual = CompareFiles("temp\file.ext", "path\to\test\data\case1out.ext");
    Assert.IsTrue(filesAreEqual);
}

Una volta che hai abbastanza di questi test di alto livello scritti per catturare il normale funzionamento delle app e i casi di errore più comuni la quantità di tempo che dovrai spendere martellando sulla tastiera per provare a catturare errori dal codice facendo qualcosa di diverso da ciò che pensavi che avrebbe dovuto scendere in modo significativo, rendendo molto più semplice il futuro refactoring (o anche una grande riscrittura).

Dato che sei in grado di espandere la copertura dei test unitari, puoi ridurre o addirittura ritirare la maggior parte dei test di integrazione. Se la tua app sta leggendo / scrivendo file o accedendo a un DB, testare quelle parti in modo isolato e deriderle o far iniziare i test creando le strutture dati lette dal file / database sono un punto ovvio da cui iniziare. In realtà la creazione di tale infrastruttura di test richiederà molto più tempo rispetto alla scrittura di una serie di test rapidi e sporchi; e ogni volta che esegui una serie di test di integrazione di 2 minuti invece di spendere 30 minuti per testare manualmente una frazione di ciò che i test di integrazione hanno coperto, stai già facendo una grande vittoria.

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.