Come faccio a testare un sistema in cui gli oggetti sono difficili da schernire?

34

Sto lavorando con il seguente sistema:

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

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

Il problema:

Se scrivo test che prendono in giro gli oggetti della libreria di terze parti, il mio test sarà sbagliato se ho fatto un errore sugli oggetti della libreria di terze parti. Ad esempio, non mi sono reso conto che i timestamp hanno cambiato la precisione, il che ha comportato la necessità di cambiare il test dell'unità, perché la mia simulazione ha restituito i dati sbagliati. 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é I non può generare quelli reali senza un vero feed di dati. Questi oggetti sono grandi e complicati e hanno molti differenti pezzi di dati in loro. 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, perché il test stesso può facilmente essere sbagliato. Inoltre, il sistema integrato è ampio e complicato ed è facile perdere qualcosa. Ad esempio, nella situazione precedente, ho regolato correttamente la gestione del timestamp in diversi punti, ma ho perso uno di essi. Sembra che il sistema stia facendo le cose giuste per lo più nel mio test di integrazione, ma quando l'ho distribuito alla produzione (che ha molti più dati), il problema è diventato evidente.

Al momento non ho un processo per i miei test di integrazione. I test sono essenzialmente: prova a mantenere buoni i test unitari, aggiungi altri test quando si verificano problemi, quindi esegui l'implementazione sul mio server di test e assicurati che le cose sembrino sensate, quindi distribuite in produzione. Questo problema di timestamp ha superato i test unitari perché i mock sono stati creati in modo errato, quindi ha superato il test di integrazione perché non causava alcun problema immediato e ovvio. Non ho un dipartimento QA.

    
posta durron597 06.10.2015 - 17:49
fonte

6 risposte

27

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

Al livello più pratico, include sempre una buona quantità 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 di creazione-lettura- [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. In tal caso, è utile inserire alcuni tipi di test "questo è il modo in cui funziona la libreria" che verificano sia la tua comprensione della libreria sia gli esempi di come utilizzare la libreria.

Supponiamo di dover capire e dipendere da come un parser JSON interpreta ogni "tipo" in una stringa JSON. È utile e banale includere qualcosa di simile 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 continueranno a non proteggerti da tutti i bug. È molto comune aggiungere test man mano che si riscontrano problemi. Non avendo un dipartimento QA, questo significa che molti di questi problemi saranno scoperti dagli utenti finali.

E in misura significativa, è normale.

E in terzo luogo, quando una libreria cambia il significato di un valore o campo di ritorno senza rinominare il campo o il metodo o altrimenti "spezzando" il codice dipendente (magari cambiando il suo tipo), vorrei essere dannatamente infelice con quell'editore. E direi che, anche se probabilmente dovresti leggere il log delle modifiche se ce n'è uno, probabilmente dovresti anche passare un po 'di stress al publisher. Direi che hanno bisogno delle critiche costruttivamente promettenti ...

    
risposta data 06.10.2015 - 19:08
fonte
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 @ptyx dice , sono necessari test di sistema, test di integrazione e test unitari:

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

Alcuni suggerimenti specifici:

  • Ci sono alcuni vantaggi semplicemente ottenendo un test di sistema per eseguire quanto più possibile il sistema. Anche se non è in grado di convalidare gran parte del comportamento o fare molto bene nel localizzare il problema. (Micheal Feathers ne parla di più in Lavorare efficacemente con il codice legacy .)
  • Investire nella testabilità aiuta. Esiste un enorme numero di tecniche che puoi utilizzare qui: integrazione continua, script, VM, strumenti per la riproduzione, proxy o reindirizza traffico di rete.
  • Uno dei vantaggi (almeno per me) di investire nella testabilità può essere non ovvio: se i test sono noiosi, noiosi o complicati da scrivere o eseguire, allora è troppo facile per me semplicemente saltarli se Sono sotto pressione o stanco. Mantenendo i tuoi test sotto "È così facile che non ci sono scuse per non fai questa" soglia è importante.
  • Il software perfetto non è fattibile. Come ogni altra cosa, lo sforzo speso per il test è un compromesso, ea volte non ne vale la pena. Esistono vincoli (come la mancanza di un dipartimento di QA). Accetta che i bug possano capitare, recuperare e imparare.

Ho visto la programmazione descritta come l'attività di apprendimento su un problema e 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 diversi punti ma ne ho saltato uno. Posso modificare i miei tipi di dati o le mie classi per rendere la gestione di timestamp più esplicita e difficile da perdere, o per renderla più centralizzata in modo da avere solo uno spazio da modificare? i miei test per verificare altri aspetti della gestione del timestamp? Posso semplificare il mio ambiente di test per renderlo più facile in futuro? Posso immaginare qualche strumento che avrebbe reso tutto più semplice, e in tal caso, posso trovare uno strumento simile su Google? "Ecc.)

    
risposta data 06.10.2015 - 19:11
fonte
7

I updated the version of the library … which … caused timestamps (which the third party library returns as long), to be changed from milliseconds after the epoch to nanoseconds after the epoch.

This is not a bug in the library

Sono strongmente in disaccordo con te qui. è un bug nella libreria , in realtà piuttosto insidioso. Hanno cambiato il tipo semantico del valore restituito, ma non hanno modificato il tipo programmatico del valore restituito. Questo può provocare tutti i tipi di devastazione, specialmente se si tratta di un bump di versione minore, ma anche se fosse uno dei maggiori.

Diciamo che invece la libreria ha restituito un tipo di MillisecondsSinceEpoch , un semplice wrapper che contiene long . Quando lo hanno modificato in un valore NanosecondsSinceEpoch , il tuo codice non sarebbe stato compilato e ovviamente ti avrebbe indirizzato ai luoghi in cui è necessario apportare modifiche. La modifica non ha potuto in alcun modo corrompere il tuo programma.

Ancora meglio sarebbe un oggetto TimeSinceEpoch che potrebbe adattare la sua interfaccia man mano che viene aggiunta più precisione, come aggiungere un metodo #toLongNanoseconds insieme al metodo #toLongMilliseconds , 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 dal resto della tua applicazione. Diverse altre risposte risolvono questo problema (e altre continuano a comparire mentre scrivo). I test di integrazione dovrebbero essere eseguiti meno frequentemente rispetto ai test delle unità. Questo è il motivo per cui avere un buffer layer aiuta. Segrega i tuoi test di integrazione in un'area separata (o dai un nome diverso) in modo da poterli eseguire secondo le necessità, ma non tutte le volte che esegui il test dell'unità.

    
risposta data 06.10.2015 - 19:16
fonte
5

Hai bisogno di test di integrazione e di sistema.

I test unitari sono ottimi per verificare che il codice si comporti come ci si aspetta. Come ti rendi conto, non fa nulla per sfidare le tue ipotesi o garantire che le tue aspettative siano equilibrate.

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

Maggiore è il livello dei test, più ti proteggeranno dagli imprevisti. Questo ha un costo (convenienza, velocità, fragilità ...), quindi i test unitari dovrebbero rimanere il fondamento dei test, ma hai bisogno di altri livelli, tra cui - alla fine - un pezzettino di test umani che fa molto per catturare cose stupide a cui nessuno ha pensato.

    
risposta data 06.10.2015 - 18:44
fonte
2

Il meglio sarebbe creare un prototipo minimale e capire come funziona esattamente la libreria. In tal modo, acquisirai una conoscenza della libreria con una documentazione scarsa. Un prototipo può essere un programma minimalista che utilizza quella libreria e fa la funzionalità.

Altrimenti, non ha senso scrivere test di unità, con requisiti definiti a metà e scarsa comprensione del sistema.

Per quanto riguarda il tuo problema specifico - sull'utilizzo di metriche sbagliate: lo tratterei come un cambiamento di requisiti. Una volta riconosciuto il problema, modifica i test unitari e il codice.

    
risposta data 06.10.2015 - 17:54
fonte
1

Se stavi usando una libreria stabile e popolare, potresti forse supporre che non giocherà brutti scherzi con te. Ma se cose come quello 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 libreria, dovrai esaminare non solo la possibilità che tu abbia commesso un errore, ma anche la possibilità che la biblioteca abbia commesso un errore. Quindi, diciamo che questa è una libreria di cui sei "incerto".

Una delle tecniche impiegate con le librerie di cui siamo "incerti" è di costruire uno strato intermedio tra il nostro sistema e dette librerie, che astrae la funzionalità offerta dalle biblioteche, asserisce che le nostre aspettative della biblioteca sono corrette, e inoltre semplifica enormemente la nostra vita in futuro, dovremmo decidere di dare a quella libreria il boot e sostituirlo con un'altra libreria che si comporta meglio.

    
risposta data 06.10.2015 - 19:02
fonte