Refactoring: è appropriato semplicemente riscrivere il codice, a condizione che tutti i test superino?

9

Di recente ho guardato "Tutte le piccole cose" di RailsConf 2014. Durante questo discorso, Sandi Metz ha funzione che include una grande istruzione if annidata:

def tick
    if @name != 'Aged Brie' && @name != 'Backstage passes to a TAFKAL80ETC concert'
        if @quality > 0
            if @name != 'Sulfuras, Hand of Ragnaros'
                @quality -= 1
            end
        end
    else
        ...
    end
    ...
end

Il primo passo è quello di suddividere la funzione in molti più piccoli:

def tick
    case name
    when 'Aged Brie'
        return brie_tick
    ...
    end
end

def brie_tick
    @days_remaining -= 1
    return if quality >= 50

    @quality += 1
    @quality += 1 if @days_remaining <= 0
end

Ciò che ho trovato interessante è il modo in cui sono state scritte queste funzioni più piccole. brie_tick , ad esempio, non è stato scritto estraendo le parti rilevanti della funzione tick originale, ma da zero facendo riferimento ai test unitari test_brie_* . Una volta superati tutti questi test unitari, brie_tick è stato considerato eseguito. Una volta completate tutte le piccole funzioni, la funzione monolitica tick originale è stata eliminata.

Sfortunatamente, il presentatore sembrava inconsapevole che questo approccio portasse a tre delle quattro funzioni *_tick sbagliate (e l'altra era vuota!). Esistono casi limite in cui il comportamento delle funzioni *_tick differisce da quello della funzione tick originale. Ad esempio, @days_remaining <= 0 in brie_tick dovrebbe essere < 0 - quindi brie_tick non funziona correttamente quando chiamato con days_remaining == 1 e quality < 50 .

Che cosa è andato storto qui? È un fallimento dei test, perché non c'erano test per questi casi particolari? O un fallimento del refactoring - perché il codice avrebbe dovuto essere trasformato passo dopo passo piuttosto che riscritto da zero?

    
posta user200783 24.10.2018 - 03:57
fonte

4 risposte

11

Is this a failure of testing - because there were no tests for these particular edge cases? Or a failure of refactoring - because the code should have been transformed step-by-step rather than rewritten from scratch?

Entrambi. Il refactoring utilizzando solo i passaggi standard da il libro originale di Fowlers è decisamente meno incline all'errore rispetto a una riscrittura, quindi è spesso preferibile usa solo questi tipi di piccoli passi. Anche se non ci sono test unitari per ogni caso limite, e anche se l'ambiente non fornisce refactoring automatici, un singolo cambio di codice come "introduce spiegando la variabile" o "funzione di estrazione" ha una possibilità molto più piccola di modificare i dettagli del comportamento del codice esistente rispetto a una completa riscrittura di una funzione.

A volte, tuttavia, riscrivere una sezione di codice è ciò che ti serve o vuoi fare. E se questo è il caso, hai bisogno di test migliori.

Si noti che anche quando si utilizza uno strumento di refactoring, c'è sempre un certo rischio di introdurre errori quando si cambia codice, indipendentemente dall'applicazione di passaggi più piccoli o più grandi. Ecco perché il refactoring ha sempre bisogno di test. Si noti inoltre che i test possono solo ridurre la probabilità di bug, ma non provarne mai la mancanza - tuttavia utilizzando tecniche come guardare il codice e la copertura delle filiali è possibile ottenere un alto livello di confidenza e, in caso di riscrittura di una sezione di codice, è spesso vale la pena applicare tali tecniche.

    
risposta data 24.10.2018 - 07:56
fonte
2

What has gone wrong here? Is this a failure of testing - because there were no tests for these particular edge cases? Or a failure of refactoring - because the code should have been transformed step-by-step rather than rewritten from scratch?

Una delle cose che è davvero difficile lavorare con il codice legacy: acquisire una comprensione completa del comportamento attuale.

Il codice legacy senza test che limita tutti i comportamenti è un pattern comune in the wild. Il che lascia indovinare: vuol dire che i comportamenti non vincolati sono variabili libere? o requisiti che sono sottospecificati?

Dal talk :

Now this is real refactoring according to the definition of refactoring; I'm going to refactor this code. I'm going to change its arrangement without altering its behavior.

Questo è l'approccio più conservativo; se i requisiti possono essere sottostimati, se i test non catturano tutta la logica esistente, devi essere molto molto attento a come procedi.

Per certi, è possibile affermare che se i test non descrivono in modo adeguato il comportamento del sistema, si ha un "fallimento del test". E penso che sia giusto - ma non effettivamente utile; questo è un problema comune avere in the wild.

Or a failure of refactoring - because the code should have been transformed step-by-step rather than rewritten from scratch?

Il problema non è abbastanza che le trasformazioni avrebbero dovuto essere dettagliate; ma piuttosto che la scelta dello strumento di refactoring (operatore di tastiera umano piuttosto che automazione guidata) non era ben allineata con la copertura del test, a causa del più alto tasso di errore.

Questo potrebbe essere stato indirizzato o usando strumenti di refactoring con maggiore affidabilità o introducendo una batteria più ampia di test per migliorare i vincoli sul sistema.

Quindi penso che la tua congiunzione sia scarsamente scelta; AND non OR .

    
risposta data 24.10.2018 - 15:19
fonte
2

Il refactoring non dovrebbe modificare il comportamento esternamente visibile del tuo codice. Questo è l'obiettivo.

Se il test dell'unità fallisce, significa che hai cambiato il comportamento. Ma passare test unitari non è mai l'obiettivo. Aiuta più o meno a raggiungere il tuo obiettivo. Se il refactoring cambia il comportamento visibile esternamente e tutti i test unitari passano, allora il refactoring ha avuto esito negativo.

I test delle unità di lavoro in questo caso ti danno solo la sensazione sbagliata di successo. Ma cosa è andato storto? Due cose: il refactoring era incurante e le prove unitarie non erano molto buone.

    
risposta data 24.10.2018 - 18:04
fonte
1

Se definisci "corretto" come "il test passa", allora per definizione non è sbagliato cambiare comportamento non testato.

Se un particolare comportamento del bordo dovrebbe essere definito, aggiungi un test per questo, in caso contrario, allora è OK non preoccuparti di cosa succede. Se sei veramente pedante, puoi scrivere un test che controlli true quando in quel caso limite a documentare che non ti interessa qual è il comportamento.

    
risposta data 24.10.2018 - 12:32
fonte

Leggi altre domande sui tag