integrazione continua per software scientifico


22

Non sono un ingegnere del software. Sono uno studente di dottorato nel campo della geoscienza.

Quasi due anni fa ho iniziato a programmare un software scientifico. Non ho mai usato l'integrazione continua (CI), principalmente perché all'inizio non sapevo che esistesse ed ero l'unica persona che lavorava su questo software.

Ora, poiché la base del software è in esecuzione, altre persone iniziano a interessarsi e vogliono contribuire al software. Il piano prevede che altre persone in altre università stiano implementando aggiunte al software principale. (Ho paura che possano introdurre dei bug). Inoltre, il software è diventato piuttosto complesso ed è diventato sempre più difficile da testare e ho anche intenzione di continuare a lavorarci.

Per questi due motivi, ora sto pensando sempre più all'utilizzo di CI. Dato che non ho mai avuto una formazione da ingegnere informatico e nessuno intorno a me ha mai sentito parlare di CI (siamo scienziati, non programmatori), trovo difficile iniziare il mio progetto.

Ho un paio di domande in cui vorrei ricevere qualche consiglio:

Prima di tutto una breve spiegazione di come funziona il software:

  • Il software è controllato da un file .xml contenente tutte le impostazioni richieste. Si avvia il software semplicemente passando il percorso al file .xml come argomento di input e viene eseguito e crea un paio di file con i risultati. Una singola corsa può richiedere ~ 30 secondi.

  • È un software scientifico. Quasi tutte le funzioni hanno più parametri di input, i cui tipi sono per lo più classi piuttosto complesse. Ho più file .txt con grandi cataloghi che vengono utilizzati per creare istanze di queste classi.

Ora veniamo alle mie domande:

  1. unit test, test di integrazione, test end-to-end? : Il mio software ora ha circa 30.000 righe di codice con centinaia di funzioni e ~ 80 classi. Mi sembra strano iniziare a scrivere unit test per centinaia di funzioni già implementate. Quindi ho pensato di creare semplicemente alcuni casi di test. Prepara 10-20 diversi file .xml e avvia il software. Immagino che questo sia ciò che viene chiamato test end-to-end? Ho letto spesso che non dovresti farlo, ma forse è ok come inizio se hai già un software funzionante? O è semplicemente una stupida idea provare ad aggiungere CI a un software già funzionante.

  2. Come si scrivono i test unitari se i parametri della funzione sono difficili da creare? supponiamo che io abbia una funzione double fun(vector<Class_A> a, vector<Class_B>)e di solito, dovrei prima leggere in più file di testo per creare oggetti di tipo Class_Ae Class_B. Ho pensato di creare alcune funzioni fittizie come Class_A create_dummy_object()senza leggere i file di testo. Ho anche pensato di implementare una sorta di serializzazione . (Non intendo testare la creazione degli oggetti di classe poiché dipendono solo da più file di testo)

  3. Come scrivere i test se i risultati sono molto variabili? Il mio software utilizza grandi simulazioni monte-carlo e funziona in modo iterativo. Di solito, hai ~ 1000 iterazioni e ad ogni iterazione stai creando ~ 500-20.000 istanze di oggetti basate su simulazioni monte-carlo. Se solo un risultato di una iterazione è leggermente diverso, tutte le iterazioni imminenti sono completamente diverse. Come gestisci questa situazione? Immagino che questo sia un punto importante rispetto ai test end-to-end, poiché il risultato finale è altamente variabile?

Qualsiasi altro consiglio con CI è molto apprezzato.



1
Come fai a sapere che il tuo software funziona correttamente? Riesci a trovare un modo per automatizzare quel controllo in modo da poterlo eseguire ad ogni modifica? Questo dovrebbe essere il tuo primo passo quando introduci CI a un progetto esistente.
Bart van Ingen Schenau,

Come ti sei assicurato che il tuo software producesse risultati accettabili in primo luogo? Cosa ti rende sicuro che "funzioni"? Le risposte a entrambe le domande ti daranno molto materiale per testare il tuo software ora e in futuro.
Polygnome,

Risposte:


23

Testare il software scientifico è difficile, sia a causa della complessa materia sia a causa dei tipici processi di sviluppo scientifico (ovvero l'hacking fino a quando funziona, il che di solito non porta a un design verificabile). Questo è un po 'ironico considerando che la scienza dovrebbe essere riproducibile. Ciò che cambia rispetto al software "normale" non è se i test sono utili (sì!), Ma quali tipi di test sono appropriati.

Gestione della casualità: tutte le esecuzioni del software DEVONO essere riproducibili. Se usi le tecniche Monte Carlo, devi rendere possibile fornire un seme specifico per il generatore di numeri casuali.

  • È facile dimenticarlo, ad esempio quando si utilizza la rand()funzione di C che dipende dallo stato globale.
  • Idealmente, un generatore di numeri casuali viene passato come oggetto esplicito attraverso le tue funzioni. L' randomintestazione della libreria standard di C ++ 11 lo rende molto più semplice.
  • Invece di condividere uno stato casuale tra i moduli del software, ho trovato utile creare un secondo RNG che viene seminato da un numero casuale dal primo RNG. Quindi, se il numero di richieste all'RNG da parte dell'altro modulo cambia, la sequenza generata dal primo RNG rimane invariata.

I test di integrazione vanno benissimo. Sono bravi a verificare che parti diverse del tuo software giochino insieme correttamente e per eseguire scenari concreti.

  • Come livello minimo di qualità "non si arresta in modo anomalo" può già essere un buon risultato del test.
  • Per risultati più forti, dovrai anche verificare i risultati rispetto ad alcune linee di base. Tuttavia, questi controlli dovranno essere alquanto tolleranti, ad es. Tenere conto degli errori di arrotondamento. Può anche essere utile confrontare le statistiche di riepilogo anziché le righe di dati complete.
  • Se il controllo su una linea di base sarebbe troppo fragile, verificare che gli output siano validi e che soddisfino alcune proprietà generali. Questi possono essere generali ("le posizioni selezionate devono essere distanti almeno 2 km") o specifiche dello scenario, ad esempio "una posizione selezionata deve trovarsi all'interno di questa area".

Quando si eseguono test di integrazione, è una buona idea scrivere un test runner come programma o script separato. Questo runner di test esegue le impostazioni necessarie, esegue l'eseguibile da testare, controlla i risultati e pulisce in seguito.

I controlli sullo stile dei test unitari possono essere piuttosto difficili da inserire nel software scientifico perché il software non è stato progettato per questo. In particolare, i test unitari diventano difficili quando il sistema sotto test ha molte dipendenze / interazioni esterne. Se il software non è puramente orientato agli oggetti, non è generalmente possibile deridere / stubare tali dipendenze. Ho trovato il modo migliore per evitare in gran parte i test unitari per tale software, ad eccezione delle funzioni matematiche pure e delle funzioni di utilità.

Anche alcuni test sono meglio di nessun test. In combinazione con il segno di spunta "deve compilare" che è già un buon inizio per l'integrazione continua. Puoi sempre tornare indietro e aggiungere altri test in seguito. È quindi possibile dare la priorità alle aree del codice che hanno maggiori probabilità di rompersi, ad esempio perché ottengono più attività di sviluppo. Per vedere quali parti del tuo codice non sono coperte dai test unitari, puoi utilizzare gli strumenti di copertura del codice.

Test manuali: specialmente per domini problematici complessi, non sarai in grado di testare tutto automaticamente. Ad esempio, sto attualmente lavorando a un problema di ricerca stocastica. Se provo che il mio software produce sempre lo stesso risultato, non posso migliorarlo senza interrompere i test. Invece, ho semplificato l' esecuzione di test manuali : eseguo il software con un seme fisso e ottengo una visualizzazionedel risultato (a seconda delle preferenze, R, Python / Pyplot e Matlab facilitano l'ottenimento di visualizzazioni di alta qualità dei set di dati). Posso usare questa visualizzazione per verificare che le cose non siano andate terribilmente male. Allo stesso modo, tracciare l'avanzamento del software tramite l'output di registrazione può essere una tecnica di test manuale praticabile, almeno se posso selezionare il tipo di eventi da registrare.


7

Mi sembra strano iniziare a scrivere unit test per centinaia di funzioni già implementate.

Ti consigliamo di (tipicamente) scrivere i test mentre modifichi tali funzioni. Non è necessario sedersi e scrivere centinaia di unit test per le funzioni esistenti, ciò sarebbe (in gran parte) una perdita di tempo. Il software funziona (probabilmente) bene così com'è. Il punto di questi test è garantire che i cambiamenti futuri non rompano il vecchio comportamento. Se non cambi mai più una particolare funzione, probabilmente non varrà mai la pena prenderla per testarla (dato che al momento funziona, ha sempre funzionato e probabilmente continuerà a funzionare). Consiglio di leggere Lavorare in modo efficace con il codice legacydi Michael Feathers su questo fronte. Ha alcune grandi strategie generali per testare cose che già esistono, tra cui tecniche di rottura delle dipendenze, test di caratterizzazione (copia / incolla dell'output della funzione nella suite di test per garantire il mantenimento del comportamento di regressione) e molto altro ancora.

Come si scrivono i test unitari se i parametri della funzione sono difficili da creare?

Idealmente, non lo fai. Invece, rendi i parametri più facili da creare (e quindi rendi più semplice il test del tuo progetto). Certo, i cambiamenti di progettazione richiedono tempo e questi refactoring possono essere difficili su progetti legacy come i tuoi. TDD (Test Driven Development) può aiutare in questo. Se i parametri sono super difficili da creare, avrai molti problemi a scrivere i test in uno stile test-first.

A breve termine, usa le beffe, ma fai attenzione a deridere l'inferno e i problemi che ne derivano a lungo termine. Mentre sono cresciuto come ingegnere del software, però, ho capito che le beffe sono quasi sempre un mini-odore che stanno cercando di concludere un problema più grande e non affrontare il problema principale. Mi piace chiamarlo "avvolgimento del turd", perché se metti un pezzo di stagnola su un po 'di cacca di cane sul tuo tappeto, puzza ancora. Quello che devi fare è alzarti, raccogliere la cacca e gettarla nella spazzatura, quindi estrarre la spazzatura. Questo è ovviamente più lavoro e rischi di avere un po 'di materia fecale per le mani, ma a lungo andare per te e la tua salute. Se continui a avvolgere quei cacca non vorrai più vivere a casa tua. Le beffe sono simili in natura.

Ad esempio, se hai il tuo Class_Adifficile da istanziare perché devi leggere in 700 file, allora potresti semplicemente deriderlo. La prossima cosa che sai, il tuo finto non è aggiornato e il reale Class_A fa qualcosa di molto diverso dal finto, e i tuoi test stanno ancora superando anche se dovrebbero fallire. Una soluzione migliore è quella di scomporre Class_Ain componenti più facili da usare / testare e testarli invece. Forse scrivere un test di integrazione che colpisce effettivamente il disco e assicurarsi che Class_Afunzioni nel suo complesso. O forse hai solo un costruttore per Class_Acui puoi creare un'istanza con una semplice stringa (che rappresenta i tuoi dati) invece di dover leggere dal disco.

Come scrivere i test se i risultati sono molto variabili?

Un paio di consigli:

1) Usa inversioni (o più in generale test basati sulle proprietà). Di cosa si tratta [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).

2) Usa asserzioni "conosciute". Se si scrive una funzione determinante, potrebbe essere difficile dire quale sia il determinante di una matrice 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 un po 'di codice qualche tempo fa che registrava due immagini generando punti di collegamento che creano una mappatura tra le immagini e facendo una distorsione tra loro per farle combaciare. Potrebbe registrarsi a un livello sub-pixel. Come puoi provarlo? Cose come:

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

poiché puoi registrarti solo su parti sovrapposte, l'immagine registrata deve essere più piccola o uguale alla tua 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 con +/- 5% dell'intervallo valido (0-255 è un intervallo comune, in scala di grigi). Dovrebbe almeno avere le stesse dimensioni. 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 un bug nel codice scientifico che si verificava in casi degenerati generati casualmente . 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.Unico inconveniente: devi registrare le tue esecuzioni di prova. Upside: correttezza e bug nuking.

HTH.


2
  1. Tipi di test

    • Mi sembra strano iniziare a scrivere unit test per centinaia di funzioni già implementate

      Pensateci al contrario: se una patch che tocca diverse funzioni interrompe uno dei test end-to-end, come riuscirete a capire qual è il problema?

      È molto più semplice scrivere test unitari per singole funzioni piuttosto che per l'intero programma. È molto più facile essere sicuri di avere una buona copertura di una singola funzione. È molto più semplice riformattare una funzione quando si è sicuri che i test unitari cattureranno tutti i casi angolari che si sono rotti.

      Scrivere unit test per funzioni già esistenti è perfettamente normale per chiunque abbia lavorato su una base di codice legacy. Sono un buon modo per confermare la comprensione delle funzioni in primo luogo e, una volta scritte, sono un buon modo per trovare cambiamenti di comportamento imprevisti.

    • Anche i test end-to-end sono utili. Se sono più facili da scrivere, fai prima di tutto quelli e aggiungi i test unitari ad hoc per coprire le funzioni di cui sei più preoccupato per l'interruzione di altri. Non devi farlo tutto in una volta.

    • Sì, l'aggiunta di CI al software esistente è ragionevole e normale.

  2. Come scrivere test unitari

    Se i tuoi oggetti sono molto costosi e / o complessi, scrivi finti. Puoi semplicemente collegare i test usando simulazioni separatamente dai test usando oggetti reali, invece di usare il polimorfismo.

    Dovresti comunque avere un modo semplice di creare istanze - una funzione per creare istanze fittizie è comune - ma è anche sensato avere test per il vero processo di creazione.

  3. Risultati variabili

    Devi avere degli invarianti per il risultato. Prova quelli, piuttosto che un singolo valore numerico.

    Potresti fornire un falso generatore di numeri pseudocasuali se il tuo codice monte carlo lo accetta come parametro, il che renderebbe prevedibili i risultati almeno per un noto algoritmo, ma è fragile a meno che non restituisca letteralmente lo stesso numero ogni volta.


1
  1. Non è mai una stupida idea aggiungere CI. Per esperienza, so che questa è la strada da percorrere quando hai un progetto open source in cui le persone sono libere di contribuire. CI consente di impedire alle persone di aggiungere o modificare il codice se il codice interrompe il programma, quindi è quasi inestimabile avere una base di codice funzionante.

    Quando si considerano i test, è sicuramente possibile fornire alcuni test end-to-end (penso che sia una sottocategoria di test di integrazione) per essere sicuri che il flusso di codice stia funzionando come dovrebbe. È necessario fornire almeno alcuni test unitari di base per assicurarsi che le funzioni producano i giusti valori, in quanto parte dei test di integrazione possono compensare altri errori commessi durante il test.

  2. Testare la creazione di oggetti è piuttosto difficile e laborioso. Hai ragione nel voler creare oggetti fittizi. Questi oggetti dovrebbero avere alcuni valori predefiniti, ma maiuscoli, per i quali sicuramente sai quale dovrebbe essere l'output.

  3. Il problema con i libri su questo argomento è che il panorama della CI (e di altre parti di Devops) si evolve così rapidamente che qualsiasi cosa in un libro sarà probabilmente obsoleta pochi mesi dopo. Non conosco libri che potrebbero aiutarti, ma Google dovrebbe, come sempre, essere il tuo salvatore.

  4. Dovresti eseguire tu stesso i test più volte e fare analisi statistiche. In questo modo è possibile implementare alcuni casi di test in cui si prende la mediana / media di più esecuzioni e la si confronta con l'analisi, in modo da sapere quali valori sono corretti.

Alcuni suggerimenti:

  • Utilizza l'integrazione degli strumenti CI nella tua piattaforma GIT per impedire che il codice non funzionante entri nella tua base di codice.
  • smettere di unire il codice prima che altri sviluppatori sviluppassero la peer review. Questo rende più facilmente noti gli errori e impedisce nuovamente al codice non inserito di entrare nella tua base di codice.

1

In una risposta precedente, Amon ha già menzionato alcuni punti molto importanti. Vorrei aggiungerne ancora:

1. Differenze tra sviluppo di software scientifico e software commerciale

Per i software scientifici, ovviamente l'attenzione è normalmente rivolta al problema scientifico. I problemi riguardano più la gestione del background teorico, la ricerca del miglior metodo numerico, ecc. Il software è solo una, più o meno, una piccola parte del lavoro.

Il software è nella maggior parte dei casi scritto da una o solo poche persone. È spesso scritto per un progetto specifico. Quando il progetto è finito e tutto è pubblicato, in molti casi il software non è più necessario.

Il software commerciale è in genere sviluppato da grandi team per un periodo di tempo più lungo. Ciò richiede molta pianificazione per l'architettura, la progettazione, i test unitari, i test di integrazione, ecc. Questa pianificazione richiede una quantità di tempo ed esperienza a livello economico. In un ambiente scientifico, normalmente non c'è tempo per quello.

Se vuoi convertire il tuo progetto in un software simile al software commerciale, dovresti controllare quanto segue:

  • Hai il tempo e le risorse?
  • Qual è la prospettiva a lungo termine del software? Cosa succederà con il software quando finisci il tuo lavoro e stai lasciando l'università?

2. Test end-to-end

Se il software diventa sempre più complesso e diverse persone ci stanno lavorando, i test sono obbligatori. Ma, come amon già detto, aggiungendo unit test per software scientifico è piuttosto difficile. Quindi devi usare un approccio diverso.

Poiché il tuo software sta ricevendo il suo input da un file, come la maggior parte dei software scientifici, è perfetto per creare diversi file di input e output di esempio. Dovresti eseguire questi test automaticamente su ogni versione e confrontare i risultati con i tuoi campioni. Questo potrebbe essere un ottimo sostituto per i test unitari. Ottieni anche test di integrazione in questo modo.

Naturalmente, per ottenere risultati riproducibili, dovresti usare lo stesso seme per il tuo generatore di numeri casuali, come già scritto da Amon .

Gli esempi dovrebbero riguardare i risultati tipici del tuo software. Ciò dovrebbe includere anche casi limite dello spazio dei parametri e algoritmi numerici.

Dovresti provare a trovare esempi che non richiedono troppo tempo per l'esecuzione, ma che coprono ancora i casi di test tipici.

3. Integrazione continua

Poiché l'esecuzione degli esempi di test può richiedere del tempo, penso che l'integrazione continua non sia fattibile. Probabilmente dovrai discutere le parti aggiuntive con i tuoi colleghi. Ad esempio, devono corrispondere ai metodi numerici utilizzati.

Quindi penso che sia meglio fare l'integrazione in un modo ben definito dopo aver discusso del background teorico e dei metodi numerici, test accurati, ecc.

Non penso che sia una buona idea avere un qualche tipo di automatismo per l'integrazione continua.

A proposito, stai usando un sistema di controllo versione?

4. Test dei tuoi algoritmi numerici

Se si stanno confrontando risultati numerici, ad esempio quando si controllano i risultati del test, non è necessario verificare la parità dei numeri fluttuanti. Ci possono sempre essere errori di arrotondamento. Invece, controlla se la differenza è inferiore a una soglia specifica.

È anche una buona idea controllare i tuoi algoritmi rispetto a diversi algoritmi o formulare il problema scientifico in un modo diverso e confrontare i risultati. Se ottieni gli stessi risultati usando due o più modi indipendenti, questa è una buona indicazione che la tua teoria e la tua implementazione sono corrette.

È possibile eseguire tali test nel codice di test e utilizzare l'algoritmo più veloce per il codice di produzione.


0

Il mio consiglio sarebbe di scegliere attentamente come impiegherai i tuoi sforzi. Nel mio campo (bioinformatica) gli algoritmi all'avanguardia cambiano così rapidamente che spendere energia per la correzione degli errori del codice potrebbe essere speso meglio per l'algoritmo stesso.

Detto questo, ciò che viene valutato è:

  • è il metodo migliore al momento, in termini di algoritmo?
  • quanto è facile effettuare il porting su diverse piattaforme di calcolo (diversi ambienti HPC, sistemi operativi ecc.)
  • robustezza - funziona sul MIO set di dati?

Il tuo istinto di costruire una base di codice a prova di proiettile è nobile, ma vale la pena ricordare che questo non è un prodotto commerciale. Rendilo il più portatile possibile, a prova di errore (per il tuo tipo di utente), conveniente per gli altri per contribuire, quindi concentrati sull'algoritmo stesso

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.