Best practice per lo sviluppo basato su test utilizzando C # e RhinoMocks [chiuso]


86

Per aiutare il mio team a scrivere codice testabile, ho creato questo semplice elenco di best practice per rendere la nostra base di codice C # più testabile. (Alcuni punti si riferiscono ai limiti di Rhino Mocks, un framework beffardo per C #, ma le regole possono essere applicate anche più in generale.) Qualcuno ha delle best practice da seguire?

Per massimizzare la testabilità del codice, segui queste regole:

  1. Scrivi prima il test, poi il codice. Motivo: questo garantisce la scrittura di codice testabile e che ogni riga di codice riceva test scritti per esso.

  2. Progettare classi utilizzando l'inserimento delle dipendenze. Motivo: non puoi deridere o testare ciò che non può essere visto.

  3. Separare il codice dell'interfaccia utente dal suo comportamento utilizzando Model-View-Controller o Model-View-Presenter. Motivo: consente di testare la logica aziendale mentre le parti che non possono essere testate (l'interfaccia utente) sono ridotte a icona.

  4. Non scrivere metodi o classi statici. Motivo: i metodi statici sono difficili o impossibili da isolare e Rhino Mocks non è in grado di deriderli.

  5. Programma le interfacce, non le classi. Motivo: l'utilizzo delle interfacce chiarisce le relazioni tra gli oggetti. Un'interfaccia dovrebbe definire un servizio di cui un oggetto ha bisogno dal suo ambiente. Inoltre, le interfacce possono essere facilmente derise usando Rhino Mocks e altri framework di mocking.

  6. Isola le dipendenze esterne. Motivo: le dipendenze esterne non risolte non possono essere testate.

  7. Contrassegna come virtuali i metodi che intendi deridere. Motivo: Rhino Mocks non è in grado di simulare metodi non virtuali.


Questa è una lista utile. Attualmente stiamo utilizzando NUnit e Rhino.Mocks, ed è bene precisare questi criteri per i membri del team che hanno meno familiarità con questo aspetto dei test unitari.
Chris Ballard,

Risposte:


58

Sicuramente una buona lista. Ecco alcuni pensieri su di esso:

Scrivi prima il test, poi il codice.

Sono d'accordo, ad alto livello. Ma sarei più specifico: "Scrivi prima un test, quindi scrivi il codice appena sufficiente per superare il test e ripeti." Altrimenti, avrei paura che i miei test unitari sarebbero più simili a test di integrazione o di accettazione.

Progettare classi utilizzando l'inserimento delle dipendenze.

Concordato. Quando un oggetto crea le proprie dipendenze, non hai il controllo su di esse. Inversion of Control / Dependency Injection ti dà quel controllo, permettendoti di isolare l'oggetto sotto test con mock / stub / ecc. Ecco come testare gli oggetti in isolamento.

Separare il codice dell'interfaccia utente dal suo comportamento utilizzando Model-View-Controller o Model-View-Presenter.

Concordato. Si noti che anche il presentatore / controller può essere testato utilizzando DI / IoC, assegnandogli una vista e un modello stubbed / mocked. Dai un'occhiata a Presenter First TDD per saperne di più.

Non scrivere metodi o classi statici.

Non sono sicuro di essere d'accordo con questo. È possibile eseguire un test unitario di un metodo / classe statico senza utilizzare mock. Quindi, forse questa è una di quelle regole specifiche di Rhino Mock che hai menzionato.

Programma le interfacce, non le classi.

Sono d'accordo, ma per un motivo leggermente diverso. Le interfacce forniscono una grande flessibilità allo sviluppatore di software, oltre al semplice supporto per vari framework di oggetti fittizi. Ad esempio, non è possibile supportare correttamente DI senza interfacce.

Isola le dipendenze esterne.

Concordato. Nascondi le dipendenze esterne dietro la tua facciata o adattatore (a seconda dei casi) con un'interfaccia. Ciò ti consentirà di isolare il tuo software dalla dipendenza esterna, che si tratti di un servizio web, una coda, un database o qualcos'altro. Ciò è particolarmente importante quando il tuo team non controlla la dipendenza (ovvero esterna).

Contrassegna come virtuali i metodi che intendi deridere.

Questa è una limitazione di Rhino Mocks. In un ambiente che preferisce gli stub codificati a mano su un framework di oggetti fittizi, ciò non sarebbe necessario.

E un paio di nuovi punti da considerare:

Usa modelli di design creativi. Questo aiuterà con DI, ma ti permette anche di isolare quel codice e testarlo indipendentemente da altra logica.

Scrivi test utilizzando la tecnica Arrange / Act / Assert di Bill Wake . Questa tecnica rende molto chiaro quale configurazione è necessaria, cosa viene effettivamente testato e cosa ci si aspetta.

Non aver paura di lanciare i tuoi mock / stub. Spesso scoprirai che l'uso di framework di oggetti fittizi rende i tuoi test incredibilmente difficili da leggere. Rotolando il tuo, avrai il controllo completo sui tuoi mock / stub e sarai in grado di mantenere leggibili i tuoi test. (Fare riferimento al punto precedente.)

Evita la tentazione di rifattorizzare la duplicazione dei tuoi unit test in classi di base astratte o metodi di installazione / smontaggio. In questo modo si nasconde il codice di configurazione / pulizia allo sviluppatore che tenta di eseguire lo unit test. In questo caso, la chiarezza di ogni singolo test è più importante del refactoring della duplicazione.

Implementare l'integrazione continua. Effettua il check-in del codice su ogni "barra verde". Crea il tuo software ed esegui la tua suite completa di unit test a ogni check-in. (Certo, questa non è una pratica di codifica, di per sé; ma è uno strumento incredibile per mantenere il tuo software pulito e completamente integrato.)


3
Di solito trovo che se un test è difficile da leggere, non è colpa del framework ma del codice che sta testando. Se il SUT è complicato da configurare, forse dovrebbe essere suddiviso in più concetti.
Steve Freeman,

10

Se stai lavorando con .Net 3.5, potresti voler esaminare la libreria di mocking Moq : utilizza alberi di espressione e lambda per rimuovere l'idioma di risposta record non intuitivo della maggior parte delle altre librerie di mocking.

Dai un'occhiata a questo avvio rapido per vedere quanto diventano più intuitivi i tuoi casi di test, ecco un semplice esempio:

// ShouldExpectMethodCallWithVariable
int value = 5;
var mock = new Mock<IFoo>();

mock.Expect(x => x.Duplicate(value)).Returns(() => value * 2);

Assert.AreEqual(value * 2, mock.Object.Duplicate(value));

5
Penso che anche la nuova versione di Rhino Mocks funzioni così
George Mauer,


3

Questo è un post molto utile!

Aggiungerei che è sempre importante comprendere il contesto e il sistema sotto test (SUT). Seguire i principali TDD alla lettera è molto più semplice quando si scrive nuovo codice in un ambiente in cui il codice esistente segue gli stessi principi. Ma quando scrivi nuovo codice in un ambiente legacy non TDD, scopri che i tuoi sforzi TDD possono gonfiarsi rapidamente ben oltre le tue stime e aspettative.

Per alcuni di voi, che vivono in un mondo interamente accademico, le tempistiche e la consegna potrebbero non essere importanti, ma in un ambiente in cui il software è denaro, è fondamentale fare un uso efficace del vostro impegno TDD.

Il TDD è fortemente soggetto alla Legge del Ritorno Marginale Decrescente . In breve, i tuoi sforzi verso TDD sono sempre più preziosi fino a quando non raggiungi un punto di massimo rendimento, dopodiché, il tempo successivo investito in TDD ha sempre meno valore.

Tendo a credere che il valore primario di TDD sia nel boundary (blackbox) così come in occasionali test whitebox di aree mission-critical del sistema.


2

La vera ragione per programmare contro le interfacce non è rendere la vita più facile a Rhino, ma chiarire le relazioni tra gli oggetti nel codice. Un'interfaccia dovrebbe definire un servizio di cui un oggetto ha bisogno dal suo ambiente. Una classe fornisce una particolare implementazione di quel servizio. Leggi il libro "Object Design" di Rebecca Wirfs-Brock su Ruoli, responsabilità e collaboratori.


D'accordo ... aggiornerò la mia domanda per riflettere questo.
Kevin Albrecht,

1

Buona lista. Una delle cose che potresti voler stabilire - e non posso darti molti consigli poiché sto appena iniziando a pensarci da solo - è quando una classe dovrebbe essere in una libreria, uno spazio dei nomi, spazi dei nomi annidati diversi. Potresti anche voler calcolare in anticipo un elenco di librerie e spazi dei nomi e chiedere al team di incontrarsi e decidere di unire due / aggiungerne uno nuovo.

Oh, ho solo pensato a qualcosa che faccio e che potresti volere anche tu. Generalmente ho una libreria di unit test con un dispositivo di test per politica di classe in cui ogni test va in uno spazio dei nomi corrispondente. Tendo anche ad avere un'altra libreria di test (test di integrazione?) Che è in uno stile più BDD . Questo mi consente di scrivere test per specificare cosa dovrebbe fare il metodo e cosa dovrebbe fare l'applicazione in generale.


Faccio anche una sezione di test in stile BDD simile (oltre al codice di unit test) in un progetto personale.
Kevin Albrecht

0

Eccone un altro che ho pensato che mi piace fare.

Se prevedi di eseguire test dalla Gui di unit test anziché da TestDriven.Net o NAnt, ho trovato più semplice impostare il tipo di progetto di unit test sull'applicazione console piuttosto che sulla libreria. Ciò consente di eseguire i test manualmente e di esaminarli in modalità di debug (cosa che il suddetto TestDriven.Net può effettivamente fare per te).

Inoltre, mi piace sempre avere un progetto Playground aperto per testare bit di codice e idee con cui non ho familiarità. Questo non dovrebbe essere controllato nel controllo del codice sorgente. Ancora meglio, dovrebbe essere in un repository di controllo del codice sorgente separato solo sulla macchina dello sviluppatore.

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.