Come migliorare drasticamente la copertura del codice?


21

Ho il compito di ottenere un'applicazione legacy in unit test. Innanzitutto alcune informazioni sull'applicazione: si tratta di una base di codice RCP Java 600k LOC con questi problemi importanti

  • enorme duplicazione del codice
  • nessun incapsulamento, la maggior parte dei dati privati ​​è accessibile dall'esterno, alcuni dei dati aziendali hanno anche reso singolari quindi non sono solo modificabili dall'esterno ma anche da qualsiasi luogo.
  • nessuna astrazione (ad es. nessun modello di business, i dati di business sono memorizzati in Object [] e double [] []), quindi nessuna OO.

Esiste una buona suite di test di regressione e un efficiente team di controllo qualità sta testando e trovando bug. Conosco le tecniche su come metterlo alla prova dai libri classici, ad esempio Michael Feathers, ma è troppo lento. Poiché esiste un sistema di test di regressione funzionante, non ho paura di riformattare in modo aggressivo il sistema per consentire la scrittura di test unitari.

Come dovrei iniziare ad attaccare il problema per ottenere rapidamente un po 'di copertura , quindi sono in grado di mostrare progressi alla gestione (e di fatto iniziare a guadagnare dalla rete di sicurezza dei test JUnit)? Non voglio utilizzare strumenti per generare suite di test di regressione, ad esempio AgitarOne, perché questi test non verificano se qualcosa è corretto.


Perché non creare automaticamente i test di regressione e verificarli singolarmente? Devo essere più veloce di scriverli tutti a mano.
Robert Harvey,

Sembra un po 'divertente chiamare qualsiasi cosa scritta in eredità Java, ma concordato, è certamente eredità. Dici che non hai paura di riformattare il sistema per consentire la scrittura dei test unitari, ma non dovresti scrivere i test unitari sul sistema così com'è, prima di tentare qualsiasi refactoring? Quindi il tuo refactoring può essere eseguito attraverso gli stessi test unitari per assicurarsi che nulla sia rotto?
dodgy_coder

1
@dodgy_coder Di solito sono d'accordo, ma spero che il QA tradizionale che funzioni in modo efficiente mi salvi un po 'di tempo.
Peter Kofler,

1
@dodgy_coder Michael C. Feathers, autore di Working Effectively with Legacy code definisce il codice Legacy come "codice senza test". Serve come utile definizione.
StuperUser

Risposte:


10

Credo che ci siano due assi principali lungo i quali è possibile posizionare il codice quando si tratta di introdurre test unitari: A) quanto è testabile il codice? e B) quanto è stabile (cioè con che urgenza ha bisogno di test)? Guardando solo agli estremi, questo produce 4 categorie:

  1. Codice facile da testare e fragile
  2. Codice facile da testare e stabile
  3. Codice difficile da testare e fragile
  4. Codice difficile da testare e stabile

La categoria 1 è il punto di partenza ovvio, dove puoi ottenere molti benefici con relativamente poco lavoro. La categoria 2 ti consente di migliorare rapidamente la tua statistica di copertura (buona per il morale) e ottenere più esperienza con la base di codice, mentre la categoria 3 è un lavoro più (spesso frustrante) ma offre anche maggiori vantaggi. Ciò che dovresti fare per primo dipende da quanto siano importanti per te le statistiche sul morale e sulla copertura. Probabilmente non vale la pena preoccuparsi della categoria 4.


Eccellente. Ho un'idea di come determinare se è facile controllare mediante analisi statiche, ad esempio conteggio delle dipendenze / Testability Explorer. Ma come posso determinare se il codice è fragile? Non riesco a far corrispondere i difetti a unità specifiche (ad es. Classi), e se è ovviamente il numero 3 (classi / singoli di Dio). Quindi forse il numero di check-in (gli hotspot)?
Peter Kofler,

1
@Peter Kofler: il commit degli hotspot è una buona idea, ma la fonte più preziosa di questo tipo di conoscenza sarebbero gli sviluppatori che hanno lavorato con il codice.
Michael Borgwardt,

1
@Peter - come ha detto Michael, gli sviluppatori che hanno lavorato con il codice. Chiunque abbia lavorato con una base di codice di grandi dimensioni per un bel po 'di tempo saprà quali parti del suo odore. Oppure, se l'intera cosa ha un odore, quali parti di esso puzzano davvero .
Carson63000,

15

Ho molta esperienza di lavoro su sistemi legacy (non Java però), molto più grandi di così. Odio essere il portatore di cattive notizie, il tuo problema è la dimensione del tuo problema. Sospetto che tu l'abbia sottovalutato.

L'aggiunta di test di regressione al codice legacy è un processo lento e costoso. Molti requisiti non sono ben documentati: una correzione di bug qui, una patch lì e prima che tu lo sappia, il software definisce il proprio comportamento. Non avere test significa che l'implementazione è tutto ciò che deve passare, nessun test per "sfidare" i requisiti impliciti implementati nel codice.

Se si tenta di ottenere rapidamente una copertura, è probabile che si affretterà a eseguire il lavoro, a metà cottura e a fallire. I test forniranno una copertura parziale delle cose ovvie e scarsa o nessuna copertura dei problemi reali. Convincerai gli stessi manager che stai cercando di vendere a quell'Unità di Test non ne vale la pena, è solo un altro proiettile d'argento che non funziona.

IMHO, l'approccio migliore è indirizzare i test. Utilizzare i report di metriche, sensibilità intestinale e registro dei difetti per identificare l'1% o il 10% del codice che genera il maggior numero di problemi. Colpisci duramente questi moduli e ignora il resto. Non provare a fare troppo, meno è di più.

Un obiettivo realistico è "Da quando abbiamo implementato l'UT, l'inserimento dei difetti nei moduli in prova è sceso al x% di quelli non in UT" (idealmente x è un numero <100).


+1, non puoi testare un'unità in modo efficace senza uno standard più forte rispetto al codice.
dan_waterworth,

Lo so e sono d'accordo. La differenza è che abbiamo test, test di regressione tradizionali lavorando sul QA, quindi esiste una sorta di rete di sicurezza. Secondo, sono profondamente favorevole ai test unitari, quindi non sarà sicuramente un'altra cosa che non ha funzionato. Un buon punto su cosa puntare per primo. Grazie.
Peter Kofler,

1
e non dimenticare che puntare semplicemente alla "copertura" non migliorerà la qualità poiché rimarrai bloccato in una massa di test imperfetti e banali (e test per codice banale che non necessita di test espliciti, ma vengono aggiunti solo per aumentare la copertura). Finirai per creare test per soddisfare lo strumento di copertura, non perché sono utili, e possibilmente cambierai il codice stesso per aumentare la copertura dei test senza scrivere test (come tagliare commenti e definizioni variabili, che alcuni strumenti di copertura chiamerà il codice scoperto).
jwenting

2

Mi viene in mente quel detto di non preoccuparsi della porta della stalla quando il cavallo è già scappato.

La realtà è che non esiste davvero un modo economico per ottenere una buona copertura dei test per un sistema legacy, certamente non in un breve lasso di tempo. Come menzionato da MattNz, sarà un processo che richiede molto tempo e, in ultima analisi, costoso. Il mio istinto mi dice che se si tenta di fare qualcosa per impressionare la gestione, probabilmente si creerà un nuovo incubo di manutenzione a causa del tentativo di mostrare troppo in fretta, senza comprendere appieno i requisiti che si sta tentando di testare.

Realisticamente, una volta che hai già scritto il codice, è quasi troppo tardi per scrivere i test in modo efficace senza il rischio di perdere qualcosa di vitale. D'altra parte, si potrebbe dire che alcuni test sono meglio di nessun test, ma in tal caso, i test stessi devono mostrare che aggiungono valore al sistema nel suo insieme.

Il mio suggerimento sarebbe quello di guardare quelle aree chiave in cui ritieni che qualcosa sia "rotto". Con ciò intendo che potrebbe essere un codice terribilmente inefficiente o che si può dimostrare che è stato precedentemente costoso da mantenere. Documenta i problemi, quindi usalo come punto di partenza per introdurre un livello di test che ti aiuti a migliorare il sistema, senza intraprendere un massiccio sforzo di reingegnerizzazione. L'idea qui è quella di evitare di giocare al passo con i test, e invece introdurre test per aiutarti a risolvere problemi specifici. Dopo un periodo di tempo, vedi se riesci a misurare e distinguere tra il costo precedente di mantenimento di quella sezione di codice e gli attuali sforzi con le correzioni che hai applicato con i loro test di supporto.

La cosa da ricordare è che i dirigenti sono più interessati ai costi / benefici e al modo in cui ciò influisce direttamente sui loro clienti e, in definitiva, sul loro budget. Non sono mai interessati a fare qualcosa semplicemente perché è la cosa migliore da fare a meno che tu non possa provare che fornirà loro un vantaggio che li interessa. Se sei in grado di dimostrare che stai migliorando il sistema e stai ottenendo una buona copertura dei test per il lavoro che stai facendo attualmente, è più probabile che il management lo consideri un'applicazione efficiente dei tuoi sforzi. Questo potrebbe eventualmente permetterti di discutere il caso di estendere i tuoi sforzi ad altre aree chiave, senza richiedere né un blocco completo dello sviluppo del prodotto, o ancora peggio la quasi impossibile discutere per la riscrittura!


1

Un modo per migliorare la copertura è scrivere più test.

Un altro modo è ridurre la ridondanza nel codice, in modo tale che i test esistenti in effetti coprano il codice ridondante non attualmente coperto.

Immagina di avere 3 blocchi di codice, a, b e b ', dove b' è un duplicato (copia esatta o quasi mancante) di B e che hai una copertura su aeb ma non b 'con il test T.

Se refactoring la base di codice per eliminare b 'estraendo la comunanza da b e b' come B, la base di codice ora appare come a, b0, B, b'0, con b0 contenente il codice non condiviso con b'0 e vice- viceversa, e sia b0 che b'0 essendo molto più piccoli di B e invocando / usando B.

Ora la funzionalità del programma non è cambiata, e nemmeno ha testato T, quindi possiamo eseguire di nuovo T e aspettarci che passi. Il codice ora coperto è a, b0 e B, ma non b'0. La base di codice è diventata più piccola (b'0 è più piccola di b '!) E continuiamo a coprire ciò che inizialmente abbiamo coperto. Il nostro rapporto di copertura è aumentato.

Per fare ciò, è necessario trovare b, b 'e idealmente B per abilitare il refactoring. Il nostro strumento CloneDR può farlo in molte lingue, specialmente Java. Dici che la tua base di codice ha un sacco di codice duplicato; questo potrebbe essere un buon modo per affrontarlo a tuo vantaggio.

Stranamente, l'atto di trovare b & b spesso aumenta il tuo vocabolario sulle idee astratte implementate dal programma. Sebbene lo strumento non abbia idea di cosa faccia b e b, l'atto stesso di isolarli dal codice, consentendo una semplice attenzione al contenuto di b e b, spesso offre ai programmatori un'ottima idea di quale astrazione B_abstract il codice clonato implementa . Questo migliora anche la tua comprensione del codice. Assicurati di dare a B un buon nome quando lo interrompi, e otterrai una migliore copertura dei test e un programma più gestibile.

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.