Implementazione di TDD per codice esistente [duplicato]

3

Ho appena iniziato a testare le unità e sto cercando di capire come incorporarlo con un progetto con codice esistente. Diciamo che volevo scrivere test per una classe specifica in quel progetto, ma che certe classi richiedono un'istanza di un'altra classe per i metodi da eseguire e / o testare. Come dovrei avvicinarmi a questo?

    
posta Julious Igmen 21.04.2014 - 06:28
fonte

4 risposte

4

Questa è una domanda molto complessa, in un sistema legacy progettato senza la possibilità di testabilità, probabilmente ci sono molti accoppiamenti e questo accoppiamento rende i test isolati (test delle unità) più difficili.

Posso dare una risposta concreta se fai una domanda concreta, con un codice e una situazione concreta, ma se chiedi un consiglio generale la cosa migliore che posso fare è raccomandarti la fantastica forma del libro di piume di michael che funziona efficacemente con il codice legacy ( link ).

    
risposta data 21.04.2014 - 07:54
fonte
4

Se il codice è già in produzione e non ha test per questo, per definizione non è possibile utilizzare TDD. A quel punto hai almeno queste alternative:

  1. Elimina il codice e ricomincia da capo usando TDD. Questo può ovviamente richiedere molto tempo, ma a giudicare dall'esperienza precedente sarebbe stato spesso più veloce rispetto alle altre alternative.
  2. Basta usare TDD per qualsiasi nuovo codice. Questo lascia il vecchio codice per diventare un nucleo marcio, una scatola nera che alla fine nessuno oserà cambiare e che probabilmente trascineranno le prestazioni, la stabilità e la sicurezza del resto del codice per tutta la vita del progetto.
  3. Aggiungi i test al codice esistente in modo incrementale e usa TDD per qualsiasi nuovo codice. Sorprendentemente, nella mia esperienza questo non sembra essere di grande aiuto. A meno che il codice non venga refactored (si veda il prossimo punto), sarà comunque, eventualmente, trasformato in una scatola nera.
  4. Usa TDD per qualsiasi nuovo codice, sostituendo i bit del vecchio codice con il codice TDD ogni volta che devi toccarne uno qualsiasi. Se l'alternativa 1 non sta per accadere, questo ti dà almeno una possibilità che il progetto non sarà incatenato a una palla di fango per il resto del tempo. Probabilmente scoprirai che estrarre una parte del codice legacy non testato potrebbe sbrogliarlo completamente, e lo sforzo combinato di tutti i refactoring potrebbe richiedere più o più tempo della re-implementazione.
risposta data 21.04.2014 - 10:35
fonte
-1

Non è sempre facile ea volte devi solo essere pragmatico e utilizzare test di integrazione o test end-to-end dall'inizio fino a quando non hai rifattorizzato il codice per renderlo più testabile.

Forse provare a rifattorizzare la classe per usare un'interfaccia invece di una classe concreta e iniettare durante l'implementazione durante la costruzione. Quindi usa una struttura di simulazione o tira su la tua classe di simulazione e inseriscila invece durante il test.

Alcuni framework di derisione sono in grado di prendere in giro lezioni concrete ma dipende dalla lingua.

    
risposta data 21.04.2014 - 08:03
fonte
-3

Sarò piuttosto radicale ma ti preghiamo di leggerlo completamente prima di criticare.

In breve: il design basato sui test è una specie di mito. È impossibile per l'implementazione anche per un nuovo codice, a meno che tutte le specifiche non siano già fisse e non cambino (è il classico caso "Waterfall" ma non "Agile", contrario all'area di applicazione TDD solitamente dichiarata). Quando funziona con progetti one-shot, funziona solo perché i suoi principi sono violati , non soddisfatti.

Per capire questo, si dovrebbe immaginare il caso in cui la specifica è cambiata e viene aggiunto un nuovo requisito, ma è già soddisfatto nel codice esistente. Il codice esistente dovrebbe essere eliminato e riscritto da zero? La descrizione TDD classica non può rispondere a tutto questo - non si occupa di un caso di codice esistente.

La seconda domanda è ciò che deve essere testato. Se un pezzo di codice deve soddisfare i test ma non fare nulla di più, potrebbe essere solo casi di test elencati in un grande switch-case ? Se non lo fa, perché? Questo è ciò che è esplicitamente richiesto (almeno nella classica spiegazione compatta).

La terza domanda è come controllare tutti i casi marginali. Se una routine moltiplica i numeri, deve controllare INT_MIN*INT_MIN ? La conoscenza dei casi marginali viene principalmente scoperta dopo alcune esperienze di utilizzo, ma non a priori . (Sì, maggiore è la competenza del programmatore, maggiore è la capacità di trovare tali casi prima di scrivere il codice che ha, ma il 90% delle competenze è ciò che non si deve fare, ma non quello che si deve.)

La mia risposta è che TDD svolge due ruoli:

  1. Mostra un ideale, irraggiungibile nella pratica (come l'infinito in matematica), ma è facile da comprendere e quindi approssimato il più vicino possibile.
  2. Fornisce una scusa ai manager per richiedere test che non vengono posticipati fino a "buoni tempi" ma solo ora, prima di riportare l'attività finita.

Quindi sarà trattato di conseguenza - "con entusiasmo ma senza fanatismo".

La mia attitudine personale al TDD è formata da un'esperienza specifica di costruzione di sistemi complessi che devono funzionare "24 * 7 * 365" e uno stile di accettazione che si avvicina al grado militare. Tali sistemi richiedono, da parte degli sviluppatori, test integrati delle catene di componenti e test regolari al volo, agli stessi componenti e con gli stessi algoritmi, ma con altri tag (quindi, solo il controllore delle decisioni finali comprende che non si trattava di dati reali) . Per i nostri sistemi, TDD è intenzionalmente sostituito con il seguente approccio:

  1. Naturalmente, il principio secondo cui il codice deve fare ciò a cui è destinato, deve essere più importante del principio TDD (o analogico) che un codice sia il più piccolo e semplice possibile. Ciò significa anche che la leggibilità del codice e la debugabilità sono molto importanti. (Tutto questo non è un commento di Captain Ovvio ma la regola scritta per quei ragazzi che tendono a seguire le istruzioni nel modo più letterale e che, purtroppo, sono presenti in ogni grande squadra.)
  2. Ogni componente inizia con un set di test ragionevolmente piccolo che consente a un programmatore esperto di dire che è stato controllato, in base alle condizioni e alle risorse correnti.
  3. Una parte del tempo (di ciascun programmatore separatamente o dell'intero team) è dedicata all'espansione di test per componenti già esistenti. Questo può essere, secondo le specifiche degli obiettivi, dal 20% all'80%, ma sicuramente non inferiore. Questo lavoro non si fermerà fino a quando l'intero sviluppo del progetto non verrà abbandonato.
  4. Per rispondere alla domanda se un test funziona (a cui risponde TDD in modo univoco e invalicabile), il test stesso deve avere tecniche per convalidarlo. Per questo compito, usiamo le cosiddette inversioni . Un'inversione è una modifica dei dati o dell'ambiente in entrata che deve interrompere il test in modo noto. Ad esempio, se una funzione converte A1 in B1 e A2 in B2, il suo test che convalida B1 è la risposta ad A1 avrà esito negativo se il valore di input diventa A2 e, inoltre, è possibile controllare i dettagli di errore (tipo di errore generato, numero di errori, ecc.) Non tutti i test devono avere inversioni; ma più è complesso, più inversioni principali sono.

P.S. Per una guida iniziale per aggiungere test a un progetto esistente, è possibile iniziare con M. Il libro di Feather . Non si basa su TDD ma, invece, è molto pratico nel suggerire come riformare un codice esistente in modo evolutivo.

    
risposta data 21.04.2014 - 08:08
fonte

Leggi altre domande sui tag