TDD e copertura completa del test in cui sono necessari casi di test esponenziali

17

Sto lavorando su un comparatore di elenchi per facilitare l'ordinamento di un elenco non ordinato di risultati di ricerca per esigenze molto specifiche del nostro cliente. I requisiti richiedono un algoritmo di pertinenza classificato con le seguenti regole in ordine di importanza:

  1. Corrispondenza esatta per nome
  2. Tutte le parole della query di ricerca nel nome o un sinonimo del risultato
  3. Alcune parole della query di ricerca nel nome o nel sinonimo del risultato (% discendente)
  4. Tutte le parole della query di ricerca nella descrizione
  5. Alcune parole della query di ricerca nella descrizione (% discendente)
  6. Data ultima modifica decrescente

La scelta di design naturale per questo comparatore sembrava essere una classifica basata su potenze di 2. La somma di regole meno importanti non può mai essere più di una corrispondenza positiva su una regola di maggiore importanza. Questo risultato è ottenuto dal seguente punteggio:

  1. 32
  2. 16
  3. 8 (Punteggio secondario di tie-breaker basato su% decrescente)
  4. 4
  5. 2 (Punteggio secondario di tie-breaker basato su% decrescente)
  6. 1

Nello spirito TDD ho deciso di iniziare prima con i miei test unitari. Avere un caso di test per ogni scenario unico dovrebbe essere almeno 63 casi di test esclusivi che non considerano casi di test aggiuntivi per la logica del tie breaker secondario sulle regole 3 e 5. Questo sembra prepotente.

I test effettivi saranno effettivamente meno però. Sulla base delle regole stesse, alcune regole assicurano che le regole inferiori siano sempre vere (ad esempio, quando "Tutte le parole della query di ricerca vengono visualizzate nella descrizione", la regola "alcune parole della query di ricerca appaiono nella descrizione" sarà sempre vera). È ancora il livello di impegno nello scrivere ognuno di questi casi di test che vale la pena? È questo il livello di test che viene tipicamente richiesto quando si parla di copertura del 100% del test in TDD? In caso contrario, quale sarebbe una strategia di test alternativa accettabile?

    
posta maple_shaft 02.01.2014 - 14:40
fonte

6 risposte

15

La tua domanda implica che TDD abbia qualcosa a che fare con "scrivere prima tutti i casi di test". IMHO che non è "nello spirito di TDD", in realtà è contro esso. Ricorda che TDD sta per "test driven development", quindi hai bisogno solo di quei casi di test che realmente "guidano" la tua implementazione, non di più. E fintanto che la tua implementazione non è progettata in modo tale che il numero di blocchi di codice cresca in modo esponenziale con ogni nuovo requisito, non avrai nemmeno bisogno di un numero esponenziale di casi di test. Nel tuo esempio, il ciclo TDD probabilmente avrà questo aspetto:

  • inizia con il primo requisito del tuo elenco: le parole con "Corrispondenza esatta per nome" devono ottenere un punteggio superiore rispetto a qualsiasi altro
  • ora scrivi un primo caso di test per questo (ad esempio: una parola che corrisponde a una determinata query) e implementa la quantità minima di codice funzionante che fa passare quel test
  • aggiungi un secondo test case per il primo requisito (ad esempio: una parola che non corrisponde alla query), e prima di aggiungere un nuovo test case , cambia il codice esistente fino al passaggio del secondo test
  • a seconda dei dettagli della tua implementazione, sentiti libero di aggiungere altri casi di test, ad esempio una query vuota, una parola vuota ecc. (ricorda: TDD è un approccio white box , puoi usare del fatto che conosci la tua implementazione quando progetti i casi di test).

Quindi, inizia con il secondo requisito:

  • "Tutte le parole della query di ricerca nel nome o un sinonimo del risultato" devono ottenere un punteggio inferiore a "Corrispondenza esatta per nome", ma un punteggio più alto di ogni altra.
  • ora costruisci casi di test per questo nuovo requisito, come sopra, uno dopo l'altro, e implementa la parte successiva del codice dopo ogni nuovo test. Non dimenticare di refactoring in mezzo, il tuo codice così come i casi di test.

Ecco il trucco : quando aggiungi casi di test per numero di requisito / categoria "n", dovrai solo aggiungere test per assicurarti che il punteggio della categoria "n-1" è superiore al punteggio per la categoria "n". Non dovrai aggiungere nessun caso di test per ogni altra combinazione delle categorie 1, ..., n-1, poiché i test che hai scritto prima faranno in modo che i punteggi di quelle categorie siano ancora nell'ordine corretto.

Quindi questo ti darà un numero di casi di test che cresce approssimativamente in linea con il numero di requisiti, non in modo esponenziale.

    
risposta data 02.01.2014 - 16:34
fonte
13

Considera di scrivere una classe che attraversa un elenco predefinito di condizioni e moltiplica un punteggio corrente di 2 per ogni controllo positivo.

Questo può essere testato molto facilmente, usando solo un paio di test di simulazione.

Quindi puoi scrivere una classe per ogni condizione e ci sono solo 2 test per ogni caso.

Non capisco davvero il tuo caso d'uso, ma spero che questo esempio possa aiutarti.

public class ScoreBuilder
{
    private ISingleScorableCondition[] _conditions;
    public ScoreBuilder (ISingleScorableCondition[] conditions)
    {
        _conditions = conditions;
    }

    public int GetScore(string toBeScored)
    {
        foreach (var condition in _conditions)
        {
            if (_conditions.Test(toBeScored))
            {
                // score this somehow
            }
        }
    }
}

public class ExactMatchOnNameCondition : ISingleScorableCondition
{
    private IDataSource _dataSource;
    public ExactMatchOnNameCondition(IDataSource dataSource)
    {
        _dataSource = dataSource;
    }

    public bool Test(string toBeTested)
    {
        return _dataSource.Contains(toBeTested);
    }
}

// etc

Noterai che i tuoi 2 ^ test di condizioni si riducono rapidamente a 4+ (2 * condizioni). 20 è molto meno prepotente di 64. E se ne aggiungi un altro dopo, non devi modificare ANY delle classi esistenti (principio open-closed), quindi non devi scrivere 64 nuovi test, hai solo per aggiungere un'altra classe con 2 nuovi test e inserirla nella classe ScoreBuilder.

    
risposta data 02.01.2014 - 15:10
fonte
4

Still is the level of effort in writing out each of these test cases worth it?

Devi definire "ne vale la pena". Il problema con questo tipo di scenario è che i test avranno un ritorno in diminuzione sull'utilità. Certamente il primo test che scriverai ne valuterà la pena. Può trovare errori evidenti nella priorità e persino cose come errori di parsing quando si cerca di suddividere le parole.

Il secondo test varrà la pena perché copre un percorso diverso attraverso il codice, probabilmente controllando un'altra relazione di priorità.

Probabilmente il 63esimo test non ne varrà la pena perché è qualcosa che il 99,99% di sicurezza è coperto dalla logica del tuo codice o da un altro test.

Is this the level of testing that is typically called for when talking about 100% test coverage in TDD?

La mia comprensione è al 100% significa che tutti i percorsi del codice sono esercitati. Questo non significa che fai tutte le combinazioni delle tue regole, ma tutti i diversi percorsi che il tuo codice potrebbe andare giù (come fai notare, alcune combinazioni non possono esistere nel codice). Ma dal momento che stai facendo TDD, non c'è ancora un "codice" per controllare i percorsi. La lettera del processo direbbe rendere tutti i 63 +.

Personalmente, trovo che il 100% di copertura sia un sogno irrealizzabile. Oltre a ciò, è poco pratico. Esiste un test unitario per servirvi, non viceversa. Man mano che si eseguono più test, si ottengono rendimenti decrescenti sul vantaggio (la probabilità che il test prevenga un errore + la certezza che il codice sia corretto). A seconda di cosa il tuo codice definisce dove su quella scala mobile smetti di fare test. Se il tuo codice esegue un reattore nucleare, allora forse tutti i 63+ test valgono la pena. Se il tuo codice sta organizzando il tuo archivio musicale, probabilmente potresti farcela con molto meno.

    
risposta data 02.01.2014 - 15:00
fonte
4

Direi che questo è un caso perfetto per TDD.

Hai una serie conosciuta di criteri da testare, con una ripartizione logica di quei casi. Supponendo che tu stia andando a testare le unità ora o dopo, sembra che abbia senso prendere il risultato noto e costruirlo attorno, assicurandoti che, in realtà, copra ognuna delle regole in modo indipendente.

Inoltre, puoi scoprire come vai se aggiungere una nuova regola di ricerca interrompe una regola esistente. Se fai tutto questo alla fine della codifica, presumibilmente corri un rischio maggiore di doverne modificare uno per sistemarne uno, che ne rompe un altro, che ne rompe un altro ... E, impari mentre implementi le regole se il tuo disegno è valido o ha bisogno di ritocchi.

    
risposta data 02.01.2014 - 15:02
fonte
1

Non sono un fan di interpretare rigorosamente la copertura di test al 100% come specifiche di scrittura contro ogni singolo metodo o testare ogni permutazione del codice. In questo modo si tende a condurre in modo fanatico a una progettazione basata sulle prove delle classi che non incapsula correttamente la logica aziendale e produce test / specifiche generalmente prive di significato in termini di descrizione della logica di business supportata. Invece, mi concentro sulla strutturazione dei test molto come le stesse regole di business e mi sforzo di esercitare ogni ramo condizionale del codice con test con l'esplicita aspettativa che tali test siano facilmente comprensibili dal tester in quanto sarebbero generalmente casi d'uso e in realtà descrive il regole aziendali implementate.

Tenendo presente questa idea, vorrei testare in modo esauriente i 6 fattori di classifica che hai elencato in modo isolato l'uno con l'altro, seguito da 2 o 3 test di stile di integrazione che assicurano che stai portando i risultati ai valori di classifica generale attesi. Ad esempio, caso n. 1, corrispondenza esatta per nome, avrei almeno due test unitari per verificare quando è esatto e quando non lo è e che i due scenari restituiscono il punteggio previsto. Se è sensibile alla distinzione tra maiuscole e minuscole, anche un caso per testare "Corrispondenza esatta" e "corrispondenza esatta" ed eventualmente altre variazioni di input come la punteggiatura, gli spazi aggiuntivi, ecc., Restituiscono anche i punteggi previsti.

Dopo aver esaminato tutti i fattori individuali che contribuiscono ai punteggi di ranking, presumo che funzionino correttamente a livello di integrazione e si concentrino sull'assicurare che i loro fattori combinati contribuiscano correttamente al punteggio di classificazione finale previsto.

Supponendo che i casi # 2 / # 3 e # 4 / # 5 siano generalizzati agli stessi metodi sottostanti, ma passando campi diversi, devi solo scrivere un set di unit test per i metodi sottostanti e scrivere semplici test unitari aggiuntivi per prova i campi specifici (titolo, nome, descrizione, ecc.) e il punteggio al factoring designato, quindi questo riduce ulteriormente la ridondanza del tuo sforzo complessivo di test.

Con questo approccio, l'approccio sopra descritto probabilmente produrrebbe 3 o 4 test unitari sul caso n. 1, forse 10 specifiche su alcuni / tutti i sinonimi considerati - oltre a 4 specifiche sul punteggio corretto dei casi n. 2 - # 5 e 2 o 3 specifiche sulla classifica finale della data finale, quindi da 3 a 4 test di livello di integrazione che misurano tutti i 6 casi combinati in modi probabili (per ora dimenticati di casi di margine oscuri a meno che non si veda chiaramente un problema nel codice che deve essere esercitato per garantire che la condizione sia gestita) o garantire che non venga violato / interrotto da revisioni successive. Ciò porta a circa 25 spec. Per esercitare il 100% del codice scritto (anche se non hai chiamato direttamente il 100% dei metodi scritti).

    
risposta data 02.01.2014 - 15:54
fonte
1

Non sono mai stato un fan del 100% di copertura del test. Nella mia esperienza, se qualcosa è abbastanza semplice da testare con solo uno o due casi di test, allora è abbastanza semplice da fallire raramente. Quando fallisce, di solito è dovuto a modifiche architettoniche che richiederebbero comunque modifiche di test.

Detto questo, per requisiti come il tuo, collaudo sempre a fondo, anche su progetti personali in cui nessuno mi sta facendo, perché questi sono i casi in cui i test unitari ti fanno risparmiare tempo ed esasperazione. Più test di unità sono necessari per testare qualcosa, più test di unità di tempo salveranno.

Questo perché puoi tenere in testa così tante cose contemporaneamente. Se stai cercando di scrivere codice che funzioni per 63 combinazioni diverse, è spesso difficile risolvere una combinazione senza romperne un'altra. Si finisce per testare manualmente altre combinazioni più e più volte. Il test manuale è molto più lento, il che non ti fa desiderare di rieseguire ogni possibile combinazione ogni volta che apporti una modifica. Ciò ti rende più probabile che manchi qualcosa e più probabilmente spreca tempo a perseguire percorsi che non funzionano in tutti i casi.

A parte il tempo risparmiato rispetto ai test manuali, c'è molta meno tensione mentale, che rende più facile concentrarsi sul problema senza preoccuparsi di introdurre accidentalmente regressioni. Ciò ti consente di lavorare più velocemente e più a lungo senza esaurimento. A mio parere, i benefici per la salute mentale valgono da soli il costo del test unitario del codice complesso, anche se non ti ha salvato in alcun momento.

    
risposta data 02.01.2014 - 17:14
fonte

Leggi altre domande sui tag