2012-03-09 17 views
5

Ho iniziato a utilizzare TDD per migliorare la qualità e il design del mio codice, ma di solito ho riscontrato un problema. Proverò a spiegarlo attraverso un semplice esempio: Provo ad implementare una semplice applicazione usando il design della vista passiva. Ciò significa che cerco di rendere la visione più stupida possibile. Consideriamo un'applicazione, in cui la GUI ha un pulsante e un'etichetta. Se l'utente preme il pulsante, viene creato un file con una riga casuale al suo interno. Quindi l'etichetta mostra se la creazione ha avuto successo o meno. Il codice potrebbe essere simile a questo:Apprendimento TDD, sempre in dipendenza circolare

  • interfaccia IView: una singola proprietà stringa setter: Risultato
  • classe GUIEventListener: metodo OnUserButtonClick che viene chiamato dal bottone della GUI
  • classe
  • FileSaver: metodo SaveFile che viene chiamato da GUIEventListener
  • Classe GUIController: metodo UpdateLabel che viene chiamato dal metodo SaveFile della classe FileSaver con un parametro che dipende dal successo del metodo SaveFile.

istanze di simile a questa:

  • di vista ctor: View (GUIEventListener eventListener)
  • ctor di GUIEventListener: GUIEventListener (FileSaver FileSaver)
  • ctor di FileSaver: FileSaver (regolatore GUIController)
  • Responsabile del GUIController: GUIController (Vista)

Come puoi vedere chiaramente, c'è una dipendenza circolare nel design. solito cerco di evitare di utilizzare gli eventi, non mi piace il test con loro e credo che questo tipo di disegno è più auto esplicativo come si afferma chiaramente che cosa sono il rapporto delle classi. Ho sentito parlare dello stile di progettazione IoC, ma non ne ho molta familiarità.

Quali sono i miei "punto critico" in TDD riguardo a questo problema? Finisco sempre in esecuzione in questo problema e voglio imparare un modello adeguato o un principio di evitarlo in futuro.

+0

Sono GUIController e Vista in diversi progetti/gruppi? – Glenn

+0

Non ho molta familiarità con MVC (presumo che lo stiate usando) ma in MVVM quello che chiamereste Controller non ha un riferimento alla vista. Esporrebbe proprietà a cui la vista si legherebbe. Il Controller avrebbe un'istanza del salvaschermo e userebbe i comandi di ritrasmissione per chiamare il salvaschermo quando si fa clic sul pulsante (in modo efficace sostituisce l'ascoltatore di eventi) –

risposta

0

Mi piacerebbe sbarazzarsi della classe GUIEventListener. Sembra un eccesso per me.

Dal momento che la vista sa quando il pulsante viene premuto, lasciare che la quota di vista la sua conoscenza con il mondo:

public interface IView 
{ 
    void DisplayMessage(string message); 
    void AddButtonClickHandler(Action handler); 
} 

Il FileSaver è ancora più semplice:

public interface IFileSaver 
{ 
    Boolean SaveFileWithRandomLine(); 
} 

Solo per divertimento, creiamo un'interfaccia per il controller:

public interface IController 
{ 

} 

E l'implementazione del controller:

public class Controller : IController 
{ 
    public Controller(IView view, IFileSaver fileSaver) 
    { 

    } 
} 

OK, scriviamo i test (sto usando NUnit e MOQ):

[TestFixture] 
public class ControllerTest 
{ 
    private Controller controller; 
    private Mock<IFileSaver> fileSaver; 
    private Mock<IView> view; 
    private Action ButtonClickAction; 

    [SetUp] 
    public void SetUp() 
    { 
     view = new Mock<IView>(); 

     //Let's store the delegate added to the view so we can invoke it later, 
     //simulating a click on the button 
     view.Setup((v) => v.AddButtonClickHandler(It.IsAny<Action>())) 
      .Callback<Action>((a) => ButtonClickAction = a); 

     fileSaver = new Mock<IFileSaver>(); 

     controller = new Controller(view.Object, fileSaver.Object); 

     //This tests if a handler was added via AddButtonClickHandler 
     //via the Controller ctor. 
     view.VerifyAll();   
    } 

    [Test] 
    public void No_button_click_nothing_happens() 
    { 
     fileSaver.Setup(f => f.SaveFileWithRandomLine()).Returns(true); 

     view.Verify(v => v.DisplayMessage(It.IsAny<String>()), Times.Never()); 
    } 

    [Test] 
    public void Say_it_worked() 
    { 
     fileSaver.Setup(f => f.SaveFileWithRandomLine()).Returns(true); 
     ButtonClickAction(); 

     view.Verify(v => v.DisplayMessage("It worked!")); 
    } 

    [Test] 
    public void Say_it_failed() 
    { 
     fileSaver.Setup(f => f.SaveFileWithRandomLine()).Returns(false); 
     ButtonClickAction(); 

     view.Verify(v => v.DisplayMessage("It failed!")); 
    } 
} 

penso che i test sono abbastanza chiare, ma non so se sai Moq.

Il codice completo per il controller potrebbe essere simile alla seguente (ho appena martellato in una sola riga, ma non è necessario, ovviamente):

public class Controller : IController 
{ 
    public Controller(IView view, IFileSaver fileSaver) 
    { 
     view.AddButtonClickHandler(() => view.DisplayMessage(fileSaver.SaveFileWithRandomLine() ? "It worked!" : "It failed!")); 
    } 
} 

Come si può vedere, in questo modo sei in grado di testare il controller, e non abbiamo nemmeno startet per implementare la View o il FileSaver. Usando le interfacce, non devono conoscersi.

La vista non sa nulla (tranne che qualcuno può essere informato quando si fa clic sul pulsante), è il più possibile. Nota che nessun evento inquina l'interfaccia, ma se hai intenzione di implementare la vista in WinForms, nulla ti impedisce di utilizzare eventi all'interno dell'implementazione View. Ma nessuno al di fuori deve saperlo, e quindi non abbiamo bisogno di testarlo.

Il FileSaver salva solo i file e indica se ha avuto esito negativo o meno. Non conosce controller e visualizzazioni.

Il controller mette tutto insieme, senza conoscere le implementazioni. Conosce solo i contratti. Conosce la Vista e il FileSaver.

Con questo modello, testiamo solo il comportamento del controller. Chiediamo: 'Se il pulsante è stato cliccato, la vista è stata informata della necessità di visualizzare tali informazioni?' e così via. È possibile aggiungere altri test per verificare se il metodo di salvataggio su FileSaver è stato chiamato dal controller, se lo si desidera.

Una bella risorsa su questo argomento è The Build Your Own CAB Series by Jeremy Miller

+0

Grazie dkson per la tua risposta dettagliata. Non sto iniziando TDD, quindi conosco l'interfaccia di derisione e così, la mia unica preoccupazione era come evitare eventi e qualsiasi tipo di "proprietà di rilegatura tardiva" Prima di pubblicare questo, ho trovato questo articolo anche dall'argomento hai incollato (che ho letto poche settimane fa e il miglior riassunto dei modelli di progettazione relativi a TDD che ho incontrato finora): http://codebetter.com/jeremymiller/2007/06/04/build-your-own -cab-part-6-view-to-presenter-communication/ – SLOBY

+0

Mi sembra di dover fare un "sacrificio" per ottenere ciò che voglio. Nell'esempio, questa "proprietà di associazione tardiva" è AddButtonClickHandler, nell'articolo menzionato è la funzione AttachPresenter. Grazie comunque per la tua risposta, mi ha aiutato a capire che questa è la soluzione più vicina che posso ottenere da solo. – SLOBY

2

Le UI di test unità sono spesso un problema, per molte ragioni ... Il modo in cui l'ho fatto negli ultimi anni sui progetti MVC è semplicemente testare l'unità solo sui controller e successivamente testare le applicazioni. sopra.

controllori possono essere unità testati facilmente perché sono classi di logica proprio come qualsiasi altro e si può deridere le dipendenze. Le interfacce utente, in particolare per le applicazioni Web, sono molto più difficili. È possibile utilizzare strumenti quali Selenium o WatiN ma che è in realtà l'integrazione/test di accettazione, piuttosto che test di unità.

Ecco qualche ulteriore lettura:

How to get started with Selenium Core and ASP.NET MVC

This is how ASP.NET MVC controller actions should be unit tested

Buona fortuna!

+0

Mi dispiace, forse mi hai frainteso. Non sto cercando modi per testare l'interfaccia utente, ho nodo è difficile. Sto solo cercando modi per evitare di incorrere in dipendenza circolare utilizzando l'iniezione di dipendenza ed evitando di usare eventi. – SLOBY

+0

Tramite l'unità testare i controller anziché i moduli, si evita questo. –

3
  • GUIController classe: metodo UpdateLabel che viene chiamato dalla classe FileSaver SaveFile

...

  • ctor di FileSaver: FileSaver (regolatore GUIController)

Ecco la falla nel tuo design. Il FileSaver dovrebbe essere agnostico di chi lo chiama (leggi: non dovrebbe avere un riferimento al livello sottostante), dovrebbe solo fare il suo lavoro, cioè salvare un file e informare il mondo su come l'operazione è andata, tipicamente attraverso un valore di ritorno.

Questo non è strettamente correlato a TDD, ma forse TDD ti avrebbe costretto a pensare in termini del comportamento di base che ci si aspetta da un FileSaver e rendersi conto che non è sua responsabilità aggiornare un'etichetta (vedere Single Responsibility Principle).

Come per le altre parti del sistema, come Roy ha detto che molto spesso saranno difficili da testare in TDD tranne che per il Controller.

+0

Hai ragione riguardo al problema della dipendenza da FileSaver, dovrebbe solo fare il lavoro e non preoccuparti di nient'altro. Ancora, in questo caso, come si aggiorna la GUI senza eventi? Dopo che il listener chiama il risultato del metodo FileSaver, come aggiornerà la GUI? – SLOBY

Problemi correlati