È fattibile e utile generare automaticamente alcuni codici di test unitari?

5

All'inizio di oggi mi è venuta un'idea, basata su un particolare caso d'uso reale, che avrei voluto verificare per fattibilità e utilità. Questa domanda sarà caratterizzata da una buona parte del codice Java, ma può essere applicata a tutte le lingue in esecuzione all'interno di una VM e forse anche al di fuori. Sebbene esista un codice reale, non utilizza nulla di specifico della lingua, quindi per favore leggilo principalmente come pseudo codice.

L'idea
Rendi il testing delle unità meno ingombrante aggiungendo in qualche modo all'autogenerazione del codice in base all'interazione umana con il codebase. Capisco che questo sia contrario al principio del TDD, ma non penso che nessuno abbia mai provato che fare TDD è meglio prima di creare il codice e poi immediatamente dopo i test. Questo può anche essere adattato per adattarsi al TDD, ma questo non è il mio obiettivo attuale.

Per mostrare come è destinato a essere usato, copierò qui uno dei miei corsi, per il quale ho bisogno di fare test unitari.

public class PutMonsterOnFieldAction implements PlayerAction {
    private final int handCardIndex;
    private final int fieldMonsterIndex;

    public PutMonsterOnFieldAction(final int handCardIndex, final int fieldMonsterIndex) {
        this.handCardIndex = Arguments.requirePositiveOrZero(handCardIndex, "handCardIndex");
        this.fieldMonsterIndex = Arguments.requirePositiveOrZero(fieldMonsterIndex, "fieldCardIndex");
    }

    @Override
    public boolean isActionAllowed(final Player player) {
        Objects.requireNonNull(player, "player");
        Hand hand = player.getHand();
        Field field = player.getField();
        if (handCardIndex >= hand.getCapacity()) {
            return false;
        }
        if (fieldMonsterIndex >= field.getMonsterCapacity()) {
            return false;
        }
        if (field.hasMonster(fieldMonsterIndex)) {
            return false;
        }
        if (!(hand.get(handCardIndex) instanceof MonsterCard)) {
            return false;
        }
        return true;
    }

    @Override
    public void performAction(final Player player) {
        Objects.requireNonNull(player);
        if (!isActionAllowed(player)) {
            throw new PlayerActionNotAllowedException();
        }
        Hand hand = player.getHand();
        Field field = player.getField();
        field.setMonster(fieldMonsterIndex, (MonsterCard)hand.play(handCardIndex));
    }
}

Possiamo osservare la necessità dei seguenti test:

  • Test del costruttore con input valido
  • Test del costruttore con input non validi
  • isActionAllowed test con input valido
  • isActionAllowed test con input non validi
  • performAction test con input valido
  • performAction test con input non validi

La mia idea si concentra principalmente sul test isActionAllowed con input non validi. Scrivendo questi test non è divertente, è necessario garantire una serie di condizioni e di verificare se esso restituisce realmente false , questo può essere estesa a performAction , dove un'eccezione deve essere gettato in quel caso.

L'obiettivo della mia idea è quello di generare quei test, indicando (attraverso la GUI di IDE si spera) che si desidera generare i test sulla base di un ramo specifico.

L'implementazione con l'esempio

  1. L'utente fa clic su "Genera codice per il ramo if (handCardIndex >= hand.getCapacity()) ".
  2. Ora lo strumento deve trovare un caso in cui sia conservato.

    (Non ho aggiunto il codice rilevante in quanto potrebbe ingombrare il post in ultima analisi)

  3. Per invalidare il ramo, lo strumento ha bisogno di trovare un handCardIndex e hand.getCapacity() in modo tale che la condizione >= detiene.

  4. Ha bisogno di costruire un Player con un Hand che abbia una capacità di almeno 1.
  5. Si noti che il capacity int privato di Hand deve essere almeno 1.
  6. Cerca i modi per impostarlo su 1. Fortunatamente trova un costruttore che prende come argomento capacity . Usa 1 per questo.
  7. Occorre fare ancora un po 'di lavoro per costruire con successo un'istanza di Player , coinvolgendo la creazione di oggetti che hanno dei vincoli visibili ispezionando il codice sorgente.
  8. Ha trovato hand con la minima capacità possibile ed è in grado di costruirlo.
  9. Ora per invalidare il test sarà necessario impostare handCardIndex = 1 .
  10. Costruisce il test e asserisce che sia falso (il valore restituito del ramo)

Che cosa deve funzionare lo strumento?
Per funzionare correttamente, avrà bisogno della possibilità di eseguire la scansione di tutto il codice sorgente (incluso il codice JDK) per capire tutti i vincoli. Opzionalmente questo potrebbe essere fatto tramite javadoc, ma questo non è sempre usato per indicare tutti i vincoli. Potrebbe anche eseguire alcuni tentativi ed errori, ma si ferma praticamente se non è possibile allegare codice sorgente a classi compilate.

Quindi ha bisogno di alcune conoscenze di base su cosa sono i tipi primitivi , inclusi gli array. E deve essere in grado di costruire una qualche forma di "alberi di modifica". Lo strumento sa che è necessario cambiare una certa variabile in un valore diverso per ottenere il testcase corretto. Quindi dovrà elencare tutti i modi possibili per cambiarlo, senza usare ovviamente il riflesso.

Ciò che questo strumento non sostituirà è la necessità di creare test unitari su misura per testare tutti i tipi di condizioni quando un determinato metodo funziona effettivamente. È puramente utile per testare i metodi quando invalidano i vincoli.

Le mie domande :

  • La creazione di uno strumento di questo tipo fattibile ? Funzionerebbe mai, o ci sono dei problemi evidenti?
  • Uno strumento del genere sarebbe utile ? È addirittura utile generare automaticamente questi testicoli? Potrebbe essere esteso per fare anche cose più utili?
  • Esiste per caso un progetto del genere e reinventerò la ruota?

Se non è stato dimostrato utile, ma è ancora possibile farlo, lo considererò comunque per divertimento. Se è considerato utile, allora potrebbe creare un progetto open source per questo a seconda del tempo.

Per le persone che cercano ulteriori informazioni di base sulla utilizzata Player e Hand classi nel mio esempio, si prega di fare riferimento a questo repository . Al momento della scrittura, PutMonsterOnFieldAction non è stato ancora caricato sul repository, ma ciò avverrà una volta terminato il test delle unità.

    
posta skiwi 29.05.2014 - 15:53
fonte

5 risposte

1

È tutto solo un software, quindi è sufficiente lo sforzo necessario ;-). Anche in un linguaggio che supporta un modo decente di fare analisi del codice dovrebbe essere fattibile.

Per quanto riguarda l'utilità, ritengo che un'automazione attorno al testing delle unità sia utile per un certo livello, a seconda di come è implementata. È necessario essere chiari su dove si vuole andare in anticipo, ed essere molto consapevoli dei limiti di questo tipo di strumenti.

Tuttavia, il flusso che descrivi ha un enorme limite, perché il codice da testare conduce il test. Ciò significa che un errore nel ragionamento durante lo sviluppo del codice finirà probabilmente anche nel test. Il risultato finale sarà probabilmente un test che sostanzialmente conferma il codice "fa quello che fa" invece di confermare che fa quello che dovrebbe fare. Questo in realtà non è completamente inutile perché può essere utilizzato in seguito per verificare che il codice faccia ancora ciò che ha fatto in precedenza, ma non è un test funzionale e non si dovrebbe considerare il codice testato in base a tale test. (Potresti ancora cogliere alcuni errori superficiali, come la gestione nulla, ecc.) Non inutile, ma non può sostituire un test 'reale'. Se ciò significa che dovrai ancora creare il test "reale", potrebbe non valerne la pena.

Potresti fare percorsi leggermente diversi con questo però. Il primo è semplicemente il lancio di dati nell'API per vedere dove si interrompe, forse dopo aver definito alcune affermazioni generiche sul codice. Questo è fondamentalmente Test dei fuzz

L'altro sarebbe quello di generare i test ma senza le asserzioni, in pratica saltare il passaggio 10. Quindi finire con un 'quiz API' dove i tuoi strumenti determinano casi di test utili e chiedono al tester la risposta attesa data una chiamata specifica . In questo modo stai di nuovo testando. Tuttavia non è ancora completo, se si dimentica uno scenario funzionale nel codice lo strumento non lo troverà magicamente e non rimuoverà tutte le ipotesi. Supponiamo che il codice debba essere if (handCardIndex > hand.getCapacity()) invece se > =, uno strumento come questo non lo scoprirà mai da solo. Se vuoi tornare a TDD potresti suggerire casi di test basati solo sull'interfaccia, ma ciò sarebbe ancora più funzionale dal punto di vista funzionale, perché non c'è nemmeno il codice da cui puoi dedurre qualche funzionalità.

I tuoi problemi principali saranno sempre 1. Errori di trasporto nel codice fino al test e 2. completezza funzionale. Entrambi i problemi possono essere soppressi, non essere mai eliminati. Dovrai sempre tornare ai requisiti e sederti per verificare che siano tutti effettivamente testati correttamente. Il chiaro pericolo qui è un falso senso di sicurezza perché la copertura del codice mostra una copertura del 100%. Prima o poi qualcuno farà questo errore, sta a te decidere se i benefici superano quel rischio. IMHO si riduce a un classico compromesso tra qualità e velocità di sviluppo.

    
risposta data 01.07.2014 - 12:35
fonte
1

TLDR : è una cattiva idea (vedi sotto per leggere perché).

In primo luogo, volevo risolvere questo problema:

I understand this goes against the principle of TDD, but I don't think anyone ever proved that doing TDD is better over first creating code and then immediatly therafter the tests. This may even be adapted to be fit into TDD, but that is not my current goal.

TDD è meglio scrivere prima il codice (sì, è stato dimostrato).

Ecco alcuni dei vantaggi (puoi anche leggere di questi in molti articoli e libri online):

  • TDD ti assicura di non scrivere cose che non ti servono (ad esempio "scrivi codice minimo per il test da passare")
  • TDD garantisce che l'interfaccia della tua API sia ottimizzata dal punto di vista del cliente, anziché dal punto di vista del implementatore. Il primo, assicura che il codice sarà più facile da usare, per il codice cliente. Il secondo tende a perfezionare l'API pubblica del tuo modulo con i dettagli di implementazione (anche quando fai attenzione a impedirlo).
  • TDD ti assicura di scrivere codice sulle tue specifiche, non su un'idea astratta del tuo algoritmo
  • prima scrivendo il codice, trovi molte situazioni in cui ritieni che il codice funzioni, e "non vedo alcun motivo per scrivere un test extra solo per un caso d'angolo difficile da raggiungere" (noto anche come "pigrizia da sviluppatore", "molto cattiva traduzione di YAGNI "e" programmatore scusa numero 8 "). Con TDD non hai la scusa. Potrebbe non sembrare molto, finché non lavori in una squadra e i tuoi colleghi ti danno una scusa - o ti chiamano per farti del male.
  • TDD riduce al minimo lo sforzo (in genere, quando si scrive il codice per primo, il ciclo di sviluppo è "implementare, scrivere test, modificare / correggere l'implementazione per i test da passare"; con TDD, è "scrivere test, implementare fino al test" può essere molto più breve).

Is creating such a tool feasible? Would it ever work, or are there some obvious problems?

Dato abbastanza risorse, è teoricamente possibile creare un tale strumento.

In pratica, è difficile che tu possa fare qualcosa del genere, che si adatta perfettamente alle implementazioni generiche. È abbastanza facile creare qualcosa per un caso semplice, ma si consideri che si può testare un'API che chiama internamente altre 25 API (con ciascuna che aggiunge alla complessità ciclomatica del proprio algoritmo). Lo strumento dovrebbe fare molto di più dei migliori analizzatori statici attualmente sul mercato.

Il problema ovvio è che i test non coprono i requisiti del tuo cliente (in alcun modo significativo) - copriranno invece tutti i casi dell'implementazione.

Un test unitario corretto dovrebbe verificare che un'unità del tuo codice faccia ciò che è supposto fare, in una situazione. Un test generato come hai detto, testerebbe che il codice fa ciò che è implementato da fare (e forse che il tuo codice è raggiungibile). Questo in nessun modo si riferisce alle tue specifiche.

Would such a tool be useful?

Parzialmente (con funzionalità limitate). Potrebbe ridurre il tempo necessario per scrivere i test unitari per i casi d'angolo.

Is it even useful to automatically generate these testcases at all? Could it be extended to do even more useful things?

Non proprio. Penso che sarebbe utile generare automaticamente un testcase alla volta, con l'utente che sceglie i casi che desidera generare (ma generare tutti i casi sarebbe un rapporto segnale / rumore molto basso - dovresti scartare il codice generato più come irrilevante) .

    
risposta data 01.07.2014 - 14:22
fonte
1

Con queste domande principali:

Is it even useful to automatically generate these testcases at all? Could it be extended to do even more useful things? Does, by chance, such a project already exist and would I be reinventing the wheel?

Sicuramente sostengo che sì, la creazione di testicoli può essere utile e sì, tali progetti esistono già e vengono utilizzati. Tuttavia, non è così utile nel modo in cui viene chiesto nella domanda, vale a dire come una sostituzione TDD. Questo non lo è.

Questo è stato detto in una delle altre risposte:

A correct unit test should test that a unit of your code does what it is supposed to do, in a situation. A test generated like you mention, would test that the code does what it is implemented to do (and maybe that your code is reachable). This in no way relates to your specs.

Per cui sono maggiormente d'accordo. In questo modo vengono generalmente utilizzati i test unitari e come vengono utilizzati in TDD. Tuttavia, questo non è tutti i test sono utilizzati per.

Assumi una situazione in cui hai ereditato una base di codice di grandi dimensioni senza test di unità esistenti. Non sai del suo comportamento e certamente non sai se eventuali cambiamenti che stai per introdurre interromperanno qualsiasi comportamento esistente. Non sarebbe bello avere test generati che catturerebbero il comportamento corrente della base di codice esistente?

Lo farebbe sicuramente. Questi tipi di test sono chiamati test regression e in questo caso vedo come utile generare test case. Esistono effettivamente strumenti che fanno già questo: forse il più popolare è uno strumento commerciale AgitarOne con i suoi < a href="http://www.agitar.com/solutions/products/automated_junit_generation.html"> capacità di generazione di test di regressione . Ci sono anche altre alternative.

Un altro caso d'uso in cui ciò sarebbe utile è verificare se alcuni contratti comuni non sono violati. I contratti comuni potrebbero essere che a) il tuo codice non dovrebbe mai lanciare un'eccezione di puntatore nullo se non gli hai dato alcun parametro nullo, e b) il tuo codice, se implementa il metodo equals, dovrebbe seguire le regole richieste dal contratto del metodo equals . Esistono anche strumenti per questo, come lo strumento gratuito Randoop , che utilizza la generazione di test casuali assistita da feedback per catturare tali problemi.

Questi sono alcuni esempi in cui vedo l'utilità nei casi di test generati. Tuttavia, come sottolineato nelle risposte precedenti, è non lo stesso vantaggio che qualcuno ottiene dai test in TDD, ad esempio, o una buona suite di test in generale. Questi sono casi alquanto angusti: nel caso generale, dovresti comunque scrivere i test da solo.

    
risposta data 06.01.2015 - 16:06
fonte
1

Recentemente ho fatto qualche ricerca sui test generati. Sembra che ci siano tre approcci principali per generare test per codice sconosciuto. Tutti e tre gli approcci presuppongono che l'attuale comportamento (o specifica) sia inteso o almeno accettato. Quindi potrebbe essere caratterizzato da un test.

I tre approcci sono:

Nessuna analisi

Il codice non viene analizzato affatto. I metodi sono chiamati in una sequenza casuale e tutti i risultati intermedi sono verificati in successivi test di regressione.

Questo approccio è implementato da randoop o evosuite .

Analisi statiche

Vengono analizzati i dati del codice e il flusso di controllo. Alcuni strumenti provano solo a costruire dati di test che toccano tutti i rami, altri addirittura verificano che il codice rispetti determinate specifiche. L'analisi dovrebbe anche analizzare lo stato che viene modificato da un metodo con determinati dati di test. Entrambi i risultati dell'analisi possono essere utilizzati per generare test (configurazione con dati di test, asserire cambiamenti di stato analizzati).

Questo mi sembra l'approccio più pratico per l'utente. È anche il più vicino al tuo problema. Symbolic Pathfinder sembra essere uno strumento per questo approccio, tuttavia non è pronto per la produzione.

Analisi dinamica

Il codice viene analizzato in fase di esecuzione (debugging, profiling). Approcci comuni tentano di acquisire determinate chiamate di metodo (con stato prima e dopo la chiamata) e serializzarle su test.

Ho sviluppato uno strumento per questo approccio, che si chiama Testrecorder .

Le domande

Is creating such a tool feasible? Would it ever work, or are there some obvious problems?

Come indicato in precedenza da alcune risposte: non può esserci una soluzione sempre corretta. Eppure penso che potrebbe esserci una soluzione che aumenta la tua efficienza.

Would such a tool be useful? Is it even useful to automatically generate these testcases at all? Could it be extended to do even more useful things?

Questo dipende dallo scenario. Penso che il modo di scrivere test insieme al tuo codice (TDD) sia un buon modo per mantenere il codice con una qualità minima.

Eppure a volte mi trovo di fronte a grandi basi di codice legacy. Nessuno vuole davvero dedicare molto tempo a tale codice, tuttavia il rischio di introdurre un nuovo codice in tale codice è piuttosto alto. Penso che potrebbe essere un valido compromesso utilizzare i test generati in questi scenari.

    
risposta data 11.01.2019 - 08:36
fonte
0

Non è fattibile, parlando con le attuali conoscenze teoriche in informatica.

Il modo più semplice per vederlo è questo, inserire nel codice una congettura che non è stata dimostrata. (Ce ne sono molti). Non sai se il codice può entrare in questo o quel ramo, inoltre non sai quale valore lo lascerebbe nel ramo.

Quindi non puoi generare casi di test che vadano in determinati rami anche se hai accesso al codice sorgente.

E se pensate che sia solo un software, lo otterrete se ci provate abbastanza, ricordate dopo qualche aumento di numero nella lunghezza della codifica della congettura, lo spazio di ricerca cresce esponenzialmente. Quindi devi trovare un altro modo.

    
risposta data 01.07.2014 - 14:04
fonte

Leggi altre domande sui tag