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

72

Di solito provo a seguire il consiglio del libro Lavorare efficacemente con il legacy cod e . Rompere le dipendenze, spostare parti del codice in metodi @VisibleForTesting public static e nuove classi per rendere testabile il codice (o almeno una parte di esso). E scrivo test per assicurarmi di non rompere nulla quando sto modificando o aggiungendo 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 correzioni e le modifiche future più difficili da quando gli sviluppatori devono comprendere e modificare anche i test.
  • Se si tratta di codice GUI con qualche logica (~ 12 righe, 2-3 se / else blocco, ad esempio), un test non vale la pena dato che il codice è troppo banale per cominciare.
  • Simili schemi negativi potrebbero esistere anche in altre parti del codice base (che non ho ancora visto, sono piuttosto nuovo); sarà più facile pulirli tutti in un unico grande refactoring. Estrarre la logica potrebbe minare questa possibilità futura.

Dovrei evitare di estrarre parti testabili e test di scrittura se non avremo tempo per il completo refactoring? C'è qualche svantaggio a questo che dovrei prendere in considerazione?

    
posta is4 06.02.2014 - 08:15
fonte

10 risposte

100

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

  1. Certo, il codice esistente potrebbe essere sbagliato. Potrebbe anche essere giusto. Dal momento che l'applicazione nel suo insieme sembra avere valore per te (altrimenti lo scarti semplicemente), in assenza di informazioni più specifiche dovresti assumere che sia prevalentemente giusto. "Scrivere test rende le cose più difficili perché c'è più codice coinvolto nel complesso" è un atteggiamento semplicistico e molto sbagliato.
  2. Con tutti i mezzi spendere i vostri sforzi di refactoring, test e miglioramento nei luoghi in cui aggiungono il massimo valore con il minimo sforzo. Le subroutine della GUI con 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 sono commessi perché le persone pensavano di aver capito qualcosa di meglio di quello che effettivamente hanno fatto.
  3. "Faremo tutto in un colpo solo in futuro" è un bel pensiero. Di solito il grande colpo rimane saldamente in futuro, mentre nel presente non succede nulla. Io, sono fermamente convinto della convinzione "lento e costante vince la razza".
risposta data 06.02.2014 - 08:28
fonte
50

Alcuni pensieri:

Quando si esegue il refactoring del codice legacy, non importa se alcuni dei test che si scrivono sono in contraddizione con le specifiche ideali. Ciò che importa è che testano il comportamento corrente del programma. Il refactoring consiste nel prendere piccoli passaggi iso-funzionali per rendere il codice più pulito; non vuoi impegnarti nella correzione dei bug mentre esegui il refactoring. Inoltre, se noti uno sfacciato insulto, non andrà perso. Puoi sempre scrivere un test di regressione e disattivarlo temporaneamente, oppure inserire un'attività di bugfix nel backlog per dopo. Una cosa alla volta.

Sono d'accordo sul fatto che il codice della pura GUI sia difficile da testare e forse non è adatto per il refactoring " Lavorare in modo efficace ... ". Tuttavia, questo non significa che non si debba estrarre un comportamento che non ha nulla da fare nel livello della GUI e testare il codice estratto. E "12 linee, 2-3 se / altro blocco" non è banale. Tutto il codice con almeno un po 'di logica condizionale dovrebbe essere testato.

Nella mia esperienza, i grandi rifacimenti non sono facili e raramente funzionano. Se non ti definisci obiettivi precisi e minuscoli, c'è un alto rischio che ti imbarchi in un'incessante rilavorazione per strappare i capelli, dove alla fine non ti atterri mai. Più grande è il cambiamento, più rischi di rompere qualcosa e più problemi avrai a scoprire dove hai fallito.

Rendere le cose progressivamente migliori con i piccoli refactoring ad hoc non sta "minando le possibilità future", ma consente loro di consolidare il terreno paludoso in cui si trova l'applicazione. Dovresti assolutamente farlo.

    
risposta data 06.02.2014 - 12:38
fonte
17

Inoltre: "Il codice originale potrebbe non funzionare correttamente" - questo non significa che cambi semplicemente il comportamento del codice senza preoccuparti dell'impatto. Altro codice può fare affidamento su ciò che sembra essere un comportamento interrotto o effetti collaterali dell'attuale implementazione. La copertura di prova dell'applicazione esistente dovrebbe rendere più facile il refactoring in un secondo momento, perché ti aiuterà a scoprire quando hai violato per errore qualcosa. Dovresti prima testare le parti più importanti.

    
risposta data 06.02.2014 - 12:01
fonte
14

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

Se uno sviluppatore vuole cambiare codice (refactor, extend, debug), deve capirlo. Deve assicurarsi che i suoi cambiamenti influenzino esattamente il comportamento che vuole (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 i test sono molto più facili da capire rispetto al codice funzionale (a meno che non siano test non validi). E i test aiutano a mostrare cosa è cambiato nel comportamento del vecchio codice. Anche se il codice originale è sbagliato e il test verifica un comportamento errato, questo è ancora un vantaggio.

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

Alcune riflessioni sul punto 3: oltre al fatto che il "big swoop" accade raramente in realtà, c'è anche un'altra cosa: non è in realtà più semplice. Per essere più semplice, dovrebbero essere applicate diverse condizioni:

  • L'antipattern da refactoring deve essere facilmente trovato. Tutti i tuoi singleton sono chiamati XYZSingleton ? Il loro getter di istanze è sempre chiamato getInstance() ? E come trovi le tue gerarchie troppo profonde? Come cerchi i tuoi oggetti di dio? Questi richiedono l'analisi della metrica del codice e quindi l'ispezione manuale delle metriche. Oppure inciampi su di loro mentre lavori, come hai fatto tu.
  • Il refactoring deve essere meccanico. Nella maggior parte dei casi, la parte più difficile del refactoring è la comprensione del codice esistente abbastanza bene da sapere come modificarlo. Singletons di nuovo: se il singleton è andato, come si ottiene l'informazione richiesta ai suoi utenti? Significa spesso capire il callgraph locale in modo da sapere da dove ottenere le informazioni. Ora, cosa è più semplice: cercare i dieci singleton nella tua app, comprenderne gli usi (il che porta a dover capire il 60% della base di codice) e strapparli? O prendendo il codice che hai già capito (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 è possibile utilizzarlo per raggrupparlo.
  • Il refactoring deve essere automatizzato. Questo è un po 'basato sull'opinione pubblica, ma qui va. Un po 'di refactoring è divertente e soddisfacente. Un sacco di refactoring è noioso e noioso. Lasciare il pezzo di codice su cui hai appena lavorato in uno stato migliore ti dà una sensazione piacevole e calda, prima di passare a cose più interessanti. Cercando di ridefinire un intero codice base ti sentirai frustrato e arrabbiato con i programmatori idioti che lo hanno scritto. Se vuoi fare un grande refactoring in picchiata, allora deve essere ampiamente automatizzato in modo da minimizzare la frustrazione. Questa è, in un certo senso, una combinazione dei primi due punti: puoi automatizzare il refactoring solo se puoi automatizzare la ricerca del codice errato (cioè facilmente reperibile) e automatizzarlo (cioè meccanico)
  • Il miglioramento graduale rappresenta un caso aziendale migliore. Il grande refactoring in picchiata è incredibilmente distruttivo. Se si refactoring un pezzo di codice, invariabilmente entrare in conflitti di fusione con altre persone che lavorano su di esso, perché basta dividere il metodo che stavano cambiando in cinque parti. Quando si refactoring un pezzo di codice di dimensioni ragionevoli, si ottiene conflitti con alcune persone (1-2 quando si divide la megafase 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 fa sì che ogni modifica del codice richieda un po 'più tempo. Ciò lo rende più prevedibile e non c'è un periodo di tempo così visibile quando non accade nulla tranne la pulizia.
risposta data 06.02.2014 - 11:47
fonte
12

In alcune aziende esiste una cultura in cui sono reticenti a consentire agli sviluppatori in qualsiasi momento di migliorare il codice che non fornisce direttamente un valore aggiuntivo, ad es. nuova funzionalità.

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

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

Detto questo, il software soffre di entropia e aumenta il debito tecnico. Gli sviluppatori precedenti poco tempo (o forse solo pigri o inesperti) potrebbero aver implementato soluzioni buggy non ottimali rispetto a quelli ben progettati. Sebbene possa sembrare desiderabile il refactoring di questi, rischi di introdurre nuovi bug in quello che è (almeno per gli utenti) codice di lavoro.

Alcune modifiche sono a rischio inferiore rispetto ad altre. Ad esempio, dove lavoro ci sono molti codici duplicati che possono essere tranquillamente raccolti in una subroutine con un impatto minimo.

In ultima analisi, devi fare una valutazione per quanto riguarda il processo di refactoring, ma è innegabile che aggiunga test automatici se non esistono già.

    
risposta data 06.02.2014 - 10:17
fonte
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.

Quindi scriverò i test unitari per le parti che si desidera modificare e lo fare 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 interessano altre parti del sistema e ti consentiamo di entrare in una posizione tale da apportare prima le modifiche richieste.

    
risposta data 07.02.2014 - 11:17
fonte
3

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

I test non sono scritti in pietra. Possono essere cambiati. E se hai provato 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.

    
risposta data 06.02.2014 - 10:15
fonte
3

Bene, si. Risposta come ingegnere di test del software. In primo luogo 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 lo vedono in modo diverso. Anche se il tuo progetto è un po 'che non può mai essere consegnato, devi guardare in faccia l'utente e dire che sai che funziona perché lo hai testato.

Il codice non banale contiene sempre bug (citando un ragazzo di uni e se non ci sono bug in esso, è banale) e il nostro compito è trovarli prima che il cliente lo faccia. Il codice legacy ha bug legacy. Se il codice originale non funziona come dovrebbe, vuoi saperlo, credimi. I bug sono ok se li conosci, non aver paura di trovarli, ecco a cosa servono le note di rilascio.

Se ricordo bene, il libro Refactoring dice di testare costantemente comunque. Quindi fa parte del processo.

    
risposta data 06.02.2014 - 16:37
fonte
3

Esegui la copertura del test automatico.

Fai attenzione ai desideri pieni di desiderio, sia i tuoi che i tuoi clienti e i tuoi capi. Per quanto mi piacerebbe credere che i miei cambiamenti saranno corretti la prima volta e dovrò testare solo una volta, ho imparato a trattare questo tipo di pensiero nello stesso modo in cui trattiamo le e-mail truffaldine nigeriane. Bene, principalmente; Non sono mai andato per una email truffa ma di recente (quando ho urlato) mi sono arreso per non aver usato le migliori pratiche. È stata un'esperienza dolorosa che ha trascinato (costoso) avanti e indietro. Mai più!

Ho una citazione preferita dal fumetto di Freefall: "Hai mai lavorato in un campo complesso in cui il supervisore ha solo un'idea approssimativa dei dettagli tecnici? ... Allora conosci il modo più sicuro per far sì che il tuo supervisore fallire è seguire ogni suo ordine senza domande. "

Probabilmente è opportuno limitare il tempo investito.

    
risposta data 07.02.2014 - 07:46
fonte
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 è.

Senza test automatici, dopo aver apportato eventuali modifiche al codice, è necessario eseguire alcuni test end-end manuali dell'applicazione per accertarsi 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 che desideri test che catturino tutto ciò.

Idealmente avrai dati da un piano di test manuale o sarai in grado di ottenere un campione di dati di produzione reali da utilizzare. In caso contrario, dal momento che l'app è in produzione, nella maggior parte dei casi sta facendo quello che dovrebbe essere, quindi basta creare dati che colpiranno tutti i punti più alti e supponiamo che l'output sia corretto per ora. Non è peggio che prendere una piccola funzione, supponendo che stia facendo quello che è il suo nome o qualsiasi commento suggerisca che dovrebbe fare, e scrivendo 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 preso 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 sulla tastiera per cercare di rilevare errori dal codice facendo qualcosa a parte ciò che pensavate che avrebbe dovuto fare andrebbe in modo significativo, rendendo molto più facile il futuro refactoring (o anche una grande riscrittura).

Dato che puoi espandere la copertura del test dell'unità, puoi ridurre o addirittura ritirare la maggior parte dei test di integrazione. Se i file di lettura / scrittura della tua app o l'accesso a un DB, testare quelle parti in isolamento e deriderle o avviare i test creando le strutture di dati lette dal file / database sono un punto di partenza ovvio. La creazione effettiva di quell'infrastruttura di test richiederà molto più tempo rispetto alla scrittura di una serie di test rapidi e sporchi; e ogni volta che esegui un test di integrazione di 2 minuti invece di spendere 30 minuti manualmente testando una frazione di ciò che i test di integrazione hanno coperto, stai già ottenendo un grosso successo.

    
risposta data 07.06.2014 - 19:26
fonte

Leggi altre domande sui tag