Come si esegue il test unitario del codice utilizzando le strutture grafiche?


18

Sto scrivendo un codice (ricorsivo) che sta navigando in un grafico delle dipendenze cerca cicli o contraddizioni nelle dipendenze. Tuttavia, non sono sicuro di come affrontare questo test unitario. Il problema è che una delle nostre principali preoccupazioni è che il codice gestirà tutte le strutture grafiche interessanti che possono sorgere e assicurandosi che tutti i nodi vengano gestiti in modo appropriato.

Mentre di solito la copertura al 100% delle linee o delle filiali è sufficiente per essere certi che un po 'di codice funzioni, sembra che anche con una copertura del percorso al 100% avresti ancora dubbi.

Quindi, come si fa a selezionare le strutture grafiche per i casi di test per avere fiducia che il loro codice possa gestire tutte le possibili ipotesi che troverai nei dati del mondo reale.


PS: se è importante, tutti i bordi nel mio grafico sono etichettati "deve avere" xor "non può avere" e non ci sono cicli banali, e c'è solo un margine tra due nodi.


PPS- Questa ulteriore dichiarazione di problema è stata originariamente pubblicata dall'autore della domanda in un commento qui sotto:

For all vertices N in forest F, for all vertices M, in F, such that if there are any walks between N and M they all must either use only edges labelled 'conflict' or 'requires'.


13
Allo stesso modo in cui testate qualsiasi altro metodo. Identifichi tutti i casi di test "interessanti" per ciascun metodo e scrivi i test unitari per essi. Nel tuo caso, dovrai creare grafici di dipendenze predefiniti per ciascuna delle strutture "interessanti" del grafico.
Dunk,

@Dunk Continuiamo a pensare che abbiamo coperto tutti quelli difficili e poi ci rendiamo conto che una certa struttura causa problemi che non avevamo mai considerato prima. Testare ogni trucco a cui possiamo pensare è quello che stiamo facendo, quello che spero di trovare sono alcune linee guida / procedure per generare esempi fastidiosi, magari usando la riducibilità delle forme fondamentali ecc.
Slitta,

6
Questo è il problema con qualsiasi forma di test. Tutto quello che sai è che i test che hai pensato funzionassero. Ciò non significa che il tuo sw sia privo di errori solo perché i test superano. Ogni progetto ha lo stesso problema. Sono nelle fasi finali della consegna del mio attuale progetto in modo da poter iniziare la produzione. I tipi di errori che incontriamo ora tendono ad essere piuttosto oscuri. Ad esempio, dove l'hardware funziona ancora secondo le specifiche, ma a malapena e quando combinato con altri hardware contemporaneamente con lo stesso problema, si verificano problemi; ma solo a volte :( Il sw è ben testato ma non abbiamo pensato a tutto
Dunk il

Quello che descrivi sembra più un test di integrazione e non un test unitario. I test unitari assicurano che un metodo sia in grado di trovare i cerchi in un grafico. Altri test unitari assicurerebbero che un cerchio specifico di un grafico specifico sia gestito dalla classe sottoposta a test.
SpaceTrucker

Il rilevamento del ciclo è un argomento ben trattato (vedi Knuth, e anche alcune risposte di seguito) e le soluzioni non comportano un gran numero di casi speciali, quindi dovresti prima determinare quale sia il problema in questo modo. È a causa delle contraddizioni che menzioni? In tal caso, abbiamo bisogno di ulteriori informazioni al riguardo. Se è il risultato di scelte di implementazione, potresti dover fare il refactoring, forse in grande stile. Fondamentalmente, questo è un problema di progettazione che dovrai affrontare, TDD è l'approccio sbagliato che può portarti in profondità nel labirinto prima del tuo vicolo cieco.
sdenham,

Risposte:


5

Continuiamo a pensare che abbiamo coperto tutti quelli difficili e poi ci rendiamo conto che una certa struttura causa problemi che non avevamo mai considerato prima. Testare ogni trucco a cui possiamo pensare è quello che stiamo facendo.

Sembra un buon inizio. Immagino che tu abbia già provato ad applicare alcune tecniche classiche come l' analisi del valore limite o il partizionamento di equivalenza , come hai già menzionato i test basati sulla copertura. Dopo aver investito molto tempo nella costruzione di buoni casi di test, arriverai al punto in cui tu, il tuo team e anche i vostri tester (se ne avete) perdono le idee. E questo è il punto in cui dovresti lasciare il percorso dei test unitari e iniziare i test con il maggior numero possibile di dati del mondo reale.

Dovrebbe essere ovvio che dovresti provare a scegliere una grande varietà di grafici dai tuoi dati di produzione. Forse devi scrivere alcuni strumenti o programmi aggiuntivi solo per quella parte del processo. La parte difficile qui è probabilmente quella di verificare la correttezza dell'output dei tuoi programmi, quando inserirai diecimila diversi grafici del mondo reale nel tuo programma, come fai a sapere se il tuo programma produce sempre l'output corretto? Ovviamente non puoi controllare che manualmente. Quindi, se sei fortunato, potresti essere in grado di eseguire una seconda, molto semplice implementazione del tuo controllo delle dipendenze, che potrebbe non soddisfare le tue aspettative di prestazione, ma è più facile da verificare rispetto al tuo algoritmo originale. Dovresti anche provare a integrare molti controlli di plausibilità direttamente nel tuo programma (ad esempio,

Infine, impara ad accettare che ogni test può solo provare l'esistenza di bug, ma non l'assenza di bug.


5

1. Generazione di test randomizzati

Scrivi un algoritmo che genera grafici, fai generare qualche centinaio (o più) grafici casuali e lancia ciascuno di essi sul tuo algoritmo.

Mantenere il seme casuale dei grafici che causano errori interessanti e aggiungerli come test unitari.

2. Parti difficili del codice

Alcune strutture grafiche che sai sono complicate da poter scrivere subito o scrivere del codice che le combina e le inserisce nel tuo algoritmo.

3. Genera un elenco completo

Ma, se vuoi essere sicuro "il codice potrebbe gestire tutte le possibili ipotesi che troverai nei dati del mondo reale", devi generare questi dati non da seme casuale, ma camminando attraverso tutte le permutazioni. (Questo viene fatto quando si testano i sistemi di segnali ferroviari della metropolitana e ti dà enormi quantità di casi che richiedono anni per essere testati. Per le metropolitane della metropolitana, il sistema è limitato, quindi esiste un limite superiore al numero di permutazioni. Non sono sicuro di come il tuo caso si applica)


L'interrogante ha scritto che non sono in grado di dire se hanno preso in considerazione tutti i casi, il che implica che non hanno un modo per enumerarli. Fino a quando non comprendono abbastanza bene il dominio problematico per farlo, come testare è una domanda controversa.
sdenham,

@sdenham Come hai intenzione di enumerare qualcosa che a livello mediterraneo ha un numero infinito di possibili combinazioni valide? Speravo di trovare qualcosa sulla falsariga di "queste sono le strutture grafiche più difficili che spesso cattureranno bug nella tua implementazione". Capisco il dominio abbastanza bene in quanto è semplice: For all vertices N in forest F, for all vertices M, in F, such that if there are any walks between N and M they all must either use only edges labelled 'conflict' or 'requires'.il dominio non è il problema.
Slitta

@ArtB: grazie per il chiarimento del problema. Come hai detto, non c'è più di un bordo tra due vertici e apparentemente escludono percorsi con cicli (o almeno più di un passaggio attorno a qualsiasi ciclo), almeno sappiamo che non c'è letteralmente un numero infinito di possibili combinazioni valide, che è progresso. Notare che saper elencare tutte le possibilità non equivale a dire che è necessario farlo, poiché potrebbe essere un punto di partenza per formulare un argomento per la correttezza, che a sua volta può guidare i test. Ci
penserò

@ArtB: è necessario modificare la domanda per includere l'aggiornamento all'istruzione del problema fornita qui. Inoltre, può essere utile affermare che si tratta di bordi diretti (in tal caso) e se un ciclo sarebbe considerato un errore nel grafico, anziché solo una situazione che l'algoritmo deve gestire.
sdenham,

4

Nessun test sarà in grado di essere sufficiente in questo caso, nemmeno tonnellate di dati del mondo reale o fuzzing. Una copertura del codice al 100% o anche una copertura del percorso al 100% non è sufficiente per testare le funzioni ricorsive.

O la funzione ricorsiva resiste a una prova formale (non dovrebbe essere così difficile in questo caso), oppure no. Se il codice è troppo intrecciato con il codice specifico dell'applicazione per escludere effetti collaterali, è da qui che iniziare.

L'algoritmo stesso suona come un semplice algoritmo di flooding, simile a una semplice prima ampia ricerca, con l'aggiunta di una lista nera che non deve intersecarsi con l'elenco dei nodi visitati, eseguito da tutti i nodi.

foreach nodes as node
    foreach nodes as tmp
        tmp.status = unmarked

    tovisit = []
    tovisit.push(node)
    node.status = required

    while |tovisit| > 0 do
        next = tovisit.pop()
        foreach next.requires as requirement
            if requirement.status = unmarked
                tovisit.push(requirement)
                requirement.status = required
            else if requirement.status = blacklisted
                return false
        foreach next.collides as collision
            if collision.status = unmarked
                requirement.status = blacklisted
            else if requirement.status = required
                return false
return true

Questo algoritmo iterativo soddisfa la condizione che nessuna dipendenza possa essere richiesta e contemporaneamente inserita nella lista nera, per grafici di struttura arbitraria, a partire da qualsiasi artefatto arbitrario per cui è sempre richiesto l'artefatto iniziale.

Sebbene possa essere o meno veloce quanto la tua implementazione, può essere dimostrato che termina per tutti i casi (come per ogni iterazione del ciclo esterno ogni elemento può essere inserito una sola volta nella tovisitcoda), inonda l'intero raggiungibile grafico (prova induttiva), e rileva tutti i casi in cui è richiesto un manufatto che deve essere richiesto e inserito nella lista nera contemporaneamente, a partire da ciascun nodo.

Se riesci a dimostrare che la tua implementazione ha le stesse caratteristiche, puoi provare la correttezza senza comportare test unitari. Solo i metodi di base per spingere e saltar fuori dalle code, contare la lunghezza della coda, iterare su proprietà e simili devono essere testati e mostrati come privi di effetti collaterali.

EDIT: Ciò che questo algoritmo non dimostra, è che il tuo grafico è privo di cicli. I grafici aciclici diretti sono un argomento ben studiato, quindi trovare un algoritmo pronto per dimostrare questa proprietà dovrebbe essere altrettanto facile.

Come puoi vedere, non è necessario reinventare la ruota.


3

Stai usando frasi come "tutte le strutture grafiche interessanti" e "gestite in modo appropriato". A meno che non ci siano modi per testare il codice su tutte quelle strutture e determinare se il codice gestisce il grafico in modo appropriato, è possibile utilizzare solo strumenti come l'analisi della copertura del test.

Ti suggerisco di iniziare trovando e testando una serie di strutture grafiche interessanti e determinare quale sarebbe la gestione appropriata e vedere che il codice lo fa. Quindi, puoi iniziare a perturbare quei grafici in a) grafici rotti che violano le regole o b) grafici non così interessanti che hanno problemi; vedere se il codice non riesce a gestirli correttamente.


Sebbene questo sia un buon approccio ai test, non risolve il problema centrale della domanda: come garantire che tutti i casi siano coperti. Ciò, credo, richiederà ulteriori analisi e possibilmente refactoring - vedi la mia domanda sopra.
sdenham,


2

Quando si tratta di questo tipo di algoritmo difficile da testare, sceglierei il TDD, dove si costruisce l'algoritmo basato su test,

TDD in breve,

  • scrivi il test
  • vedi che sta fallendo
  • modifica il codice
  • assicurarsi che tutti i test abbiano superato
  • refactoring

e ripeti il ​​ciclo,

In questa situazione particolare,

  1. Il primo test sarebbe, grafico a nodo singolo in cui l'algoritmo non dovrebbe restituire alcun ciclo
  2. Il secondo sarebbe un grafico a tre nodi senza ciclo in cui l'algoritmo non dovrebbe restituire alcun ciclo
  3. Il prossimo sarebbe usare un grafico a tre nodi con un ciclo in cui l'algoritmo non dovrebbe restituire alcun ciclo
  4. Ora potresti testarlo con un ciclo un po 'più complesso a seconda delle possibilità

Un aspetto importante di questo metodo è che devi sempre aggiungere un test per l'eventuale passaggio (in cui dividi possibili scenari in semplici test) e quando copri tutti gli scenari possibili, l'algoritmo si evolve automaticamente.

Infine, devi aggiungere uno o più test di integrazione complicati per vedere se ci sono problemi imprevisti (come errori di overflow dello stack / errori di prestazioni quando il tuo grafico è molto grande e quando usi la ricorsione)


2

La mia comprensione del problema, come inizialmente affermato e quindi aggiornato dai commenti nella risposta di Macke, include quanto segue: 1) entrambi i tipi di bordi (dipendenze e conflitti) sono diretti; 2) se due nodi sono collegati da un bordo, non devono essere collegati da un altro, anche se è dell'altro tipo o al contrario; 3) se un percorso tra due nodi può essere costruito mescolando bordi di tipi diversi, allora questo è un errore, piuttosto che una circostanza che viene ignorata; 4) Se esiste un percorso tra due nodi che utilizzano i bordi di un tipo, allora potrebbe non esserci un altro percorso tra loro che utilizza i bordi dell'altro tipo; 5) non sono consentiti cicli di un singolo tipo di bordo o di tipi di bordi misti (da un'ipotesi dell'applicazione, non sono sicuro che i cicli di solo conflitto siano un errore, ma questa condizione può essere rimossa, in caso contrario).

Inoltre, supporrò che la struttura dei dati utilizzata non impedisca l'espressione di violazioni di questi requisiti (ad esempio, un grafico che viola la condizione 2 non potrebbe essere espresso in una mappa dalla coppia di nodi a (tipo, direzione) se la coppia di nodi sempre ha prima il nodo meno numerato.) Se alcuni errori non possono essere espressi, riduce il numero di casi da considerare.

In realtà ci sono tre grafici che possono essere considerati qui: i due di un solo tipo di bordo e il grafico misto formato dall'unione di uno di ciascuno dei due tipi. Puoi usarlo per generare sistematicamente tutti i grafici fino a un certo numero di nodi. Innanzitutto genera tutti i possibili grafici di N nodi che non hanno più di un bordo tra due coppie ordinate di nodi (coppie ordinate perché questi sono grafici diretti.) Ora prendi tutte le possibili coppie di questi grafici, uno che rappresenta le dipendenze e l'altro che rappresenta i conflitti, e forma l'unione di ogni coppia.

Se la struttura dei dati non è in grado di esprimere violazioni della condizione 2, è possibile ridurre in modo significativo i casi da considerare costruendo solo tutti i possibili grafici di conflitto che si adattano agli spazi dei grafici di dipendenza o viceversa. Altrimenti, puoi rilevare violazioni della condizione 2 mentre formi l'unione.

Su un primo attraversamento del grafico combinato dal primo nodo, è possibile creare l'insieme di tutti i percorsi per ogni nodo raggiungibile e, mentre lo si fa, è possibile verificare la presenza di violazioni di tutte le condizioni (per il rilevamento del ciclo, è possibile usa l'algoritmo di Tarjan .)

Devi solo considerare i percorsi dal primo nodo, anche se il grafico è disconnesso, perché i percorsi di qualsiasi altro nodo appariranno come percorsi dal primo nodo in qualche altro caso.

Se i percorsi a bordo misto possono semplicemente essere ignorati, piuttosto che essere errori (condizione 3), è sufficiente considerare i grafici di dipendenza e conflitto in modo indipendente e verificare che se un nodo è raggiungibile in uno, non è nell'altro.

Se ricordi i percorsi trovati nell'esame dei grafici dei nodi N-1, puoi usarli come punto di partenza per generare e valutare i grafici dei nodi N.

Questo non genera più spigoli dello stesso tipo tra i nodi, ma potrebbe essere esteso per farlo. Ciò aumenterebbe notevolmente il numero di casi, quindi sarebbe meglio se il codice in prova rendesse impossibile rappresentare o, in mancanza, filtrare in anticipo tutti questi casi.

La chiave per scrivere un oracolo come questo è di mantenerlo il più semplice possibile, anche se ciò significa essere inefficienti, in modo da poter stabilire fiducia in esso (idealmente attraverso argomenti per la sua correttezza, supportato da test).

Una volta che hai i mezzi per generare casi di test e ti fidi dell'oracolo che hai creato per separare accuratamente il bene dal male, potresti usarlo per guidare il test automatizzato del codice target. Se ciò non è possibile, la tua prossima opzione migliore è quella di esaminare i risultati per casi distintivi. L'oracolo può classificare gli errori che trova e darti alcune informazioni sui casi accettati, come il numero e la lunghezza dei percorsi di ciascun tipo e se ci sono nodi che sono all'inizio di entrambi i tipi di percorso, e questo potrebbe aiutarti a cercare casi che non hai mai visto prima.

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.