Come si scrivono i test unitari per il codice con risultati difficili da prevedere?


124

Lavoro spesso con programmi molto numerici / matematici, dove è difficile prevedere in anticipo il risultato esatto di una funzione.

Nel tentativo di applicare TDD con questo tipo di codice, trovo spesso la scrittura del codice in prova molto più semplice rispetto alla scrittura di unit test per quel codice, perché l'unico modo che conosco per trovare il risultato atteso è applicare l'algoritmo stesso (sia nel mio testa, su carta o al computer). Questo sembra sbagliato, perché sto effettivamente usando il codice in prova per verificare i test delle mie unità, invece del contrario.

Esistono tecniche note per scrivere unit test e applicare TDD quando è difficile prevedere il risultato del codice in prova?

Un esempio (reale) di codice con risultati difficili da prevedere:

Una funzione weightedTasksOnTimeche, data una quantità di lavoro svolto al giorno workPerDaynell'intervallo (0, 24], l'ora corrente initialTime> 0 e un elenco di attività taskArray; ognuna con un tempo per completare la proprietà time> 0, data di scadenza duee valore di importanza importance; restituisce un valore normalizzato nell'intervallo [0, 1] che rappresenta l'importanza delle attività che possono essere completate prima della duedata se ciascuna attività viene completata nell'ordine indicato da taskArray, a partire da initialTime.

L'algoritmo per implementare questa funzione è relativamente semplice: iterare le attività in taskArray. Per ogni attività, aggiungi timea initialTime. Se il nuovo tempo < due, aggiungi importancea un accumulatore. Il tempo viene regolato da inverso workPerDay. Prima di restituire l'accumulatore, dividere per somma delle importazioni di attività da normalizzare.

function weightedTasksOnTime(workPerDay, initialTime, taskArray) {
    let simulatedTime = initialTime
    let accumulator = 0;
    for (task in taskArray) {
        simulatedTime += task.time * (24 / workPerDay)
        if (simulatedTime < task.due) {
            accumulator += task.importance
        }
    }
    return accumulator / totalImportance(taskArray)
}

Credo che il problema di cui sopra possa essere semplificato, pur mantenendo il suo nucleo, rimuovendo workPerDaye il requisito di normalizzazione, per dare:

function weightedTasksOnTime(initialTime, taskArray) {
    let simulatedTime = initialTime
    let accumulator = 0;
    for (task in taskArray) {
        simulatedTime += task.time
        if (simulatedTime < task.due) {
            accumulator += task.importance
        }
    }
    return accumulator
}

Questa domanda affronta situazioni in cui il codice in esame non è una reimplementazione di un algoritmo esistente. Se il codice è una reimplementazione, ha intrinsecamente facili risultati da prevedere, poiché le implementazioni di fiducia esistenti dell'algoritmo fungono da naturale oracolo del test.


4
Potete fornire un semplice esempio di una funzione il cui risultato è difficile da prevedere?
Robert Harvey,

62
FWIW non stai testando l'algoritmo. Presumibilmente è corretto. Stai testando l'implementazione. Allenarsi a mano va spesso bene come una costruzione parallela.
Kristian H,


7
Ci sono situazioni in cui un algoritmo non può essere ragionevolmente testato in unità, ad esempio se il suo tempo di esecuzione è di più giorni / mesi. Ciò può accadere durante la risoluzione dei problemi NP. In questi casi, può essere più fattibile fornire una prova formale che il codice sia corretto.
Hulk,

12
Qualcosa che ho visto in un codice numerico molto complicato è trattare i test unitari solo come test di regressione. Scrivi la funzione, eseguila per diversi valori interessanti, convalida i risultati manualmente, quindi scrivi il test unitario per rilevare le regressioni dal risultato atteso. Coding horror? Curioso ciò che pensano gli altri.
Chuu,

Risposte:


251

Ci sono due cose che puoi testare nel codice difficile da testare. Innanzitutto, i casi degeneri. Cosa succede se non ci sono elementi nell'array di attività, o solo uno, o due ma uno è passato alla data di scadenza, ecc. Tutto ciò che è più semplice del vero problema, ma comunque ragionevole da calcolare manualmente.

Il secondo è i controlli di sanità mentale. Questi sono i controlli che fai dove non sai se una risposta è giusta , ma sicuramente sapresti se è sbagliata . Queste sono cose come il tempo deve andare avanti, i valori devono essere in un intervallo ragionevole, le percentuali devono aggiungere fino a 100, ecc.

Sì, questo non è buono come un test completo, ma rimarrai sorpreso da quanto spesso sbagli sui controlli di sanità mentale e sui casi degenerati, che rivela un problema nel tuo algoritmo completo.


54
Penso che questo sia un ottimo consiglio. Inizia scrivendo questo tipo di test unitari. Mentre sviluppi il software, se trovi bug o risposte errate, aggiungi quelli come unit test. Fai lo stesso, in una certa misura, quando trovi risposte sicuramente corrette. Costruiscili nel tempo, e tu (alla fine) avrai una serie molto completa di test unitari nonostante tu abbia iniziato a non sapere cosa sarebbero stati ...
Algy Taylor

21
Un'altra cosa che potrebbe essere utile in alcuni casi (anche se forse non in questo) è scrivere una funzione inversa e testare che, se concatenati, l'input e l'output sono gli stessi.
Cyberspark

7
il controllo di integrità spesso costituisce un buon obiettivo per i test basati su proprietà con qualcosa come QuickCheck
jk.

10
l'altra categoria di test che consiglierei sono alcuni per verificare eventuali cambiamenti involontari nell'output. Puoi "imbrogliare" su questi usando il codice stesso per generare il risultato atteso poiché l'intento di questi è di aiutare i manutentori segnalando che qualcosa inteso come un cambiamento neutro in uscita ha influenzato involontariamente il comportamento algoritmico.
Dan Neely,

5
@iFlo Non sono sicuro se stavi scherzando, ma esiste già l'inverso inverso. Vale la pena rendersi conto che il fallimento del test potrebbe essere un problema nella funzione inversa
lucidbrot

80

Scrivevo test per software scientifici con risultati difficili da prevedere. Abbiamo fatto molto uso delle relazioni metamorfiche. Fondamentalmente ci sono cose che sai su come dovrebbe comportarsi il tuo software anche se non conosci esatti risultati numerici.

Un possibile esempio per il tuo caso: se diminuisci la quantità di lavoro che puoi svolgere ogni giorno, la quantità totale di lavoro che puoi fare rimarrà nella migliore delle ipotesi, ma probabilmente diminuirà. Quindi esegui la funzione per un numero di valori di workPerDaye assicurati che la relazione sia valida.


32
Le relazioni metamorfiche sono un esempio specifico di test basati sulle proprietà , che è in generale uno strumento utile per situazioni come queste
Dannnno

38

Le altre risposte hanno buone idee per lo sviluppo di test per casi limite o di errore. Per gli altri, l'uso dell'algoritmo stesso non è l'ideale (ovviamente) ma è comunque utile.

Rileverà se l'algoritmo (o i dati da cui dipende) è cambiato

Se la modifica è un incidente, è possibile ripristinare un commit. Se la modifica è stata intenzionale, è necessario rivedere il test unitario.


6
E per la cronaca, questo tipo di test sono spesso chiamati "test di regressione" secondo il loro scopo e sono sostanzialmente una rete di sicurezza per qualsiasi modifica / refactoring.
Pac0,

21

Allo stesso modo in cui scrivi test unitari per qualsiasi altro tipo di codice:

  1. Trova alcuni casi di test rappresentativi e testali.
  2. Trova casi limite e testali.
  3. Trova le condizioni di errore e testale.

A meno che il codice non includa qualche elemento casuale o non sia deterministico (cioè non produrrà lo stesso output dato lo stesso input), è testabile in unità.

Evita gli effetti collaterali o le funzioni che sono influenzate da forze esterne. Le funzioni pure sono più facili da testare.


2
Per gli algoritmi non deterministici è possibile salvare il seme di RNG o deriderlo utilizzando una sequenza fissa o una serie deterministica a bassa discrepanza, ad esempio la sequenza di Halton
wondra

14
@PaintingInAir Se è impossibile verificare l'output dell'algoritmo, l'algoritmo può anche essere errato?
WolfgangGroiss,

5
Unless your code involves some random elementIl trucco qui è rendere il tuo generatore di numeri casuali una dipendenza iniettata, quindi puoi sostituirlo con un generatore di numeri che dia il risultato esatto che desideri. Ciò consente di testare nuovamente accuratamente, contando anche i numeri generati come parametri di input. not deterministic (i.e. it won't produce the same output given the same input)Poiché un test unitario dovrebbe iniziare da una situazione controllata , può essere non deterministico solo se ha un elemento casuale - che è quindi possibile iniettare. Non riesco a pensare ad altre possibilità qui.
Flater,

3
@PaintingInAir: O oppure. Il mio commento si applica sia all'esecuzione rapida sia alla scrittura rapida di test. Se ti occorrono tre giorni per calcolare manualmente un singolo esempio (supponiamo che tu utilizzi il metodo più veloce disponibile che non utilizza il codice), allora sono necessari tre giorni. Se invece hai basato il risultato del test previsto sul codice stesso, il test si sta compromettendo. È come fare if(x == x), è un confronto inutile. Hai bisogno dei tuoi due risultati ( effettivi : provengono dal codice; previsti : provengono dalle tue conoscenze esterne) per essere indipendenti l'uno dall'altro.
Flater,

2
È ancora unitamente testabile anche se non deterministico, a condizione che sia conforme alle specifiche e che la conformità possa essere misurata (ad es. Distribuzione e diffusione per casuale) Potrebbe richiedere solo un gran numero di campioni per eliminare il rischio di anomalia.
mckenzm,

17

Aggiornamento a causa di commenti pubblicati

La risposta originale è stata rimossa per brevità: puoi trovarla nella cronologia delle modifiche.

PaintingInAir Per il contesto: come imprenditore e accademico, la maggior parte degli algoritmi che disegno non sono richiesti da nessuno tranne me stesso. L'esempio fornito nella domanda fa parte di un ottimizzatore privo di derivati ​​per massimizzare la qualità di un ordinamento di attività. In termini di come ho descritto la necessità della funzione di esempio internamente: "Ho bisogno di una funzione obiettiva per massimizzare l'importanza dei compiti che sono completati in tempo". Tuttavia, sembra esserci ancora un grande divario tra questa richiesta e l'implementazione di unit test.

Innanzitutto, un TL; DR per evitare una risposta altrimenti lunga:

Pensala in questo modo:
un cliente entra in McDonald's e chiede un hamburger con lattuga, pomodoro e sapone per le mani come condimento. Questo ordine viene dato al cuoco, che prepara l'hamburger esattamente come richiesto. Il cliente riceve questo hamburger, lo mangia e poi si lamenta con il cuoco che questo non è un gustoso hamburger!

Non è colpa del cuoco - sta solo facendo ciò che il cliente ha esplicitamente chiesto. Non è compito del cuoco verificare se l'ordine richiesto è effettivamente gustoso . Il cuoco crea semplicemente ciò che il cliente ordina. È la responsabilità del cliente di ordinare qualcosa che trovano gustoso .

Allo stesso modo, non è compito dello sviluppatore mettere in discussione la correttezza dell'algoritmo. Il loro unico lavoro è implementare l'algoritmo come richiesto.
Il test unitario è uno strumento per sviluppatori. Conferma che l'hamburger corrisponde all'ordine (prima che lasci la cucina). Non (e non dovrebbe) provare a confermare che l'hamburger ordinato è effettivamente gustoso.

Anche se sei sia il cliente che il cuoco, esiste ancora una significativa distinzione tra:

  • Non ho preparato correttamente questo pasto, non è stato gustoso (= errore di cottura). Una bistecca bruciata non avrà mai un buon sapore, anche se ti piace la bistecca.
  • Ho preparato il pasto correttamente, ma non mi piace (= errore del cliente). Se non ti piace la bistecca, non ti piacerà mai mangiare la bistecca, anche se l'hai cucinata alla perfezione.

Il problema principale qui è che non stai facendo una separazione tra il cliente e lo sviluppatore (e l'analista, anche se quel ruolo può essere rappresentato anche da uno sviluppatore).

È necessario distinguere tra test del codice e test dei requisiti aziendali.

Ad esempio, il cliente desidera che funzioni come [questo] . Tuttavia, lo sviluppatore fraintende e scrive il codice che fa [quello] .

Lo sviluppatore quindi scriverà unit test che testeranno se [che] funziona come previsto. Se ha sviluppato l'applicazione in modo corretto, i suoi test di unità passeranno anche se l'applicazione non fa [questo] , cui il cliente si aspettava.

Se si desidera verificare le aspettative del cliente (i requisiti aziendali), è necessario farlo in una fase separata (e successiva).

Un semplice flusso di lavoro di sviluppo per mostrarti quando eseguire questi test:

  • Il cliente spiega il problema che desidera risolvere.
  • L'analista (o sviluppatore) lo scrive in un'analisi.
  • Lo sviluppatore scrive codice che fa ciò che descrive l'analisi.
  • Lo sviluppatore verifica il suo codice (unit test) per vedere se ha seguito correttamente l'analisi
  • Se i test unitari falliscono, lo sviluppatore torna allo sviluppo. Questo si svolge in modo indefinito, fino a quando l'unità testa tutti i passaggi.
  • Ora avendo una base di codice testata (confermata e superata), lo sviluppatore crea l'applicazione.
  • La domanda è data al cliente.
  • Il cliente ora verifica se l'applicazione che gli viene data risolve effettivamente il problema che ha cercato di risolvere (test di qualità) .

Potresti chiederti qual è il punto di fare due test separati quando il cliente e lo sviluppatore sono la stessa cosa. Dal momento che non c'è "passaggio" dallo sviluppatore al cliente, i test vengono eseguiti uno dopo l'altro, ma sono comunque passaggi separati.

  • I test unitari sono uno strumento specializzato che consente di verificare se la fase di sviluppo è terminata.
  • I test di qualità vengono eseguiti utilizzando l'applicazione .

Se si desidera verificare se l'algoritmo stesso è corretto, questo non fa parte del lavoro dello sviluppatore . Questa è la preoccupazione del cliente e il cliente lo verificherà utilizzando l'applicazione.

In qualità di imprenditore e accademico, potresti perdere qui un'importante distinzione, che evidenzia le diverse responsabilità.

  • Se l'applicazione non aderisce a quanto inizialmente richiesto dal cliente, le successive modifiche al codice vengono generalmente eseguite gratuitamente ; poiché è un errore dello sviluppatore. Lo sviluppatore ha commesso un errore e deve pagare i costi per correggerlo.
  • Se l'applicazione fa ciò che il cliente aveva inizialmente richiesto, ma il cliente ora ha cambiato idea (ad esempio, hai deciso di utilizzare un algoritmo diverso e migliore), le modifiche alla base di codice sono a carico del cliente , poiché non è il colpa dello sviluppatore che il cliente ha chiesto qualcosa di diverso da quello che vogliono ora. È responsabilità del cliente (costo) cambiare idea e pertanto gli sviluppatori devono impegnarsi maggiormente per sviluppare qualcosa che non era stato precedentemente concordato.

Sarei felice di vedere ulteriori elaborazioni sulla situazione "Se hai inventato l'algoritmo da solo", poiché penso che questa sia la situazione che molto probabilmente presenta problemi. Soprattutto nelle situazioni in cui non sono forniti esempi "se A allora B, altro C". (ps I am not the downvoter)
PaintingInAir

@PaintingInAir: Ma non posso davvero approfondire questo perché dipende dalla tua situazione. Se hai deciso di creare questo algoritmo, ovviamente lo hai fatto per fornire una funzione particolare. Chi ti ha chiesto di farlo? Come hanno descritto la loro richiesta? Ti hanno detto cosa dovevano accadere in determinati scenari? (questa informazione è ciò che chiamo "analisi" nella mia risposta) Qualsiasi spiegazione tu abbia ricevuto (che ti ha portato a creare l'algoritmo) può essere usata per verificare se l'algoritmo funziona come richiesto. In breve, è possibile utilizzare qualsiasi cosa tranne l'algoritmo codice / auto-creato .
Flater

2
@PaintingInAir: è pericoloso associare strettamente cliente, analista e sviluppatore; poiché sei incline a saltare passaggi essenziali come la definizione dell'inizio del problema . Credo sia quello che stai facendo qui. Sembra che tu voglia testare la correttezza dell'algoritmo, piuttosto che se sia stato implementato correttamente. Ma non è così che lo fai. Il test dell'implementazione può essere eseguito utilizzando unit test. Il test dell'algoritmo stesso è una questione di utilizzo della tua applicazione (testata) e verifica dei suoi risultati - questo test effettivo non rientra nell'ambito del tuo codebase (come dovrebbe essere ).
Flater,

4
Questa risposta è già enorme. Consiglio vivamente di provare a trovare un modo per riformulare il contenuto originale in modo da poterlo integrare nella nuova risposta se non vuoi buttarlo via.
jpmc26,

7
Inoltre, non sono d'accordo con la tua premessa. I test possono e devono assolutamente rivelare quando il codice genera un output errato in base alle specifiche. È valido per i test per convalidare gli output per alcuni casi di test noti. Inoltre, il cuoco dovrebbe sapere meglio che accettare il "sapone per le mani" come un valido ingrediente per hamburger, e il datore di lavoro ha quasi sicuramente istruito il cuoco su quali ingredienti sono disponibili.
jpmc26,

9

Test di proprietà

A volte le funzioni matematiche sono meglio servite da "Property Testing" che dai tradizionali test unitari basati su esempi. Ad esempio, immagina di scrivere unit test per qualcosa come una funzione "moltiplica" intera. Mentre la funzione stessa può sembrare molto semplice, se è l'unico modo per moltiplicarsi, come si fa a testarla completamente senza la logica nella funzione stessa? È possibile utilizzare tabelle giganti con input / output previsti, ma questo è limitato e soggetto a errori.

In questi casi, è possibile testare le proprietà note della funzione, anziché cercare risultati specifici previsti. Per la moltiplicazione, potresti sapere che la moltiplicazione di un numero negativo e di un numero positivo dovrebbe comportare un numero negativo e che la moltiplicazione di due numeri negativi dovrebbe comportare un numero positivo, ecc. L'uso di valori randomizzati e quindi la verifica che queste proprietà siano conservate per tutti i valori di test sono un buon modo per testare tali funzioni. In genere è necessario verificare più di una proprietà, ma è spesso possibile identificare un insieme finito di proprietà che convalidano insieme il comportamento corretto di una funzione senza necessariamente conoscere il risultato previsto per ogni caso.

Una delle migliori introduzioni al Property Testing che ho visto è questa in F #. Speriamo che la sintassi non sia un ostacolo alla comprensione della spiegazione della tecnica.


1
Suggerirei forse di aggiungere qualcosa di un po 'più specifico nel tuo esempio di moltiplicazione, come la generazione di quartetti casuali (a, b, c) e la conferma che (ab) (cd) produce (ac-ad) - (bc-bd). Un'operazione di moltiplicazione potrebbe essere piuttosto frammentata e mantenere comunque la regola (tempi negativi rendimenti positivi), ma la regola distributiva prevede risultati specifici.
supercat,

4

È allettante scrivere il codice e vedere se il risultato "sembra giusto", ma, come giustamente intuisci, non è una buona idea.

Quando l'algoritmo è difficile, è possibile eseguire una serie di operazioni per semplificare il calcolo manuale del risultato.

  1. Usa Excel. Imposta un foglio di calcolo che esegua alcuni o tutti i calcoli per te. Mantenerlo abbastanza semplice in modo da poter vedere i passaggi.

  2. Dividi il tuo metodo in metodi più piccoli testabili, ognuno con i propri test. Quando sei sicuro che le parti più piccole funzionano, usale per eseguire manualmente il passaggio successivo.

  3. Utilizzare le proprietà aggregate per il controllo di integrità. Ad esempio, supponiamo che tu abbia un calcolatore di probabilità; potresti non sapere quali dovrebbero essere i singoli risultati, ma sai che tutti devono aggiungere fino al 100%.

  4. Forza bruta. Scrivi un programma che genera tutti i possibili risultati e verifica che nessuno sia migliore di quello che genera l'algoritmo.


Per 3., consentire alcuni errori di arrotondamento qui. È possibile che il totale sia pari a 100,000001% o cifre ugualmente vicine ma non esatte.
Flater,

2
Non sono del tutto sicuro su 4. Se sei in grado di generare il risultato ottimale per tutte le possibili combinazioni di input (che poi usi per confermare il test), allora sei intrinsecamente già in grado di calcolare il risultato ottimale e quindi non ' Ho bisogno di questa seconda parte di codice che stai provando a testare. A quel punto, staresti meglio usando il tuo generatore di risultati ottimale esistente poiché ha già dimostrato di funzionare. (e se non ha ancora dimostrato di funzionare, non puoi fare affidamento sul risultato per verificare i tuoi test per iniziare).
Flater,

6
@flater di solito hai altri requisiti e la correttezza che la forza bruta non soddisfa. ad es. performance.
Ewan,

1
@flater Odierei usare la tua specie, il percorso più breve, il motore di scacchi, ecc. se ci credi. Ma il gioco d'azzardo totale nel tuo errore di arrotondamento ha permesso al casinò tutto il giorno
Ewan

3
@flater ti dimetti quando arrivi a una partita di re pedone? solo perché l'intero gioco non può essere brutale forzato non significa che una posizione individuale non possa. Solo perché forza bruta il percorso più breve corretto verso una rete non significa che conosci il percorso più breve in tutte le reti
Ewan

2

TL; DR

Vai alla sezione "test comparativi" per consigli che non sono in altre risposte.


Beginnings

Inizia testando i casi che dovrebbero essere rifiutati dall'algoritmo (zero o negativo workPerDay, per esempio) e i casi che sono banali (ad esempio tasksarray vuoto ).

Successivamente, si desidera prima testare i casi più semplici. Per l' tasksinput, dobbiamo testare diverse lunghezze; dovrebbe essere sufficiente testare 0, 1 e 2 elementi (2 appartiene alla categoria "molti" per questo test).

Se riesci a trovare input che possono essere calcolati mentalmente, è un buon inizio. Una tecnica che a volte uso è quella di partire dal risultato desiderato e tornare indietro (nelle specifiche) agli input che dovrebbero produrre quel risultato.

Test comparativi

A volte la relazione dell'output con l'input non è ovvia, ma quando si modifica un input si ha una relazione prevedibile tra output diversi . Se ho compreso correttamente l'esempio, l'aggiunta di un'attività (senza modificare altri input) non aumenterà mai la percentuale di lavoro svolto in tempo, quindi possiamo creare un test che chiama la funzione due volte, una con e una senza l'attività aggiuntiva - e afferma la disuguaglianza tra i due risultati.

fallback

A volte ho dovuto ricorrere a un lungo commento che mostra un risultato calcolato a mano nei passaggi corrispondenti alle specifiche (un commento di solito è più lungo del caso di test). Il caso peggiore è quando devi mantenere la compatibilità con un'implementazione precedente in una lingua diversa o per un ambiente diverso. A volte devi solo etichettare i dati del test con qualcosa di simile /* derived from v2.6 implementation on ARM system */. Non è molto soddisfacente, ma può essere accettabile come test di fedeltà durante il porting o come stampella a breve termine.

promemoria

L'attributo più importante di un test è la sua leggibilità: se gli input e gli output sono opachi per il lettore, allora il test ha un valore molto basso, ma se il lettore viene aiutato a comprendere le relazioni tra loro, il test ha due scopi.

Non dimenticare di usare un "approssimativo uguale" appropriato per risultati inesatti (ad esempio in virgola mobile).

Evita i test eccessivi: aggiungi un test solo se copre qualcosa (come un valore limite) che non viene raggiunto da altri test.


2

Non c'è nulla di molto speciale in questo tipo di funzione difficile da testare. Lo stesso vale per il codice che utilizza interfacce esterne (ad esempio, un'API REST di un'applicazione di terze parti che non è sotto il tuo controllo e certamente non può essere testata dalla tua suite di test; o utilizzando una libreria di terze parti di cui non sei sicuro del formato esatto di byte dei valori restituiti).

È un approccio abbastanza valido semplicemente eseguire l'algoritmo per un input sano, vedere cosa fa, assicurarsi che il risultato sia corretto e incapsulare l'input e il risultato come test case. Puoi farlo per alcuni casi e quindi ottenere diversi campioni. Prova a rendere i parametri di input il più diversi possibile. Nel caso di una chiamata API esterna, dovresti fare alcune chiamate contro il sistema reale, rintracciarle con qualche strumento e poi deriderle nei test delle unità per vedere come reagisce il tuo programma, il che equivale a scegliere solo alcune esegue il codice di pianificazione delle attività, verificandole manualmente e quindi codificando il risultato nei test.

Quindi, ovviamente, porta in casi limite come (nel tuo esempio) un elenco vuoto di attività; cose del genere.

La tua suite di test potrebbe non essere eccezionale come per un metodo in cui puoi facilmente prevedere i risultati; ma comunque migliore del 100% rispetto a nessuna suite di test (o solo a un test del fumo).

Se il tuo problema, tuttavia, è che trovi difficile decidere se un risultato è corretto, allora questo è un problema completamente diverso. Ad esempio, supponiamo di avere un metodo che rileva se un numero arbitrariamente grande è primo. Difficilmente puoi lanciare un numero casuale su di esso e poi semplicemente "guardare" se il risultato è corretto (supponendo che tu non possa decidere la perfezione nella tua testa o su un pezzo di carta). In questo caso, c'è davvero poco che puoi fare - dovresti ottenere risultati noti (ad esempio, alcuni numeri primi di grandi dimensioni) o implementare la funzionalità con un algoritmo diverso (forse anche un team diverso - la NASA sembra amare quello) e spero che se una delle implementazioni è errata, almeno il bug non porta agli stessi risultati sbagliati.

Se questo è un caso normale per te, allora devi avere una buona chiacchierata con i tuoi ingegneri dei requisiti. Se non riescono a formulare le tue esigenze in un modo che sia facile (o per niente possibile) verificarlo, allora quando sai se hai finito?


2

Altre risposte sono buone, quindi cercherò di colpire alcuni punti che finora hanno perso collettivamente.

Ho scritto (e testato a fondo) un software per eseguire l'elaborazione delle immagini utilizzando il Synthetic Aperture Radar (SAR). È di natura scientifica / numerica (implicano molta geometria, fisica e matematica).

Un paio di suggerimenti (per prove scientifiche / numeriche generali):

1) Usa inversioni. Qual è la fftdi [1,2,3,4,5]? Nessuna idea. Cosa ifft(fft([1,2,3,4,5]))? Dovrebbe essere [1,2,3,4,5](o vicino ad esso, potrebbero sorgere errori in virgola mobile). Lo stesso vale per il caso 2D.

2) Usa assert noti. Se si scrive una funzione determinante, potrebbe essere difficile dire quale sia il determinante di una matrice casuale 100x100. Ma sai che il determinante della matrice identità è 1, anche se è 100x100. Sai anche che la funzione dovrebbe restituire 0 su una matrice non invertibile (come un 100x100 pieno di tutti gli 0).

3) Usa asserzioni approssimative anziché affermazioni esatte . Ho scritto del codice per la suddetta elaborazione SAR che registrava due immagini generando punti di legame che creano una mappatura tra le immagini e quindi eseguendo una distorsione tra loro per farle corrispondere. Potrebbe registrarsi a un livello sub-pixel. A priori, è difficile dire qualcosa su come potrebbe essere la registrazione di due immagini. Come puoi provarlo? Cose come:

EXPECT_TRUE(register(img1, img2).size() < min(img1.size(), img2.size()))

poiché è possibile registrarsi solo su parti sovrapposte, l'immagine registrata deve essere più piccola o uguale all'immagine più piccola e anche:

scale = 255
EXPECT_PIXEL_EQ_WITH_TOLERANCE(reg(img, img), img, .05*scale)

poiché un'immagine registrata su se stessa dovrebbe essere CHIUSA su se stessa, ma potresti riscontrare un po 'più di errori in virgola mobile dovuti all'algoritmo a portata di mano, quindi controlla che ogni pixel sia entro +/- 5% dell'intervallo che i pixel possono assumere (0-255 è in scala di grigi, comune nell'elaborazione delle immagini). Il risultato dovrebbe avere almeno le stesse dimensioni dell'input.

Puoi anche solo fumare test (es. Chiamalo e assicurati che non si blocchi). In generale, questa tecnica è migliore per test più grandi in cui il risultato finale non può essere (facilmente) calcolato a priori per l'esecuzione del test.

4) Usa O MEMORIZZA un seme di numero casuale per il tuo RNG.

Corre non hanno bisogno di essere riproducibile. È falso, tuttavia, che l'unico modo per ottenere una corsa riproducibile sia fornire un seme specifico a un generatore di numeri casuali. A volte il test di casualità è prezioso. Ho visto / sentito parlare di bug nel codice scientifico che sorgono in casi degeneri che sono stati generati casualmente (in algoritmi complicati può essere difficile vedere che cosa è anche il caso degenerato). Invece di chiamare sempre la tua funzione con lo stesso seme, genera un seme casuale, quindi usa quel seme e registra il valore del seme. In questo modo ogni esecuzione ha un seme casuale diverso, ma se si verifica un arresto anomalo, è possibile rieseguire il risultato utilizzando il seme registrato per il debug. In realtà l'ho usato in pratica e ha risolto un bug, quindi ho pensato di menzionarlo. Certo, questo è successo solo una volta, e sono sicuro che non sempre valga la pena farlo, quindi usa questa tecnica con prudenza. Casualmente con lo stesso seme è sempre sicuro. Unico inconveniente (invece di usare sempre lo stesso seme per tutto il tempo): devi registrare le tue prove. Upside: correttezza e bug nuking.

Il tuo caso particolare

1) Verifica che un vuoto taskArray restituisca 0 (asserzione nota).

2) Generare input casuali tale che task.time > 0 , task.due > 0, e task.importance > 0 per tutti task s, e affermare il risultato è maggiore di 0 (asserzione ruvida, casuale di ingresso) . Non devi impazzire e generare semi casuali, il tuo algoritmo non è abbastanza complesso da giustificarlo. Ci sono circa 0 possibilità che sarebbe ripagato: mantieni il test semplice.

3) Prova se task.importance == 0 per tutte le task s, quindi il risultato è 0 (affermazione nota)

4) Altre risposte toccate su questo, ma potrebbe essere importante per il tuo caso particolare : se stai creando un'API per essere utilizzata da utenti esterni al tuo team, devi testare i casi degenerati. Ad esempio, se workPerDay == 0, assicurati di lanciare un errore adorabile che dice all'utente che l'input non è valido. Se non stai creando un'API, ed è solo per te e il tuo team, probabilmente puoi saltare questo passaggio e semplicemente rifiutarti di chiamarlo con il caso degenerato.

HTH.


1

Incorporare test di asserzione nella suite di unit test per test basati su proprietà dell'algoritmo. Oltre a scrivere unit test che controllano l'output specifico, scrivere test progettati per fallire innescando errori di asserzione nel codice principale.

Molti algoritmi si affidano alle prove di correttezza per mantenere determinate proprietà durante le fasi dell'algoritmo. Se riesci a controllare sensibilmente queste proprietà osservando l'output di una funzione, il solo test unitario è sufficiente per testare le tue proprietà. Altrimenti, i test basati su asserzioni consentono di verificare che un'implementazione mantenga una proprietà ogni volta che l'algoritmo la assume.

I test basati su asserzioni esporranno difetti dell'algoritmo, errori di codifica e errori di implementazione dovuti a problemi come l'instabilità numerica. Molte lingue hanno meccanismi che eliminano le asserzioni in fase di compilazione o prima che il codice venga interpretato in modo tale che quando eseguite in modalità di produzione le asserzioni non comportano una penalità di prestazione. Se il codice supera i test unitari ma non riesce in un caso reale, è possibile riattivare le asserzioni come strumento di debug.


1

Alcune delle altre risposte qui sono molto buone:

  • Basi di prova, bordi e custodie angolari
  • Eseguire controlli di integrità
  • Eseguire test comparativi

... aggiungerei alcune altre tattiche:

  • Decomponi il problema.
  • Prova l'algoritmo al di fuori del codice.
  • Verifica che l'algoritmo [provato esternamente] sia implementato come progettato.

La decomposizione ti consente di garantire che i componenti dell'algoritmo facciano ciò che ti aspetti che facciano. E una "buona" decomposizione ti consente anche di assicurarti che siano incollati insieme correttamente. Una grande decomposizione generalizza e semplifica l'algoritmo nella misura in cui tu possibile prevedere i risultati (dell'algoritmo (i) generico (i) semplificato e generico) abbastanza bene da scrivere test approfonditi.

Se non riesci a decomporsi in tale misura, prova l'algoritmo al di fuori del codice con qualsiasi mezzo sia sufficiente per soddisfare te e i tuoi colleghi, stakeholder e clienti. E poi, basta scomporre abbastanza per dimostrare che l'implementazione corrisponde al design.


0

Potrebbe sembrare una risposta idealistica, ma aiuta a identificare diversi tipi di test.

Se le risposte rigorose sono importanti per l'implementazione, è necessario fornire esempi e risposte attese nei requisiti che descrivono l'algoritmo. Questi requisiti devono essere esaminati in gruppo e se non si ottengono gli stessi risultati, è necessario identificare il motivo.

Anche se stai interpretando il ruolo di analista e di implementatore, dovresti effettivamente creare requisiti e farli revisionare molto prima di scrivere unit test, quindi in questo caso conoscerai i risultati previsti e potrai scrivere i test di conseguenza.

D'altra parte, se questa è una parte che stai implementando che non fa parte della logica di business o supporta una risposta di logica di business, allora dovrebbe andare bene eseguire il test per vedere quali sono i risultati e quindi modificare il test per aspettarti quei risultati. I risultati finali sono già stati confrontati con i tuoi requisiti, quindi se sono corretti allora tutto il codice che alimenta quei risultati finali deve essere numericamente corretto e a quel punto i test delle unità sono più per rilevare casi di guasti ai bordi e future modifiche di refactoring che per dimostrare che un dato l'algoritmo produce risultati corretti.


0

Penso che sia perfettamente accettabile in alcune occasioni seguire il processo:

  • progettare un caso di prova
  • usa il tuo software per ottenere la risposta
  • controlla la risposta a mano
  • scrivere un test di regressione in modo che le versioni future del software continuino a fornire questa risposta.

Questo è un approccio ragionevole in qualsiasi situazione in cui è più semplice verificare manualmente la correttezza di una risposta che calcolare la risposta a mano dai primi principi.

Conosco persone che scrivono software per il rendering di pagine stampate e hanno test che controllano che sulla pagina stampata siano impostati esattamente i pixel giusti. L'unico modo sensato per farlo è scrivere il codice per rendere la pagina, controllare ad occhio che appaia bene e quindi catturare il risultato come test di regressione per le versioni future.

Solo perché hai letto in un libro che una particolare metodologia incoraggia a scrivere prima i casi di test, non significa che devi sempre farlo in quel modo. Le regole sono lì per essere infrante.


0

Altre risposte Le risposte hanno già delle tecniche per l'aspetto di un test quando il risultato specifico non può essere determinato al di fuori della funzione testata.

Ciò che faccio inoltre, che non ho notato nelle altre risposte, è di generare automaticamente i test in qualche modo:

  1. Ingressi 'casuali'
  2. Iterazione attraverso intervalli di dati
  3. Costruzione di casi di test da insiemi di confini
  4. Tutto quanto sopra.

Ad esempio, se la funzione accetta tre parametri ciascuno con intervallo di input consentito [-1,1], testare tutte le combinazioni di ciascun parametro, {-2, -1,01, -1, -0,99, -0,5, -0,01, 0,0,01 , 0,5,0,99,1,1,01,2, alcuni più casuali in (-1,1)}

In breve: a volte la scarsa qualità può essere sovvenzionata dalla quantità.

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.