Scrivere codice testabile vs evitare la generalità speculativa


11

Stamattina stavo leggendo alcuni post sul blog e mi sono imbattuto in questo :

Se l'unica classe che implementa mai l'interfaccia del cliente è CustomerImpl, non hai davvero polimorfismo e sostituibilità perché non c'è nulla in pratica da sostituire in fase di esecuzione. È falsa generalità.

Ciò ha senso per me, poiché l'implementazione di un'interfaccia aggiunge complessità e, se esiste sempre un'unica implementazione, si potrebbe sostenere che essa aggiunge complessità inutili. Scrivere un codice più astratto di quanto sia necessario è spesso considerato un odore di codice chiamato "generalità speculativa" (anche menzionato nel post).

Ma, se seguo TDD, non posso (facilmente) creare duplicati di test senza quella generalità speculativa, sia nella forma di implementazione dell'interfaccia che nella nostra altra opzione polimorfica, rendendo la classe ereditabile e i suoi metodi virtuali.

Quindi, come possiamo conciliare questo compromesso? Vale la pena di essere speculativamente generale per facilitare i test / TDD? Se stai usando i doppi di test, questi contano come seconde implementazioni e quindi rendono la generalità non più speculativa? Dovresti prendere in considerazione un framework di derisione più pesante che permetta di deridere i collaboratori concreti (ad esempio Moles contro Moq nel mondo C #)? Oppure, dovresti testare con le classi concrete e scrivere quelli che potrebbero essere considerati test di "integrazione" fino a quando il tuo progetto richiede naturalmente il polimorfismo?

Sono curioso di leggere le opinioni di altre persone su questo argomento - grazie in anticipo per le tue opinioni.


Personalmente penso che le entità non dovrebbero essere derise. Io prendo in giro solo i servizi, e quelli hanno bisogno di un'interfaccia in ogni caso, perché il codice di dominio del codice in genere non ha alcun riferimento al codice in cui i servizi sono implementati.
CodesInChaos,

7
Noi utenti di lingue tipizzate dinamicamente ridiamo del tuo sfregio contro le catene che le tue lingue tipizzate staticamente ti hanno messo. Questa è una cosa che semplifica i test unitari in linguaggi tipizzati dinamicamente, non devo aver sviluppato un'interfaccia per sottotitolare un oggetto a scopo di test.
Winston Ewert,

Le interfacce non vengono utilizzate solo per effettuare la generalità. Sono utilizzati per molti scopi, il disaccoppiamento del codice è uno dei più importanti. Il che a sua volta rende molto più semplice il test del codice.
Marjan Venema,

@WinstonEwert Questo è un interessante vantaggio della digitazione dinamica che non avevo mai considerato prima come qualcuno che, come fai notare, in genere non funziona in linguaggi tipizzati dinamicamente.
Erik Dietrich,

@CodeInChaos Non avevo considerato la distinzione ai fini di questa domanda, sebbene sia una distinzione ragionevole da fare. In questo caso, potremmo pensare a classi di servizi / framework con una sola (attuale) implementazione. Diciamo che ho un database a cui accedo con DAO. Non dovrei interfacciare i DAO fino a quando non avrò un modello di persistenza secondario? (Questo sembra essere ciò che l'autore del post sul blog sta insinuando)
Erik Dietrich,

Risposte:


14

Sono andato a leggere il post sul blog e sono d'accordo con molte delle affermazioni dell'autore. Tuttavia, se stai scrivendo il tuo codice utilizzando le interfacce a scopo di unit test, direi che l'implementazione fittizia dell'interfaccia è la tua seconda implementazione. Direi che in realtà non aggiunge molto in termini di complessità al tuo codice, soprattutto se il compromesso di non farlo comporta che le tue classi siano strettamente accoppiate e difficili da testare.


3
Assolutamente giusto. Il test del codice fa parte dell'applicazione perché è necessario per ottenere la progettazione, l'implementazione, la manutenzione, ecc. Giusti. Il fatto che tu non lo spedisca al cliente è irrilevante - se c'è una seconda implementazione nella tua suite di test, allora la generalità è lì e dovresti accontentarla.
Kilian Foth,

1
Questa è la risposta che trovo più convincente (e @KilianFoth aggiungendo che esiste ancora una seconda implementazione, indipendentemente dal fatto che il codice venga spedito o meno). Tratterò l'accettazione un po 'della risposta per vedere se qualcuno interviene, comunque.
Erik Dietrich,

Vorrei anche aggiungere, quando si dipende dalle interfacce nei test, la generalità non è più speculativa
Pete

"Non farlo" (utilizzando le interfacce) non comporta automaticamente che le tue classi siano strettamente accoppiate. Semplicemente no. Ad esempio in .NET Framework, esiste una Streamclasse, ma non esiste un accoppiamento stretto.
Luke Puplett,

3

Testare il codice in generale non è facile. Se lo fosse, lo avremmo fatto tutto molto tempo fa, e non avremmo fatto così tanto solo negli ultimi 10-15 anni. Una delle maggiori difficoltà è sempre stata nel determinare come testare il codice che è stato scritto in modo coeso, ben ponderato e testabile senza interrompere l'incapsulamento. Il preside BDD suggerisce che ci concentriamo quasi interamente sul comportamento, e in qualche modo sembra suggerire che non devi davvero preoccuparti dei dettagli interiori in misura così grande, ma questo può spesso rendere le cose abbastanza difficili da testare se ci sono molti metodi privati ​​che fanno "cose" in un modo molto nascosto, in quanto può aumentare la complessità complessiva del test per gestire tutti i possibili risultati a un livello più pubblico.

Il deridere può aiutare in una certa misura, ma di nuovo è focalizzato abbastanza esternamente. L'iniezione delle dipendenze può anche funzionare abbastanza bene, sempre con simulazioni o doppie di prova, ma ciò può anche richiedere che tu esponga elementi tramite un'interfaccia o direttamente, che altrimenti avresti preferito preferire rimanere nascosto - questo è particolarmente vero se desideri avere un buon livello di sicurezza paranoico su certe classi all'interno del tuo sistema.

Per me, la giuria non ha ancora deciso se progettare le tue lezioni in modo che siano più facilmente verificabili. Questo può creare problemi se ti trovi nella necessità di fornire nuovi test mantenendo il codice legacy. Accetto che dovresti essere in grado di provare assolutamente tutto in un sistema, eppure non mi piace l'idea di esporre - anche indirettamente - gli interni privati ​​di una classe, solo per poter scrivere un test per loro.

Per me, la soluzione è sempre stata quella di adottare un approccio abbastanza pragmatico e combinare una serie di tecniche per soddisfare ogni situazione specifica. Uso molti doppi di test ereditati per esporre proprietà e comportamenti interni per i miei test. Prendo in giro tutto ciò che può essere collegato alle mie lezioni e, laddove ciò non comprometta la sicurezza delle mie lezioni, fornirò un mezzo per sovrascrivere o iniettare comportamenti ai fini del test. Prenderò anche in considerazione la possibilità di fornire un'interfaccia più guidata dagli eventi se ciò contribuirà a migliorare la capacità di testare il codice

Dove trovo qualsiasi codice " non verificabile " , cerco di vedere se riesco a refactoring per rendere le cose più testabili. Dove hai un sacco di codice privato nascosto dietro le quinte, spesso troverai nuove classi in attesa di essere lanciate. Queste classi potrebbero essere utilizzate internamente, ma spesso possono essere testate indipendentemente con comportamenti meno privati ​​e spesso successivamente con livelli di accesso e complessità inferiori. Una cosa che devo fare attenzione, tuttavia, è scrivere il codice di produzione con il codice di test integrato. Può essere allettante creare " test bugs " che comportino l'inclusione di orrori come questo if testing then ..., il che indica un problema di test non completamente decostruito e non completamente risolto.

Potresti trovare utile leggere il libro xUnit Test Patterns di Gerard Meszaros , che copre tutto questo genere di cose in modo molto più dettagliato di quanto io possa entrare qui. Probabilmente non faccio tutto ciò che suggerisce, ma aiuta a chiarire alcune delle situazioni di test più complicate da affrontare. Alla fine della giornata, vuoi essere in grado di soddisfare i tuoi requisiti di test pur applicando i tuoi progetti preferiti e aiuta a comprendere meglio tutte le opzioni al fine di decidere meglio dove potresti dover scendere a compromessi.


1

Il linguaggio che usi ha un modo per "deridere" un oggetto per il test? In tal caso queste fastidiose interfacce possono scomparire.

Su una nota diversa ci possono essere ragioni per avere un SimpleInterface e un unico ComplexThing che lo implementa. Potrebbero esserci pezzi di ComplexThing che non desideri siano accessibili all'utente SimpleInterface. Non è sempre a causa di un esagerato codificatore OO-ish.

Ora mi allontano e lascio che tutti saltino sul fatto che il codice che fa questo "ha un cattivo odore" per loro.


Sì, io lavoro in linguaggi con quadri beffardi che supportano beffe di oggetti concreti. Questi strumenti richiedono vari gradi di salto attraverso i cerchi per farlo.
Erik Dietrich,

0

Risponderò in due parti:

  1. Non hai bisogno di interfacce se sei interessato solo ai test. Uso i framework di derisione a tale scopo (in Java: Mockito o easymock). Credo che il codice progettato non debba essere modificato a scopo di test. Scrivere codice testabile, equivale a scrivere codice modulare, quindi tendo a scrivere codice modulare (testabile) e testare solo le interfacce pubbliche del codice.

  2. Ho lavorato in un grande progetto Java e sto diventando profondamente convinto che usare le interfacce di sola lettura (solo getter) sia la strada da percorrere (tieni presente che sono un grande fan dell'immutabilità). La classe di implementazione può avere setter, ma si tratta di un dettaglio di implementazione che non deve essere esposto a layer esterni. Da un'altra prospettiva preferisco la composizione rispetto all'eredità (modularità, ricordi?), Quindi anche le interfacce aiutano qui. Sono disposto a pagare il costo della generallità speculativa piuttosto che spararmi nel piede.


0

Ho visto molti vantaggi da quando ho iniziato a programmare di più su un'interfaccia oltre il polimorfismo.

  • Mi costringe a pensare in anticipo all'interfaccia della classe (sono metodi pubblici) e al modo in cui interagirà con le interfacce di altre classi.
  • Mi aiuta a scrivere classi più piccole che sono più coerenti e seguono il principio della responsabilità singola.
  • È più facile testare il mio codice
  • Meno classi statiche / stato globale in quanto la classe deve essere a livello di istanza
  • È più facile integrare e assemblare i pezzi insieme prima che l'intero programma sia pronto
  • Iniezione delle dipendenze, che separa la costruzione di oggetti dalla logica aziendale

Molte persone concordano sul fatto che un numero maggiore di classi più piccole è meglio di un numero inferiore di classi più grandi. Non devi concentrarti su così tanto in una volta e ogni classe ha uno scopo ben definito. Altri potrebbero dire che stai aumentando la complessità avendo più classi.

È utile utilizzare strumenti per migliorare la produttività, ma penso che basarsi esclusivamente su framework Mock e tali al posto di costruire testabilità e modularità direttamente nel codice comporterà un codice di qualità inferiore nel lungo periodo.

Tutto sommato, credo che mi abbia aiutato a scrivere un codice di qualità superiore e i benefici superano di gran lunga qualsiasi conseguenza.

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.