Come posso applicare TDD alle funzioni di lettura / scrittura?


10

Sembra un problema di pollo e uova.

Puoi fare scrivere una funzione di scrittura in alcuni archivi di dati, ma non sai mai di averla salvata correttamente senza una funzione di lettura testata.

Puoi fare in modo che una funzione di lettura venga letta da un archivio dati, ma come metti cose in quell'archivio dati da leggere senza una funzione di scrittura testata?

MODIFICARE:

Mi sto collegando e facendo transazioni con un database SQL per salvare e caricare oggetti da usare. Non ha senso testare le funzioni di accesso fornite dal DB, ma avvolgo tali funzioni DB per serializzare / deserializzare gli oggetti. Voglio essere sicuro di scrivere e leggere correttamente le cose giuste da e verso il DB.

Non è come aggiungere / eliminare, come menziona @snowman. Voglio sapere che i contenuti che ho scritto sono corretti, ma che richiede una funzione di lettura ben testata. Quando leggo, voglio essere sicuro che la mia lettura abbia creato correttamente un oggetto uguale a quello che è stato scritto; ma ciò richiede una funzione di scrittura ben collaudata.


Stai scrivendo il tuo archivio dati o ne usi uno esistente? Se ne stai usando uno esistente, supponi che funzioni già. Se stai scrivendo il tuo, funziona allo stesso modo di qualsiasi altro software che utilizza TDD.
Robert Harvey,


1
Sebbene strettamente correlato, non penso che questo sia un duplicato di quella particolare domanda. Il target duplicato parla di aggiungi / elimina, questo è read / write. La differenza è che una dipendenza lettura / scrittura si basa probabilmente sul contenuto degli oggetti letti / scritti, mentre un semplice test di aggiunta / eliminazione sarebbe probabilmente molto più semplice: l'oggetto esiste o no.

2
È possibile mettere in scena un database con dati e nessuna funzione di scrittura per testare una funzione di lettura.
JeffO

Risposte:


7

Inizia con la funzione di lettura.

  • Nell'impostazione del test : creare il database e aggiungere i dati del test. tramite script di migrazione o da un backup. Poiché questo non è il tuo codice, non richiede un test in TDD

  • Nel test : installa il tuo repository, punta al tuo db di test e chiama il metodo Read. Verificare che i dati del test vengano restituiti.

Ora hai una funzione di lettura completamente testata, puoi passare alla funzione di scrittura, che può utilizzare la lettura esistente per verificare i propri risultati


Immagino che potresti creare un DB in memoria per velocizzare le cose, ma potrebbe essere troppo complesso. Perché non usare derisioni invece nei test unitari?
BЈовић

1
Un po 'di Devil's Advocate qui, ma come testare che il database è stato creato correttamente? Come affermato da OP, pollo e uova.
user949300

1
@Ewan - Sono assolutamente d'accordo che non dovresti testare il loro codice DB. Ma come fai a sapere che il tuo codice di installazione del DB non ha dimenticato un INSERT da qualche parte o ha inserito un valore errato in una colonna?
user949300

1
da un approccio TDD puro il test è il requisito. quindi logicamente non può essere sbagliato. obvs nel mondo reale devi guardarlo negli occhi
Ewan

1
Quis custodiet ipsos custodes? Oppure, "Chi verifica i test?" :-) Concordo con te sul fatto che in un mondo di TDD purista questo sarebbe il modo orribilmente noioso e soggetto a bug (specialmente se fosse una complicata struttura a più tavoli con 8 JOIN) per farlo. Upvoted.
user949300

6

Spesso faccio solo una scrittura seguita da una lettura. ad es. (pseudocodice)

Foo foo1 = setup some object to write
File tempfile = create a tempfile, possibly in memory 
writeFoo(foo1, tempfile) 
Foo foo2 = readFoo(tempfile) 
assertEquals(foo1, foo2); 
clean-up goes here

Aggiunto in seguito

Oltre a considerare questa soluzione "prgamatica" e "abbastanza buona", si potrebbe sostenere che le altre soluzioni testano la cosa sbagliata . Testare se le stringhe o le istruzioni SQL corrispondono non è un'idea terribile, l'ho fatto da solo, ma sta testando un effetto collaterale ed è fragile. Cosa succede se si modifica la capitalizzazione, si aggiunge un campo o si aggiorna un numero di versione all'interno dei propri dati? Cosa succede se il driver SQL cambia l'ordine delle chiamate per maggiore efficienza o il serializzatore XML aggiornato aggiunge uno spazio aggiuntivo o modifica una versione dello schema?

Ora, se devi aderire in modo molto rigoroso ad alcune specifiche ufficiali, allora sono d'accordo che il controllo dei dettagli è appropriato.


1
Perché è pseudocodice davvero denso al 90%? Non sono sicuro. Forse evidenziare il testo e rendere il codice meno rumoroso?
RubberDuck

1
Sì @Ewan. Lo zelota disapproverebbe ciò, tuttavia il programmatore pragmatico direbbe "abbastanza buono" e andrà avanti.
RubberDuck,

1
ho letto la domanda come .. "supponendo che seguo TDD come uno zelota ..."
Ewan

1
la mia interpretazione di OP e TDD è che il tuo test dovrebbe essere scritto per primo e non usare sia in lettura che in scrittura a meno che uno non sia testato altrove.
Ewan

2
stai testando, "leggi dovrebbe restituire ciò che scrivo" ma i requisiti sono "leggi dovrebbe restituire i dati dal db" e "scrivi dovrebbe scrivere i dati nel db"
Ewan

4

Non farlo. Non eseguire I / O di unit test. È una perdita di tempo.

Logica di unit test. Se è presente molta logica che si desidera testare nel codice I / O, è necessario refactoring del codice per separare la logica di come si esegue l'I / O e ciò che si esegue dall'attività effettiva di I / O (che è quasi impossibile testare).

Per elaborare un po ', se si desidera testare un server HTTP, è necessario farlo attraverso due tipi di test: test di integrazione e test unitari. I test unitari non devono assolutamente interagire con l'I / O. È lento e introduce molte condizioni di errore che non hanno nulla a che fare con la correttezza del codice. I test unitari non dovrebbero essere soggetti allo stato della tua rete!

Il tuo codice dovrebbe separare:

  • La logica di determinare quali informazioni inviare
  • La logica di determinare quali byte inviare per inviare un determinato bit di informazioni (come codificare una risposta ecc. In byte non elaborati) e
  • Il meccanismo per scrivere effettivamente quei byte in un socket.

I primi due riguardano la logica e le decisioni e necessitano di unit test. L'ultimo non implica prendere molte o nessuna decisione e può essere testato meravigliosamente usando i test di integrazione.

Questo è solo un buon design in generale, in realtà, ma uno dei motivi è che lo rende più facile da testare.


Ecco alcuni esempi:

  • Se si sta scrivendo codice che ottiene dati da un database relazionale, è possibile testare l'unità su come mappare i dati restituiti dalle query relazionali al modello dell'applicazione.
  • Se si sta scrivendo codice che scrive i dati in un database relazionale, è possibile testare unità di dati quali dati si desidera scrivere nel database senza effettivamente testare le query SQL specifiche utilizzate. Ad esempio, è possibile mantenere in memoria due copie dello stato dell'applicazione: una copia che rappresenta l'aspetto del database e la copia di lavoro. Quando si desidera eseguire la sincronizzazione con il database, è necessario differli e scrivere le differenze nel database. Puoi facilmente testare l'unità di quel codice diff.
  • Se si sta scrivendo codice che legge qualcosa da un file di configurazione, si desidera testare il parser del formato del file di configurazione, ma con le stringhe del file di origine del test anziché le stringhe ottenute dal disco.

2

Non so se questa è una pratica standard o no, ma funziona bene per me.

Nelle implementazioni del mio metodo di lettura e scrittura non basate su database, utilizzo i miei metodi specifici toString()e il tipo fromString()come dettagli di implementazione.

Questi possono essere facilmente testati separatamente:

 assertEquals("<xml><car type='porsche'>....", new Car("porsche").toString());

Per i metodi di lettura e scrittura effettivi ho un test di integrazione che legge e scrive fisicamente in un test

A proposito: c'è qualcosa di sbagliato ad avere un test che prova a leggere / scrivere insieme?


Potrebbe non essere piacevole o "puro", ma questa è la soluzione pragmatica.
RubberDuck,

Anche a me piace l'idea di provare a leggere e scrivere insieme. Il tuo toString () è un bel compromesso pragmatico.
user949300

1

I dati noti devono essere formattati in modo noto. Il modo più semplice per implementarlo è usare una stringa costante e confrontare il risultato, come descritto da @ k3b.

Non sei limitato alle costanti però. Potrebbero esserci un certo numero di proprietà dei dati scritti che è possibile estrarre utilizzando un diverso tipo di parser, come espressioni regolari o persino sonde ad hoc che cercano funzionalità dei dati.

Per quanto riguarda la lettura o la scrittura dei dati, può essere utile disporre di un file system in memoria che consenta di eseguire i test senza la possibilità di interferenze da altre parti del sistema. Se non si ha accesso a un buon file system in memoria, utilizzare un albero di directory temporaneo.


1

Usa iniezione di dipendenza e derisione.

Non si desidera testare il driver SQL e non si desidera verificare se il database SQL è in linea e configurato correttamente. Sarebbe parte di un test di integrazione o di sistema. Si desidera verificare se il codice invia le istruzioni SQL che dovrebbe inviare e se interpreta le risposte nel modo in cui dovrebbe.

Quindi, quando hai un metodo / classe che dovrebbe fare qualcosa con un database, non farlo ottenere da solo quella connessione al database. Modificarlo in modo che l'oggetto che rappresenta la connessione al database gli sia passato.

Nel tuo codice di produzione, passa l'oggetto database effettivo.

Nei test unitari, passa un oggetto simulato che si comporta come se un database reale non contatta effettivamente un server di database. Basta controllare se riceve le istruzioni SQL che dovrebbe ricevere e quindi risponde con risposte codificate.

In questo modo è possibile testare il livello di astrazione del database senza nemmeno aver bisogno di un database reale.


Devil's Advocate: come fai a sapere quali istruzioni SQL deve "ricevere"? Cosa succede se il driver DB ottimizza l'ordine da ciò che appare nel codice?
user949300

@ user949300 Un oggetto di simulazione del database di solito sostituisce il driver del database.
Philipp

quando si sta testando un repository non ha senso iniettare un client di database deriso. Devi verificare che il tuo codice esegua sql che funziona sul database. altrimenti finisci solo per testare il tuo finto
Ewan

@Ewan Non è questo il test unitario. Un test unitario verifica un'unità di codice, isolata dal resto del mondo. Non stai testando le interazioni tra componenti, come il tuo codice e il database. Ecco a cosa servono i test di integrazione.
Philipp

sì. sto dicendo che non c'è nessun punto di test di un repository db. test di integrazione è l'unica cosa che vale la pena fare
Ewan

0

Se si utilizza un mappatore relazionale di oggetti, in genere esiste una libreria associata che può essere utilizzata per verificare che i mapping funzionino correttamente creando un aggregato, persistendolo e ricaricandolo da una nuova sessione, seguito da una verifica dello stato rispetto l'oggetto originale.

NHibernate offre test sulle specifiche di persistenza . Può essere configurato per funzionare contro un archivio in memoria per test di unità veloci.

Se segui la versione più semplice dei modelli di repository e Unit of Work e collaudi tutti i tuoi mapping, puoi contare su cose praticamente funzionanti.

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.