Perché scrivere test per il codice che refactoring?

15

Sto refactoring una grande classe di codice legacy. Refactoring (presumo) sostiene questo:

  1. scrivi test per la classe precedente
  2. rifatta il diavolo dalla classe

Problema: una volta effettuato il refactoring della classe, i miei test nel passaggio 1 dovranno essere modificati. Ad esempio, ciò che una volta era in un metodo legacy, ora potrebbe essere una classe separata. Qual era un metodo potrebbe essere ora diversi metodi. L'intero panorama della classe precedente può essere cancellato in qualcosa di nuovo, e quindi i test che scrivo nel passaggio 1 saranno quasi nulli. In sostanza aggiungerò Step 3. riscriviamo i miei test in modo profuso

Qual è lo scopo di scrivere test prima del refactoring? Sembra più un esercizio accademico di creare più lavoro per me stesso. Sto scrivendo i test per il metodo ora e sto imparando di più su come testare le cose e come funziona il metodo legacy. Si può imparare questo semplicemente leggendo il codice legacy stesso, ma scrivere test è quasi come sfiorarmi il naso e anche documentare questa conoscenza temporanea in test separati. Quindi in questo modo quasi non ho altra scelta che apprendere cosa sta facendo il codice. Ho detto temporaneamente qui, perché rifarò il diavolo dal codice e tutta la mia documentazione e le prove saranno nulle per una parte significativa, tranne la mia conoscenza rimarrà e mi consentirà di essere più fresco sul refactoring.

È questa la vera ragione per scrivere test prima di refactoring - per aiutarmi a capire meglio il codice? Deve esserci un'altra ragione!

Per favore, spiega!

Nota:

C'è questo post: Ha senso scrivere test per il codice legacy quando non c'è tempo per un completo refactoring? ma si dice" scrivi test prima del refactoring ", ma doesn dire "perché", o cosa fare se "scrivere test" sembra "un lavoro impegnativo che verrà presto distrutto"

    
posta Dennis 21.03.2014 - 19:24
fonte

5 risposte

45

Il refactoring sta pulendo un pezzo di codice (ad esempio migliorando lo stile, il design o gli algoritmi), senza modificare il comportamento (visibile esternamente). Si scrivono test per assicurarsi che il codice prima e dopo aver rifatto sia lo stesso, invece si scrivono i test come un indicatore che l'applicazione prima e dopo il refactoring si comporta lo stesso: Il nuovo codice è compatibile e non sono stati introdotti nuovi bug.

La tua preoccupazione principale dovrebbe essere quella di scrivere test unitari per l'interfaccia pubblica del tuo software. Questa interfaccia non dovrebbe cambiare, quindi i test (che sono un controllo automatico per questa interfaccia) non dovrebbero cambiare neanche.

Tuttavia, i test sono anche utili per individuare gli errori, quindi può avere senso scrivere test per parti private del software. Questi test dovrebbero cambiare durante il refactoring. Se desideri modificare un dettaglio di implementazione (ad esempio la denominazione di una funzione privata), devi prima aggiornare i test per riflettere le tue aspettative modificate, quindi assicurarti che il test non abbia successo (le tue aspettative non sono soddisfatte), quindi cambi il codice effettivo e controllare che tutti i test passino di nuovo. In nessun caso i test per l'interfaccia pubblica iniziano a fallire.

Questo è più difficile quando si eseguono modifiche su una scala più grande, ad es. ridisegnare più parti codipendenti. Ma ci sarà un qualche tipo di limite, e a quel confine sarai in grado di scrivere dei test.

    
risposta data 21.03.2014 - 20:10
fonte
7

Ah, mantenendo i sistemi legacy.

Idealmente i tuoi test trattano la classe solo attraverso la sua interfaccia con il resto della base di codice, altri sistemi e / o l'interfaccia utente. Interfacce. Non è possibile ridefinire l'interfaccia senza influire sui componenti upstream o downstream. Se è tutto un casino strettamente accoppiato allora si potrebbe anche considerare lo sforzo una riscrittura piuttosto che un refactoring, ma è in gran parte semantica.

Modifica: Diciamo che parte del tuo codice misura qualcosa e ha una funzione che restituisce semplicemente un valore. L'unica interfaccia chiama la funzione / metodo / whatnot e riceve il valore restituito. Questo è un accoppiamento lento e facile da testare. Se il tuo programma principale ha un sottocomponente che gestisce un buffer, e tutte le chiamate ad esso dipendono dal buffer stesso, alcune variabili di controllo e rimbalza i messaggi di errore attraverso un'altra sezione di codice, allora potresti dire che è strettamente accoppiato ed è difficile test unitario. Puoi ancora farlo con una quantità sufficiente di oggetti finti e quant'altro, ma diventa disordinato. Soprattutto in c. Qualsiasi quantità di refactoring su come funziona il buffer interromperà il sottocomponente.
Modifica fine

Se stai testando la tua classe tramite interfacce che rimangono stabili, i tuoi test dovrebbero essere validi prima e dopo il refactoring. Ciò ti consente di apportare modifiche con la certezza di non averlo infranto. Almeno, più sicurezza.

Ti consente anche di apportare modifiche incrementali. Se questo è un grande progetto, non penso che vorrete semplicemente distruggere tutto, costruire un nuovo sistema e quindi iniziare a sviluppare test. Puoi cambiarne una parte, testarla e assicurarti che la modifica non porti giù il resto del sistema. Oppure, se lo fa, puoi almeno vedere il gigantesco groviglio aggrovigliato svilupparsi piuttosto che sorprenderlo quando lo rilasci.

Anche se potresti dividere un metodo in tre, faranno comunque la stessa cosa del metodo precedente, quindi puoi eseguire il test per il vecchio metodo e dividerlo in tre. Lo sforzo di scrivere il primo test non è sprecato.

Inoltre, trattare la conoscenza del sistema legacy come "conoscenza temporanea" non andrà bene. Sapere come è stato fatto in precedenza è fondamentale quando si tratta di sistemi legacy. Molto utile per la vecchia questione di "perché diavolo fa?"

    
risposta data 21.03.2014 - 19:54
fonte
4

La mia risposta / realizzazione:

Dalla correzione di vari errori durante il refactoring mi sto rendendo conto che non avrei fatto il codice spostato facilmente senza test. I test mi avvisano delle "diffs" comportamentali / funzionali che presento cambiando il mio codice.

Non devi essere iperconscio quando hai dei buoni test in atto. Puoi modificare il tuo codice in un modo più rilassato. I test eseguono i controlli di verifica e integrità per te.

Inoltre, i miei test sono rimasti più o meno come se avessi refactato e non fossero stati distrutti. Ho notato alcune opportunità in più per aggiungere asserzioni ai miei test mentre approfondivo il codice.

Aggiorna

Bene, ora sto cambiando molto i miei test: / Perché ho rifattorizzato la funzione originale (rimosso la funzione e creato invece una nuova classe cleaner, spostando la lanuggine che prima era all'interno della nuova classe) , quindi ora il codice sotto test che ho eseguito prima prende diversi parametri sotto un nome di classe diverso e produce risultati diversi (il codice originale con il lanugine aveva più risultati da testare). Quindi i miei test devono riflettere questi cambiamenti e fondamentalmente sto riscrivendo i miei test in qualcosa di nuovo.

Suppongo che ci siano altre soluzioni che posso fare per evitare di riscrivere i test. vale a dire mantenere il vecchio nome della funzione con il nuovo codice e il fluff all'interno di esso ... ma non so se è la migliore idea e non ho ancora molta esperienza per fare una sentenza su cosa fare.

    
risposta data 01.04.2014 - 18:01
fonte
3

Usa i tuoi test per guidare il tuo codice mentre lo fai. Nel codice legacy questo significa scrivere test per il codice che si intende modificare. In questo modo non sono un artefatto separato. I test dovrebbero riguardare ciò che il codice deve raggiungere e non le budella interne di come lo fa.

Generalmente vuoi aggiungere test sul codice che non ne contiene nessuno) per il codice che stai per refactoring per assicurarti che il comportamento dei codici continui a funzionare come previsto. Quindi, eseguire continuamente la suite di test mentre il refactoring è una fantastica rete di sicurezza. L'idea di cambiare codice senza una suite di test per confermare che le modifiche non influiscono su qualcosa di inaspettato è spaventoso.

Per quanto riguarda il nocciolo duro dell'aggiornamento dei vecchi test, la scrittura di nuovi test, l'eliminazione di vecchi test, ecc. L'ho visto solo come parte del costo del moderno sviluppo di software professionale.

    
risposta data 21.03.2014 - 20:13
fonte
1

Qual è l'obiettivo del refactoring nel tuo caso specifico?

Presume allo scopo di sopportare la mia risposta che tutti noi crediamo (in una certa misura) nel TDD (Test-Driven Development).

Se lo scopo del refactoring è ripulire il codice esistente senza modificare il comportamento esistente, la scrittura di test prima del refactoring è il modo in cui si garantisce di non aver modificato il comportamento del codice, se si riesce, i test avranno esito positivo entrambi prima e dopo il refactoring.

  • I test ti aiuteranno a garantire che il tuo nuovo lavoro funzioni effettivamente.

  • I test probabilmente scopriranno anche casi in cui il lavoro originale non funziona.

Ma come si fa davvero qualsiasi refactoring significativo senza influenzare il comportamento a alcuni gradi?

Ecco un breve elenco di alcune cose che potrebbero accadere durante il refactoring:

  • rinomina variabile
  • rinomina funzione
  • aggiungi funzione
  • elimina la funzione
  • divide la funzione in due o più funzioni
  • combina due o più funzioni in una funzione
  • divisione classe
  • combina classi
  • rinomina classe

Ho intenzione di sostenere che ognuna di quelle attività elencate cambia comportamento in qualche modo.

E ho intenzione di sostenere che se il tuo refactoring cambia comportamento, i tuoi test saranno ancora come assicurarti di non aver infranto nulla.

Forse il comportamento non cambia a livello macro, ma il punto del test unità non è quello di garantire un comportamento macro. Questo è integration testing. Il punto di test unitario è quello di garantire che i singoli pezzi e pezzi da cui si crea il tuo prodotto non siano rotti. Catena, collegamento più debole, ecc.

Che ne dici di questo scenario:

  • Presume che tu abbia function bar()

  • function foo() effettua una chiamata a bar()

  • function flee() effettua anche una chiamata alla funzione bar()

  • Solo per varietà, flam() effettua una chiamata a foo()

  • Tutto funziona splendidamente (almeno apparentemente).

  • Refactor ...

  • bar() è rinominato in barista()

  • flee() è cambiato per chiamare barista()

  • foo() è non modificato per chiamare barista()

Ovviamente, i tuoi test per foo() e flam() ora falliscono.

Forse non hai realizzato foo() chiamato bar() in primo luogo. Sicuramente non ti sei reso conto che flam() dipendeva da bar() per foo() .

Qualunque cosa. Il punto è che i tuoi test scopriranno il comportamento appena rotto sia di foo() che di flam() , in modo incrementale durante il tuo lavoro di refactoring.

I test finiscono per aiutarti a refactoring

A meno che tu non abbia alcun test.

Questo è un esempio un po 'forzato. Ci sono quelli che sostengono che se cambiando bar() rompe foo() , allora foo() era troppo complesso per cominciare e dovrebbe essere suddiviso. Ma le procedure sono in grado di chiamare altre procedure per un motivo ed è impossibile eliminare la complessità all , giusto? Il nostro compito è gestire la complessità ragionevolmente bene.

Considera un altro scenario.

Stai costruendo un edificio.

Costruisci un'impalcatura per assicurarti che l'edificio sia costruito correttamente.

L'impalcatura ti aiuta a costruire un pozzo dell'ascensore, tra le altre cose. In seguito, abbatti l'impalcatura, ma il pozzo dell'ascensore rimane. Hai distrutto il "lavoro originale" distruggendo l'impalcatura.

L'analogia è tenue, ma il punto è che non è inaudito costruire strumenti per aiutarti a costruire un prodotto. Anche se gli strumenti non sono permanenti, sono utili (anche necessari). I carpentieri fanno sempre delle maschere, a volte solo per un lavoro. Poi fanno a pezzi le maschere, a volte usando le parti per costruire altre maschere per altri lavori, a volte no. Ma questo non rende le maschere inutili o inutili.

    
risposta data 22.03.2014 - 01:22
fonte

Leggi altre domande sui tag