Come testare un sistema in cui gli oggetti sono difficili da deridere?


34

Sto lavorando con il seguente sistema:

Network Data Feed -> Third Party Nio Library -> My Objects via adapter pattern

Di recente abbiamo avuto un problema in cui ho aggiornato la versione della libreria che stavo usando, che, tra le altre cose, ha causato i timestamp (che la libreria di terze parti restituisce come long), essere cambiati da millisecondi dopo l'epoca a nanosecondi dopo l'epoca.

Il problema:

Se scrivo test che deridono gli oggetti della libreria di terze parti, il mio test sarà errato se ho fatto un errore sugli oggetti della libreria di terze parti. Ad esempio, non mi rendevo conto che i timestamp hanno cambiato la precisione, il che ha comportato la necessità di cambiare il test unitario, perché il mio mock ha restituito dati errati. Questo non è un bug nella libreria , è successo perché ho perso qualcosa nella documentazione.

Il problema è che non posso essere sicuro dei dati contenuti in queste strutture dati perché non posso generare quelli reali senza un feed di dati reale. Questi oggetti sono grandi e complicati e contengono molti dati diversi. La documentazione per la libreria di terze parti è scadente.

La domanda:

Come posso impostare i miei test per testare questo comportamento? Non sono sicuro di poter risolvere questo problema in un test unitario, poiché il test stesso può essere facilmente errato. Inoltre, il sistema integrato è ampio e complicato ed è facile perdere qualcosa. Ad esempio, nella situazione sopra, avevo corretto correttamente la gestione del timestamp in diversi punti, ma ne ho perso uno. Il sistema sembrava fare principalmente le cose giuste nel mio test di integrazione, ma quando l'ho distribuito alla produzione (che ha molti più dati), il problema è diventato evidente.

Non ho un processo per i miei test di integrazione in questo momento. Il test è essenzialmente: prova a mantenere buoni i test unitari, aggiungi altri test quando le cose si rompono, quindi distribuiscile sul mio server di test e assicurati che le cose sembrino sane, quindi distribuiscile alla produzione. Questo problema di data e ora ha superato i test unitari perché le beffe sono state create in modo errato, quindi ha superato il test di integrazione perché non ha causato problemi immediati e immediati. Non ho un dipartimento di controllo qualità.


3
Puoi "registrare" un feed di dati reale e "riprodurlo" in seguito nella libreria di terze parti?
Idan Arye,

2
Qualcuno potrebbe scrivere un libro su problemi come questo. In realtà, Michael Feathers ha scritto proprio quel libro: c2.com/cgi/wiki?WorkingEffectivelyWithLegacyCode In esso, descrive una serie di tecniche per rompere le dipendenze difficili in modo che il codice possa diventare più testabile.
cbojar,

2
L'adattatore attorno alla libreria di terze parti? Sì, è esattamente quello che raccomando. Quei test unitari non miglioreranno il tuo codice. Non lo renderanno più affidabile o più mantenibile. A quel punto stai solo duplicando parzialmente il codice di qualcun altro; in questo caso, stai duplicando un po 'di codice scritto male dal suo suono. Questa è una perdita netta. Alcune delle risposte suggeriscono di fare alcuni test di integrazione; è una buona idea se vuoi solo un "Funziona?" controllo sanitario. Un buon test è difficile e ci vuole tanta abilità e intuizione quanto un buon codice.
jpmc26,

4
Un'illustrazione perfetta del male dei built-in. Perché la libreria non restituisce una Timestampclasse (che contiene alcuna dichiarazione che vogliono) e di fornire metodi chiamati ( .seconds(), .milliseconds(), .microseconds(), .nanoseconds()) e dei costruttori naturalmente con nome. Quindi non ci sarebbero stati problemi.
Matthieu M.,

2
Il detto "tutti i problemi nella codifica possono essere risolti da uno strato di indiretta (tranne, ovviamente, il problema di troppi strati di indiretta)" mi viene in mente qui ...
Dan Pantry,

Risposte:


27

Sembra che tu stia già facendo la dovuta diligenza. Ma ...

Al livello più pratico, includi sempre una buona manciata di test di integrazione "full-loop" nella tua suite per il tuo codice e scrivi più asserzioni di quelle che ritieni necessarie. In particolare, dovresti avere una manciata di test che eseguono un ciclo completo create-read- [do_stuff] -validate.

[TestMethod]
public void MyFormatter_FormatsTimesCorrectly() {

  // this test isn't necessarily about the stream or the external interpreter.
  // but ... we depend on them working how we think they work:
  var stream = new StreamThingy();
  var interpreter = new InterpreterThingy(stream);
  stream.Write("id-123, some description, 12345");

  // this is what you're actually testing. but, it'll also hiccup
  // if your 3rd party dependencies introduce a breaking change.
  var formatter = new MyFormatter(interpreter);
  var line = formatter.getLine();
  Assert.equal(
    "some description took 123.45 seconds to complete (id-123)", line
  );
}

E sembra che tu stia già facendo questo genere di cose. Hai solo a che fare con una libreria traballante e / o complicata. E in quel caso, è bene gettare alcuni tipi di test "questo è come funziona la libreria" che verificano sia la comprensione della libreria che servono come esempi su come utilizzare la libreria.

Supponiamo che tu debba capire e dipendere da come un parser JSON interpreta ogni "tipo" in una stringa JSON. È utile e banale includere qualcosa del genere nella tua suite:

[TestMethod]
public void JSONParser_InterpretsTypesAsExpected() {
  String datastream = "{nbr:11,str:"22",nll:null,udf:undefined}";
  var o = (new JSONParser()).parse(datastream);

  Assert.equal(11, o.nbr);
  Assert.equal(Int32.getType(), o.nbr.getType());
  Assert.equal("22", o.str);
  Assert.equal(null, o.nll);
  Assert.equal(Object.getType(), o.nll.getType());
  Assert.isFalse(o.KeyExists(udf));
}

Ma in secondo luogo, ricorda che i test automatici di qualsiasi tipo, e quasi a qualsiasi livello di rigore, non riusciranno comunque a proteggerti da tutti i bug. È perfettamente comune aggiungere test quando scopri i problemi. Non avendo un dipartimento di controllo qualità, ciò significa che molti di questi problemi verranno scoperti dagli utenti finali.

E in misura significativa, è normale.

E in terzo luogo, quando una libreria cambia il significato di un valore di ritorno o di un campo senza rinominare il campo o il metodo o altrimenti "rompere" il codice dipendente (magari cambiando il suo tipo), sarei dannatamente insoddisfatto di quell'editore. E direi che, anche se probabilmente avresti dovuto leggere il log delle modifiche, se ce n'è uno, probabilmente dovresti anche trasmettere parte del tuo stress all'editore. Direi che hanno bisogno delle critiche che si spera costruttive ...


Vorrei che fosse semplice come inserire una stringa JSON nella libreria. Non è. Non posso fare l'equivalente di (new JSONParser()).parse(datastream), poiché prendono i dati direttamente da una NetworkInterfacee tutte le classi che eseguono l'analisi effettiva sono pacchetti privati ​​e controllati.
durron597,

Inoltre, il log delle modifiche non includeva il fatto che hanno cambiato i timestamp da ms a ns, tra gli altri mal di testa che non hanno documentato. Sì, sono molto scontento di loro e ho espresso questo a loro.
durron597,

@ durron597 Oh, quasi mai. Tuttavia, puoi spesso falsificare l'origine dati sottostante, come nel primo esempio di codice. ... Point è il seguente: i test di integrazione full-ciclo, quando possibile, testare la vostra comprensione della biblioteca, quando possibile, e solo essere consapevoli del fatto che sarà ancora lasciare bug nell'ambiente naturale. E i vostri fornitori di terze parti devono essere responsabili per rendere invisibili, interrompendo le modifiche.
svidgen,

@ durron597 Non ho familiarità con NetworkInterface... è qualcosa in cui puoi alimentare i dati collegando l'interfaccia a una porta su localhost o qualcosa del genere?
svidgen,

NetworkInterface. È un oggetto di basso livello per lavorare direttamente con una scheda di rete e aprire socket su di essa, ecc.
durron597

11

Risposta breve: è difficile. Probabilmente ti senti come se non ci fossero buone risposte, e questo perché non ci sono risposte facili.

Risposta lunga: come dice @ptyx , sono necessari test di sistema e test di integrazione, nonché test unitari:

  • I test unitari sono veloci e facili da eseguire. Catturano i bug in singole sezioni di codice e usano i mock per renderli possibili. Per necessità, non possono rilevare disallineamenti tra pezzi di codice (come millisecondi contro nanosecondi).
  • I test di integrazione e i test di sistema sono lenti (er) e difficili (er) da eseguire ma rilevano più errori.

Alcuni suggerimenti specifici:

  • C'è qualche vantaggio nel far semplicemente eseguire un test di sistema per eseguire il più possibile il sistema. Anche se non è in grado di convalidare gran parte del comportamento o fare molto bene a individuare il problema. (Micheal Feathers ne discute di più nel lavorare in modo efficace con il codice legacy .)
  • Investire nella testabilità aiuta. Esistono un gran numero di tecniche che è possibile utilizzare qui: integrazione continua, script, macchine virtuali, strumenti per riprodurre, proxy o reindirizzare il traffico di rete.
  • Uno dei vantaggi (almeno per me) dell'investimento nella testabilità potrebbe non essere ovvio: se i test sono noiosi, noiosi o ingombranti da scrivere o eseguire, allora è troppo facile per me semplicemente saltarli se sono sotto pressione o stanco. Mantenere i test al di sotto della soglia "È così facile che non ci sono scuse per non farlo" è importante.
  • Il software perfetto non è fattibile. Come tutto il resto, lo sforzo speso per i test è un compromesso e talvolta non ne vale la pena. Esistono vincoli (come la mancanza di un dipartimento di controllo qualità). Accetta che i bug si verifichino, recuperino e imparino.

Ho visto la programmazione descritta come l'attività di apprendimento di un problema e di uno spazio di soluzione. Ottenere tutto perfetto in anticipo potrebbe non essere fattibile, ma puoi imparare dopo il fatto. ("Ho corretto la gestione del timestamp in più punti ma ne ho perso uno. Posso modificare i miei tipi di dati o le mie classi per rendere più esplicito e difficile perdere la gestione del timestamp o renderlo più centralizzato in modo da avere solo un posto da cambiare? Posso modificare i miei test per verificare più aspetti della gestione del timestamp? Posso semplificare il mio ambiente di test per renderlo più facile in futuro? Posso immaginare alcuni strumenti che lo avrebbero reso più semplice e, in tal caso, posso trovare tale strumento su Google? " Eccetera.)


7

Ho aggiornato la versione della libreria ... che ... ha causato i timestamp (che la libreria di terze parti restituisce come long), da cambiare da millisecondi dopo l'epoca a nanosecondi dopo l'epoca.

...

Questo non è un bug nella libreria

Non sono assolutamente d'accordo con te qui. È un bug nella libreria , in effetti piuttosto insidioso. Hanno modificato il tipo semantico del valore restituito, ma non hanno modificato il tipo programmatico del valore restituito. Questo può provocare tutti i tipi di caos, soprattutto se si trattava di una versione minore, ma anche se fosse una delle maggiori.

Diciamo invece che la libreria ha restituito un tipo di MillisecondsSinceEpoch, un semplice wrapper che contiene un long. Quando lo hanno cambiato in un NanosecondsSinceEpochvalore, il codice non sarebbe stato compilato e ovviamente ti avrebbe indicato i luoghi in cui è necessario apportare modifiche. La modifica non ha potuto corrompere silenziosamente il tuo programma.

Meglio ancora sarebbe un TimeSinceEpochoggetto in grado di adattare la sua interfaccia man mano che veniva aggiunta maggiore precisione, come l'aggiunta di un #toLongNanosecondsmetodo a fianco del #toLongMillisecondsmetodo, senza richiedere alcuna modifica al codice.

Il prossimo problema è che non hai un set affidabile di test di integrazione nella libreria. Dovresti scrivere quelli. Meglio sarebbe creare un'interfaccia attorno a quella libreria per incapsularla lontano dal resto dell'applicazione. Diverse altre risposte rispondono a questo (e altre continuano a spuntare mentre scrivo). I test di integrazione devono essere eseguiti con minore frequenza rispetto ai test di unità. Questo è il motivo per cui avere un buffer layer aiuta. Separare i test di integrazione in un'area separata (o denominarli in modo diverso) in modo da poterli eseguire secondo necessità, ma non ogni volta che si esegue il test dell'unità.


2
@ durron597 Continuerei a sostenere che si tratta di un bug. Oltre alla mancanza di documentazione, perché cambiare del tutto il comportamento previsto? Perché non un nuovo metodo che fornisce la nuova precisione e che il vecchio metodo fornisca ancora millis? E perché non fornire un modo per il compilatore di avvisarti attraverso una modifica nel tipo di ritorno? Non ci vuole molto per renderlo molto più chiaro, non solo nella documentazione, ma nel codice stesso.
cbojar,

1
@gbjbaanb, "che hanno scarse pratiche di rilascio" mi sembra un bug
Arturo Torres Sánchez,

2
@gbjbaanb Una libreria di terze parti [dovrebbe] stipulare un "contratto" con i suoi utenti. La violazione di quel contratto, sia esso documentato o meno, può / dovrebbe essere considerata un bug. Come altri hanno già detto, se devi cambiare qualcosa, aggiungi al contratto con una nuova funzione / metodo (vedi tutti i ...Ex()metodi in Win32API). Se ciò non fosse possibile, "rompere" il contratto rinominando la funzione (o il suo tipo di ritorno) sarebbe stato meglio che alterare il comportamento.
TripeHound,

1
Questo è un bug nella libreria. L'uso di nanosecondi a lungo lo sta spingendo.
Giosuè,

1
@gbjbaanb Dici che non è un bug poiché è un comportamento previsto, anche se inaspettato. In questo senso non è un bug di implementazione , ma è lo stesso un bug. Potrebbe essere chiamato un difetto di progettazione o un bug di interfaccia . I difetti risiedono nel fatto che espone un'ossessione primitiva con i long piuttosto che con le unità esplicite, la sua astrazione è permeabile in quanto esporta i dettagli della sua implementazione interna (che i dati sono memorizzati come un lungo di una certa unità) e che viola il principio del minimo stupore con un sottile cambio di unità.
cbojar,

5

Sono necessari test di integrazione e di sistema.

I test unitari sono ottimi per verificare che il codice si comporti come previsto. Come ti rendi conto, non fa nulla per contestare i tuoi presupposti o garantire che le tue aspettative siano sane.

A meno che il tuo prodotto non abbia poca interazione con sistemi esterni o interagisca con sistemi così ben noti, stabili e documentati da poter essere derisi con sicurezza (ciò accade raramente nel mondo reale) - i test unitari non sono sufficienti.

Più alto è il livello dei tuoi test, più ti proteggeranno dagli imprevisti. Questo ha un costo (convenienza, velocità, fragilità ...), quindi i test unitari dovrebbero rimanere alla base dei tuoi test, ma hai bisogno di altri livelli, tra cui - eventualmente - un piccolo pezzetto di test sull'uomo che fa molto per catturare cose stupide a cui nessuno ha pensato.


2

Il migliore sarebbe creare un prototipo minimale e capire come funziona esattamente la libreria. In questo modo, acquisirai alcune conoscenze sulla biblioteca con scarsa documentazione. Un prototipo può essere un programma minimalista che utilizza quella libreria e fa la funzionalità.

Altrimenti, non ha senso scrivere unit test, con requisiti semi-definiti e scarsa comprensione del sistema.

Per quanto riguarda il tuo problema specifico, sull'utilizzo di metriche errate: lo considererei come una modifica dei requisiti. Una volta riconosciuto il problema, modificare i test unitari e il codice.


1

Se stavi usando una libreria popolare e stabile, potresti forse supporre che non ti giocherà brutti scherzi. Ma se cose come ciò che hai descritto accadono con questa libreria, ovviamente, questo non è uno. Dopo questa brutta esperienza, ogni volta che qualcosa va storto nella tua interazione con questa biblioteca, dovrai esaminare non solo la possibilità di aver fatto un errore, ma anche la possibilità che la biblioteca possa aver fatto un errore. Quindi, diciamo che questa è una libreria di cui non sei "sicuro".

Una delle tecniche impiegate con le biblioteche di cui non siamo "sicuri" è quella di costruire uno strato intermedio tra il nostro sistema e dette librerie, che astragga la funzionalità offerta dalle biblioteche, afferma che le nostre aspettative sulla biblioteca sono giuste e semplifica notevolmente la nostra vita in futuro, dovremmo decidere di dare l'avvio a quella libreria e sostituirla con un'altra libreria che si comporta meglio.


Questo non risponde davvero alla domanda. Ho già un livello che separa la libreria dal mio sistema, ma il problema è che il mio livello di astrazione può avere "bug" quando la libreria cambia su di me senza preavviso.
durron597,

1
@ durron597 Quindi forse il layer non isola sufficientemente la libreria dal resto dell'applicazione. Se stai riscontrando difficoltà nel testare quel livello, forse devi semplificare il comportamento e isolare più fortemente i dati sottostanti.
cbojar,

Cosa ha detto @cbojar. Inoltre, lasciami ripetere qualcosa che potrebbe essere passato inosservato nel testo sopra: la assertparola chiave (o la funzione o la funzione, a seconda della lingua che stai utilizzando), è tuo amico. Non sto parlando di asserzioni nei test unità / integrazione, sto dicendo che il livello di isolamento dovrebbe essere molto pesante con asserzioni, affermando tutto asseribile sul comportamento della biblioteca.
Mike Nakis,

Queste asserzioni non si eseguono necessariamente sulle esecuzioni di produzione, ma si eseguono durante i test, con una vista in bianco del livello di isolamento e quindi in grado di assicurarsi (per quanto possibile) che le informazioni che il livello riceve dalla libreria è sano.
Mike Nakis,
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.