Perché il test del white box è scoraggiato in OOP?

1

Sembra che il consenso generale per le classi di test unitari sia quello di testare il tuo oggetto solo attraverso la sua interfaccia pubblica. Quindi, se volevi testare il metodo removeElement su una classe LinkedList dovresti chiamare addElement , poi removeElement e infine containsElement per affermare che l'elemento è stato rimosso.

Questo è fragile perché se addElement o containsElement si rompono, il test fallirà anche se l'implementazione di removeElement è corretta.

Quando provo le procedure standalone, provo a chiamarle isolatamente. Se dovessi testare una procedura removeElement , creerò lo stato dei parametri direttamente nel test e poi asserirò che lo stato della chiamata è corretto.

L'unica differenza tra un metodo e una procedura è che a un metodo viene assegnato implicitamente l'oggetto come parametro. Quindi dal momento che list.removeElement(el) e removeElement(list, el) sono funzionalmente uguali, perché non testarli allo stesso modo? per esempio. Nel test, crea un'istanza della classe LinkedList, configura i suoi campi "privati", chiama removeElement , e asserisci i suoi campi post-call modificati correttamente.

Questo è il test unitario ideale perché riguarda l'input e l'output di asserzione per una singola unità di funzionalità. Dover chiamare metodi pubblici A, B, C, D, E e F solo per testare il metodo G è un test di integrazione borderline, può potenzialmente creare falsi positivi (poiché i dati stessi non vengono mai convalidati), e rende isolante il fallimento del test durante la manutenzione più difficile.

Ho scoperto in modo anonimo che il test della scatola nera tenta gli sviluppatori di aggiungere metodi pubblici non necessari per rendere il loro test "più semplice" ma aumenta la manutenzione a lungo termine.

Quindi la mia domanda è: perché il test della scatola bianca è scoraggiato nel mondo OO quando sembra che abbia un senso comune nel mondo procedurale e funzionale?

EDIT : esiste un modo OO per gestire i grip che ho delineato nel mio post, in particolare nella parte successiva che not implica l'aggiunta di nuovi metodi pubblici ed evitare di chiamare metodi pubblici diversi da quelli testati? Considera il dilemma di affermare il puntatore del nodo "precedente" nel mio commento .

EDIT # 2 : Apparentemente il mio concetto di classe potrebbe essere diverso dagli altri. Una classe per me è solo un (molto vecchio) modello di progettazione: "costrutto", "consuma" (ad es. Metodi di chiamata) e "destruct" che non è diverso da fopen , fread, fwrite fseek e fclose in C Indipendentemente dal fatto che sia implicato un parametro implicito, le cose vengono riempite dietro uno spazio dei nomi, o vengono chiamate private, pubbliche o protette, tutto è solo trasformazioni di dati e dati alla fine della giornata. Ho difficoltà a cogliere le classi come un'unità quando sembra più simile a un modello di progettazione o addirittura a un "contenitore" per le unità reali che sono le funzioni stesse.

    
posta Zippers 18.06.2017 - 19:41
fonte

2 risposte

13

Stai mescolando due cose correlate, ma comunque diverse:

  • test della casella bianca

  • test dell'unità utilizzando metodi privati

Le ragioni per scrivere o non scrivere unit test solo usando metodi pubblici sono state discusse numerose volte prima su questo sito, ad esempio qui o qui . Non penso che abbia senso ripetere questi argomenti, se questa è la tua domanda, probabilmente troverai una risposta seguendo quei link.

Il test della casella bianca, tuttavia, non significa utilizzare membri privati per l'impostazione di un test. Significa progettare test utilizzando conoscenze specifiche sui componenti interni della classe o componente testati. Ad esempio, creando test per ottenere la copertura completa del codice e / o la copertura delle filiali, e questo in genere viene fatto da utilizzando solo membri pubblici . Quindi il test del riquadro bianco richiede di conoscere gli interni di una classe, ma direttamente non utilizza l'accesso agli interni. Ciò consente al progettista di un componente in una situazione in cui può ancora modificare i dettagli di implementazione senza preoccuparsi troppo dei test.

Questo tipo di test è non scoraggiato in OOP , al contrario. Il noto Test Driven Development (che è popolare in OOP e in non-OOP) è una forma di test che in realtà porta a questo tipo di test: ogni volta che si desidera aggiungere una nuova funzione a una funzione, una classe di componenti, per prima cosa si scrive un test "rosso", si aggiunge del nuovo codice o si modifica un codice esistente per aggiungere la funzione, e poiché il nuovo test ora diventa "verde", è ovvio che il codice aggiunto o modificato deve essere stato coperto dal test.

Al tuo esempio: se removeElement è un metodo pubblico di un modulo "elenco", e non un membro di una classe, ti consiglio comunque di provare usando solo l'interfaccia pubblica di quel modulo, proprio come se era una classe. Il tuo esempio di una percentuale incompleta di addElement o% co_de è inventato (e la tua idea di "evitare di chiamare metodi pubblici diversi da quelli testati" è - senza offesa - fuorviata). In realtà, si potrebbe progettare un test del genere

  • creazione di un nuovo elenco
  • asserire che l'elenco non contiene l'elemento X
  • aggiungi un elemento X all'elenco
  • asserire che l'elenco delle righe contiene l'elemento X
  • rimuovi l'elemento X dall'elenco
  • asserire che l'elenco non contiene più l'elemento X

che è tutto possibile utilizzando metodi pubblici.

Se containsElement o addElement sono stati interrotti, la suddetta sequenza di test si assicura che il test riveli questo (e non dia un falso positivo per containsElement ).

Naturalmente, ci sono casi di classi o componenti complessi in cui l'utilizzo dell'interfaccia pubblica da sola potrebbe non essere l'approccio migliore per creare uno scenario di test completo e dove può essere utile sciogliere l'incapsulamento in una certa misura, ad esempio, aggiungendo "botole di manutenzione" nel codice. Ma penso che questi casi siano casi eccezionali, e un buon test e la progettazione dei componenti dovrebbero cercare di evitare queste situazioni. Un componente semplice come una lista collegata non dovrebbe richiedere tali misure.

    
risposta data 18.06.2017 - 20:13
fonte
0

Penso che cosa sta succedendo qui è una differenza di prospettiva: vedi le classi come l'unità o le funzioni più piccole? Ho intenzione di decostruire la mia domanda dal punto di vista di qualcuno che vede classi come l'unità più piccola:

[The] ideal unit test ... is about taking input and asserting output for a single unit of functionality. Having to call public methods A, B, C, D, E, and F just to test method G is a borderline integration test.

Questo non ha senso perché la classe è l'unità più piccola, quindi chiamare più metodi non è un problema perché verifica l'unità nel suo complesso.

[It] can potentially create false positives (since the data itself is never validated).

Non si tratta di dati affidabili riguardo all'interfaccia e se appare per comportarsi correttamente.

Isolating the failure of the test during maintenance is more difficult.

Se un test fallisce, non importa quale metodo abbia indotto l'errore perché l'unità nel suo complesso è rotta. È difficile trovare la causa principale perché il risultato risolve l'intera unità.

If you wanted to test the removeElement method on a LinkedList class you'd need to call addElement, then removeElement, and lastly containsElement to assert the element was removed. This is brittle because if addElement or containsElement broke, then the test will fail even if the implementation of removeElement is correct.

Non si tratta di un metodo fallito, si tratta della classe nel suo complesso in mancanza. Testare un metodo specifico in isolamento non ha senso. È meglio testare la classe nel suo complesso attraverso il flusso di lavoro oi test dei requisiti.

Ora stabilirò la prospettiva di qualcuno che visualizza le funzioni come l'unità più piccola. Penso di averlo descritto meglio nella modifica n. 2 (ho modificato leggermente la dicitura):

A class is a design pattern: "construct", "consume", and "destruct" which is no different than fopen, fread/fwrite/fseek, and fclose in C. Regardless of whether there is an implicit self parameter, definitions are placed in a namespace, or you call it private, public, or protected everything is data and data transformations at the end of the day.

Da questa prospettiva non c'è niente di sbagliato nello stabilire input di dati direttamente durante il test. Se non lo fai, finisci per giocare con la sequenza di chiamata della funzione per spingere i dati (di cui non dovresti sapere nulla) in uno stato che puoi indirettamente verifica; questo sacrifica la confidenza diretta dei dati e la chiarezza dei test.

Sembra che ci sia una divisione riguardo al fatto se gli ingegneri debbano pensare in termini di trasformazioni o astrazioni di dati e dati ea quale livello durante lo sviluppo e il testing. Ciò solleva la domanda sul motivo per cui esiste questa divisione. È una domanda importante perché ha un effetto a catena sul modo in cui i tecnici visualizzano, progettano e testano il software. Sono tentato di pubblicare una domanda separata perché credo che valga la pena chiedere.

    
risposta data 19.06.2017 - 21:56
fonte