Cosa aiuterebbe nel refactoring di un metodo di grandi dimensioni per garantire che non rompa nulla?


10

Attualmente sto eseguendo il refactoring di una parte di una base di codice di grandi dimensioni senza test unitari. Ho cercato di refactificare il codice in modo brutale, cioè cercando di indovinare cosa sta facendo il codice e quali cambiamenti non cambierebbero il suo significato, ma senza successo: interrompe casualmente le funzionalità intorno alla base di codice.

Si noti che il refactoring include lo spostamento del codice C # legacy in uno stile più funzionale (il codice legacy non utilizza nessuna delle funzionalità di .NET Framework 3 e versioni successive, incluso LINQ), aggiungendo generici in cui il codice può trarne beneficio, ecc.

Non posso usare metodi formali , dato quanto costerebbero.

D'altra parte, presumo che almeno la regola "Qualsiasi codice legacy refactored verrà fornito con unit test" dovrebbe essere seguita rigorosamente, indipendentemente da quanto costerebbe. Il problema è che quando refactoring una piccola parte di un metodo privato 500 LOC, l'aggiunta di unit test sembra essere un compito difficile.

Cosa può aiutarmi a sapere quali unit test sono rilevanti per un dato codice? Immagino che l'analisi statica del codice sarebbe in qualche modo utile, ma quali sono gli strumenti e le tecniche che posso usare per:

  • Sapere esattamente quali test unitari dovrei creare,

  • E / o sai se la modifica che ho apportato ha influito sul codice originale in un modo che sta eseguendo in modo diverso da ora?


Qual è il tuo ragionamento secondo cui scrivere unità di test aumenterà il tempo per questo progetto? Molti sostenitori non sarebbero d'accordo, ma dipende anche dalla tua capacità di scriverli.
JeffO,

Non dico che aumenterà il tempo complessivo per il progetto. Quello che volevo dire è che aumenterà il tempo a breve termine (vale a dire il tempo immediato che trascorro subito quando refactoring il codice).
Arseni Mourzenko,

1
Non vorrai formal methods in software developmentcomunque usarlo perché è usato per dimostrare la correttezza di un programma usando la logica predicata e non avrebbe applicabilità al refactoring di una base di codice di grandi dimensioni. Metodi formali generalmente utilizzati per dimostrare che il codice funziona correttamente in aree come le applicazioni mediche. Hai ragione, è costoso fare ed è per questo che non viene usato spesso.
Mushy,

Un buon strumento come le opzioni di refactoring in ReSharper rendono questo compito molto più semplice. In situazioni come questa vale la pena .
billy.bob,

1
Non una risposta completa ma una tecnica stupida che trovo sorprendentemente efficace quando tutte le altre tecniche di refactoring mi stanno fallendo: creare una nuova classe, suddividere la funzione in funzioni separate con esattamente il codice già presente, appena interrotto ogni 50 o più righe, promuovere qualsiasi i locali che sono condivisi tra le funzioni ai membri, quindi le singole funzioni si adattano meglio alla mia testa e mi danno la possibilità di vedere nei membri quali pezzi sono fatti passare attraverso l'intera logica. Questo non è un obiettivo finale, solo un modo sicuro per ottenere un pasticcio legacy per essere pronti per il refactoring in sicurezza.
Jimmy Hoffa,

Risposte:


12

Ho avuto sfide simili. Il libro Working with Legacy Code è una grande risorsa, ma si presume che sia possibile usare il clacson nei test unitari per supportare il proprio lavoro. A volte non è possibile.

Nel mio lavoro di archeologia (il mio termine per la manutenzione di codice legacy come questo), seguo un approccio simile a quello che hai delineato.

  • Inizia con una solida comprensione di ciò che la routine sta attualmente facendo.
  • Allo stesso tempo, identifica cosa avrebbe dovuto fare la routine . Molti pensano che questo proiettile e il precedente siano gli stessi, ma c'è una sottile differenza. Spesso, se la routine stesse facendo quello che doveva fare, non si applicano le modifiche di manutenzione.
  • Esegui alcuni esempi attraverso la routine e assicurati di colpire i casi limite, i percorsi di errore rilevanti e il percorso della linea principale. La mia esperienza è che il danno collaterale (rottura di funzionalità) deriva da condizioni al contorno non implementate esattamente allo stesso modo.
  • Dopo questi casi di esempio, identifica ciò che è persistente che non deve necessariamente essere persistito. Ancora una volta, ho scoperto che sono gli effetti collaterali come questo che portano a danni collaterali altrove.

A questo punto, dovresti avere un elenco di candidati di ciò che è stato esposto e / o manipolato da quella routine. È probabile che alcune di queste manipolazioni siano involontarie. Ora utilizzo findstrIDE per capire quali altre aree potrebbero fare riferimento agli elementi nell'elenco candidati. Trascorrerò un po 'di tempo a capire come funzionano quei riferimenti e qual è la loro natura.

Infine, una volta che mi sono illuso nel pensare di aver compreso gli impatti della routine originale, apporterò le mie modifiche una alla volta e eseguirò nuovamente i passaggi di analisi che ho delineato sopra per verificare che il cambiamento funzioni come mi aspetto per funzionare. In particolare, cerco di evitare di cambiare più cose contemporaneamente poiché ho scoperto che questo mi esplode quando provo a verificare l'impatto. A volte puoi fare a meno di più modifiche, ma se posso seguire un percorso alla volta, questa è la mia preferenza.

In breve, il mio approccio è simile a quello che hai esposto. È un sacco di lavoro di preparazione; poi fai dei cambiamenti individuali e avveduti; e quindi verificare, verificare, verificare.


2
+1 solo per l'uso dell'archeologia. È lo stesso termine che uso per descrivere questa attività e penso che sia un ottimo modo per dirla (anche se la risposta era buona - non sono poi così superficiale)
Erik Dietrich,

10

Cosa aiuterebbe nel refactoring di un metodo di grandi dimensioni per garantire che non rompa nulla?

Risposta breve: piccoli passi.

Il problema è che quando refactoring una piccola parte di un metodo privato 500 LOC, l'aggiunta di unit test sembra essere un compito difficile.

Considera questi passaggi:

  1. Spostare l'implementazione in un'altra funzione (privata) e delegare la chiamata.

    // old:
    private int ugly500loc(int parameters) {
        // 500 LOC here
    }
    
    // new:    
    private int ugly500loc_old(int parameters) {
        // 500 LOC here
    }
    
    private void ugly500loc(int parameters) {
        return ugly500loc_old(parameters);
    }
    
  2. Aggiungi il codice di registrazione (assicurati che la registrazione non fallisca) nella tua funzione originale, per tutti gli ingressi e le uscite.

    private void ugly500loc(int parameters) {
        static int call_count = 0;
        int current = ++call_count;
        save_to_file(current, parameters);
        int result = ugly500loc_old(parameters);
        save_to_file(current, result); // result, any exceptions, etc.
        return result;
    }
    

    Esegui l'applicazione e fai tutto il possibile (uso valido, uso non valido, uso tipico, uso atipico, ecc.).

  3. Ora hai max(call_count)set di input e output con cui scrivere i tuoi test; Puoi scrivere un singolo test che scorre su tutti i tuoi parametri / set di risultati che hai e li esegue in un ciclo. È inoltre possibile scrivere un test adizionale che esegue una particolare combinazione (da utilizzare per verificare rapidamente il passaggio su un determinato set di I / O).

  4. Spostare // 500 LOC herenuovamente dentro la vostra ugly500locfunzione (e rimuovere funzionalità di registrazione).

  5. Inizia ad estrarre funzioni dalla grande funzione (non fare nient'altro, basta estrarre funzioni) ed eseguire i test. Dopodiché dovresti avere più piccole funzioni per il refactoring, invece di 500LOC.

  6. Vivi felici e contenti.


3

Di solito i test unitari sono la strada da percorrere.

Fai i test necessari per dimostrare che la corrente funziona come previsto. Prenditi il ​​tuo tempo e l'ultimo test deve renderti sicuro dell'output.

Cosa può aiutarmi a sapere quali unit test sono rilevanti per un dato codice?

Stai eseguendo il refactoring di un pezzo di codice, devi sapere esattamente cosa fa e che impatto ha. Quindi in pratica devi testare tutte le zone interessate. Questo richiederà molto tempo ... ma è un risultato atteso da qualsiasi processo di refactoring.

Quindi puoi fare a pezzi tutto senza problemi.

AFAIK, non esiste una tecnica a prova di proiettile per questo ... devi solo essere metodico (su qualunque metodo ti senti a tuo agio), molto tempo e molta pazienza! :)

Saluti e buona fortuna!

alex


Gli strumenti di copertura del codice sono essenziali qui. Confermare che hai coperto ogni percorso attraverso un metodo complesso e ampio tramite ispezione è difficile. Uno strumento che mostra collettivamente KitchenSinkMethodTest01 () ... KitchenSinkMethodTest17 () copre le linee 1-45, 48-220, 245-399 e 488-500 ma non tocca il codice tra; farà capire quali test aggiuntivi sono necessari per scrivere molto più semplice.
Dan Fiddling By Firelight,
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.