A real example of how to develop an application for Windows Phone 7 with Test Driven Development approach. In this presentation you'll see also hoew to implements the Model-View-ViewModel (MVVM) pattern.
7. Track
public class Track
{
public int Id{get; set; }
public string Kind { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Artwork_url { get; set; }
public string Created_at { get; set; }
}
21. Problem with code behind
• Code coupled with UI
– Xaml + Code Behind -> one class
• Not testable
22. MVVM approach
• Architectural Pattern
• Derived from Presentation Model pattern
(Fowler)
• Clear separation between UI and Logic
UI ViewModel
Collections, DelegateCommand, Properties
23. MVVM approach
• Structure our code:
– ViewModel (c#): Logic
– View (Xaml): Presentation
– No more code behind
• Now the ViewModel is testable
24. Test Driven Development
• As easy as complex
• Life Cycle:
– Write test (red)
– Write logic to pass the test (green)
– Refactor code (refactor)
– Again..
25. Test Driven Development
• It’s about code design, not test
• Test suite are good side effect of
tdd
• It require a lot of discipline and
practice
26. Testing Tools
• Nunit for Windows Phone 7
• No official mocking framework for
Windows Phone 7, but I found out
that Moq 3.1 for silverlight works!
37. TDD –Refactor
public class MainViewModel{
private IList<Track> _tracks;
public IList<Track> Tracks
{
get { return _tracks; }
}
public MainViewModel()
{
_tracks = new List<Track>();
}
}
40. Architecture
Main
ViewModel
Ilist<Track>SearchUserTrack(string
)
Search Rest Call
Service
41. Architecture
public interface ISearchService
Main {
ViewModel IList<Track> SearchUserTrack(string user);
}
public class SearchService : ISearchService
{
ISearchService public IList<Track> SearchUserTrack(string user){ }
}
Search Rest Call
Service
42. TDD
[Test]
public void Search_Should_RetrieveSearchedUserTrack ()
{
Mock<ISearchService> service = new Mock<ISearchService>();
MainViewModel viewModel = new MainViewModel(service.Object);
viewModel.SearchedText = "michelecapra";
viewModel.Search.Execute();
service.Verify(p=>p.SearchUserTrack("michelecapra"));
}
43. TDD
[Test]
public void Search_Should_RetrieveSearchedUserTrack ()
{
Mock<ISearchService> service = new Mock<ISearchService>();
MainViewModel viewModel = new MainViewModel(service.Object);
viewModel.SearchedText = "michelecapra";
viewModel.Search.Execute();
service.Verify(p=>p.SearchUserTrack("michelecapra"));
}
44. TDD - Mock
• Simulated objects that mimic the behavior
of real objects in controlled ways
• Mock objects have the same interface as
the real objects they mimic, allowing a
client object to remain unaware of
whether it is using a real object or a mock
object.
45. TDD
[Test]
public void Search_Should_RetrieveSearchedUserTrack ()
{
Mock<ISearchService> service = new Mock<ISearchService>();
MainViewModel viewModel = new MainViewModel(service.Object);
viewModel.SearchedText = "michelecapra";
viewModel.Search.Execute();
service.Verify(p=>p.SearchUserTrack("michelecapra"));
}
46. TDD- ICommand
• The ICommand interface enables the
abstraction of a parameterized method call
through its Execute method.
• Typically objects implement this interface
to enable method calls on the objects
through the use of XAML bindings.
47. TDD- DelegateCommand
• ICommand whose delegates can be
attached for Execute(T)
• Execute(T) is the method to be
called when the command is
invoked.
<Button Command=“”></Button>
48. TDD
[Test]
public void Search_Should_RetrieveSearchedUserTrack ()
{
Mock<ISearchService> service = new Mock<ISearchService>();
MainViewModel viewModel = new MainViewModel(service.Object);
viewModel.SearchedText = "michelecapra";
viewModel.Search.Execute();
service.Verify(p=>p.SearchUserTrack("michelecapra"));
}
49. TDD - Red
Add properties in order to compile
public string SearchedText { get; set; }
public DelegateCommand Search { get; set; }
64. TDD
• We need to introduce the
NavigationService
• But how to decouple it from our
ViewModel?
65. TDD – Navigation Service
public interface INavigationService THX to
{
void NavigateTo(Uri pageUri); Laurent Bugnion
}
public class NavigationService : INavigationService
{
private static PhoneApplicationFrame _mainFrame;
public event NavigatingCancelEventHandler Navigating;
public void NavigateTo(Uri pageUri) { … }
}
66. TDD
[Test]
public void ShowDetail_Should_NavigateToDetailView()
{
var navigationService = new Mock<INavigationService>();
var searchService = new Mock<ISearchService>();
var viewModel = new MainViewModel(searchService.Object,
navigationService.Object);
viewModel.ShowDetail.Execute(new Track{Id=345});
navigationService.Verify(p => p.NavigateTo(new Uri("/View/
DetailView.xaml?id=345", UriKind.Relative)));
}
67. TDD
[Test]
public void ShowDetail_Should_NavigateToDetailView()
{
var navigationService = new Mock<INavigationService>();
var searchService = new Mock<ISearchService>();
var viewModel = new MainViewModel(searchService.Object,
navigationService.Object);
viewModel.ShowDetail.Execute(new Track{Id=345});
navigationService.Verify(p => p.NavigateTo(new
Uri("/View/DetailView.xaml?id=345", UriKind.Relative)));
}
68. TDD
[Test]
public void ShowDetail_Should_NavigateToDetailView()
{
var navigationService = new Mock<INavigationService>();
var searchService = new Mock<ISearchService>();
var viewModel = new MainViewModel(searchService.Object,
navigationService.Object);
viewModel.ShowDetail.Execute(new Track{Id=345});
navigationService.Verify(p => p.NavigateTo(new
Uri("/View/DetailView.xaml?id=345", UriKind.Relative)));
}
69. TDD
[Test]
public void ShowDetail_Should_NavigateToDetailView()
{
var navigationService = new Mock<INavigationService>();
var searchService = new Mock<ISearchService>();
var viewModel = new MainViewModel(searchService.Object,
navigationService.Object);
viewModel.ShowDetail.Execute(new Track{Id=345});
navigationService.Verify(p => p.NavigateTo(new
Uri("/View/DetailView.xaml?id=345", UriKind.Relative)));
}
70. TDD - Red
public class MainViewModel
{
private IList<Track> _tracks;
public IList<Track> Tracks
{
get { return _tracks; }
}
public string SearchedText { get; set; }
public DelegateCommand Search { get; private set; }
public DelegateCommand<Track> ShowDetail{get; set; }
71. TDD - Red
public MainViewModel(ISearchService searchService,
INavigationService navigationService)
{
_searchService = searchService;
_navigationService = navigationService;
_tracks = new List<Track>();
Search = new DelegateCommand(OnSearch);
ShowDetail= new DelegateCommand<Track>(OnShowDetail);
}
72. TDD - Red
public MainViewModel(ISearchService searchService,
INavigationService navigationService)
{
_searchService = searchService;
_navigationService = navigationService;
_tracks = new List<Track>();
Search = new DelegateCommand(OnSearch);
ShowDetail= new DelegateCommand<Track>(OnShowDetail);
}
80. TDD – Test suite refactor
[Test]
public void ShowDetail_Should_NavigateToDetailView()
{
var navigationService = new Mock<INavigationService>();
var searchService = new Mock<ISearchService>();
var viewModel = new MainViewModel(searchService.Object,
navigationService.Object);
viewModel.ShowDetail.Execute(new Track{Id=345});
navigationService.Verify(p => p.NavigateTo(new
Uri("/View/DetailView.xaml?id=345", UriKind.Relative)));
}
81. TDD – Test suite refactor
[Test]
public void ShowDetail_Should_NavigateToDetailView()
{
var navigationService = new Mock<INavigationService>();
var searchService = new Mock<ISearchService>();
var viewModel = new MainViewModel(searchService.Object,
navigationService.Object);
viewModel.ShowDetail.Execute(new Track{Id=345});
navigationService.Verify(p => p.NavigateTo(new
Uri("/View/DetailView.xaml?id=345", UriKind.Relative)));
}
82. TDD – Test suite refactor
public class MainViewModelFixture
{
private Mock<INavigationService> _navigationService;
private Mock<ISearchService> _searchService;
private MainViewModel _viewModel;
[SetUp]
public void Setup()
{
_navigationService = new Mock<INavigationService>();
_searchService = new Mock<ISearchService>();
_viewModel = new MainViewModel(_searchService.Object,
_navigationService.Object);
}
83. TDD – Test suite refactor
public class MainViewModelFixture
{
private Mock<INavigationService> _navigationService;
private Mock<ISearchService> _searchService;
private MainViewModel _viewModel;
[SetUp]
public void Setup()
{
_navigationService = new Mock<INavigationService>();
_searchService = new Mock<ISearchService>();
_viewModel = new MainViewModel(_searchService.Object,
_navigationService.Object);
}
84. ViewModel and UI
• FrameworkElement.DataContext
• “A directly embedded object that
serves as data context for any
bindings within the parent element”
• We’ll put here our ViewModel!
<phone:PhoneApplicationPage DataContext=“”/>
85. ViewModel and UI
public partial class MainView :PhoneApplicationPage
{
public MainView()
{
InitializeComponent();
DataContext = new MainViewModel(new SearchService(),
new NavigationService());
}
}
Applicazione base: Dato un utente di soundcloud scaricare la lista delle sue canzoni pubblicate Mostrarla nella schermata principale Cliccando sulla canzone mostrare I suoi dettagli, come l’immagine e la data di pubblicazione E’ solo un esempio giusto per avere qualcosa su cui discutere
Ecco le due schermate: La prima con liste delle canzoni box e tasto per cercare La seconda dove mostro I dettagli della canzone Non mi sono messo molto sulla grafica perchè ci server solo per lavorare
Proviamo a fare l’applicazione in modo tradizionale
Il tipo di dato che andrò a scaricare da SoundCloud è formato così Ho preso un subset di tutte le informazioni che SoundCloud espone per ogni brano
View semplicficata Ho tolto alcuni tag xaml per questioni di spazio sulla slide
View semplicficata Ho tolto alcuni tag xaml per questioni di spazio sulla slide
Parte in alto Una textbox per inserire il nome dell’artista da cercare Un tasto search per effettuare la ricerca e scaricare I dati
Un itemcontrol che mostra I brani scaricati
Vediamo che cosa viene fatto nel code behind
Utilizzo RestSharp che è un componente per poter effettuare la manipolazione di risorse esposte tramite REST. Definisco l’url su cui lavorare Definisco la rappresentazione della risorsa la voglio in json
Effettuo la chiamata vera e propria il risultato lo carico come itemsource delll’itemcontrol
Definisco il metodo che verrà chiamato quando seleziono una canzone Utilizzo il NavigationService di silverlight per spostarmi nella prossima pagina Passo come parametro alla pagina l’id della canzone che ho selezionato
In questa vista mostro il dettaglio della canzone che ho selezionato
Sostanzialmente mostro l’immagine collegata alla canzone data di creazione Titolo descrizione
Mi aggancio all’evento di navigazione in ingresso alla pagina Reperisco l’id E imposto la chiamata rest
Deserializzo le informazioni reperite E le carico nei controlli
Problemi con questo approccio Codice fortemente accoppiato con l’interfaccia Lo xaml e il code behind di fatto diventano una classe sola (partial) Quindi il codice non è testabile
Per ultima cosa - Mostrare come viene aganciato il viewmodel alla vista
Proviamo a farlo in tdd Questa volta avremo due classi separate: Il viewmodel dovee mettiamo tutta la logica dell’applicazione Lo xaml solo per la presentazione Niente più code behind
In se non è molto difficile Pensare alla funzionalità su cui si sta lavorando Scrivere in modo dichiarativo il test Verificare che sia rotto Scrivere il codice per farlo passare Verificare che il test sia passato Fare refactor del codice scritto Per il refactor vi consiglio il libro di Fowler
Non voglio dilungarmi sul tdd e sul perchè farlo Giusto due cose Tdd serve per ottenere un miglior design del codice I test sono un ottimo effetto collaterale del tdd Disciplina e pratica
Nunit come framework di unit test Moq 3.1 per silverlight funziona su wp7
Partiamo con la prima storia Voglio scaricare le canzoni di un utente che è stato cercato da Soundcloud
immagino che ci sia una lista,intesa come struttura dati che devo mostrare Difficile partire con I test Quindi stupidamente quasi giusto per partire penso che questa lista dovrà essere inizializzata
- Faccio questo perchè ho visto spesso che partire è una delle cose piu difficili quando ci si trova di fronte a questo
Faccio questo perchè ho visto spesso che partire è una delle cose piu difficili quando ci si trova di fronte a questo Istanzio il viewmodel E verifico che il costruttore mi inizializzi la proprietà track
Giusto per essere chiari Serve disciplina Quindi bisogna scriverei minimo codice per fare fallire il test
Creo la proprietà
E finalemente abbiamo la barra rossa
Mi raccomando disciplina Bisgona scrivere il minimo codice per poter fare passare il test
Inizializzo la proprietà
Aggiungo un backfield all proprietà E rendo la proprietà in sola lettura
Faccio riandare I test
Ritorniamo alla storia da implementare Un altro test che mi viene in mente è Se schiaccio search il viewmodel deve recuperare la lista delle canzoni
Penso che ci sia un servicio che si occuperà di fare la chiamata Rest con RestSharp emi restituirà I dati
Per poter disaccoppiare l’implementazione di questo servizio dal viewmodel intriduco un interfaccia Stabilisce il contratto tra le due classi
Arriviamo al test e leggiamolo E poi ve lo spiego in dettaglio
Come visto prima ho introdotto un servizio che si occupa di reperire per me I dati via rest Per potermi concentrare sul viewmodel in realtà nel test non uso direttamente la mia implementazione del servizio Altrimenti test di integrazione ma non so se il test fallisce di chi è la colpa Per poter fare questo uso dei Mock
Simulano il comportamento dell’oggetto che sostituiscono Di fatto implementano l’interfaccia dell’oggetto reale Possono essere più o meno stupidi
Altro elemento nuovo e il comando che viene utilizzato dal tasto search - Definito come una proprietà
Per fare questo ci appoggiamo all’interfaccia ICommand
In realtà l’implementazione che utilizzo io è quella di Prism Dopo vi farò vedere come collegare la proprietà che abbiamo scritto prima con la proprietà command del button
Quindi rieccoci al nostro test
Per fare compilare dobbiamo aggiungere due proprietà al ViewModel
Facciamo girare il test
Implementiamo il comando ed effettuiamo la chiamata Proprio per il fatto che devo aggiungere il minimo codice che serve per passare il test nella OnSearch metto solo la chiamata al servizio
Rendiamo in sola lettura il comando per il principio dell’incapsulation
Altra storia visualizzare la lista Una volta ottenuti I dati dal servizio Vogliamo visualizzarli
Istruiamo il mock per rispondere ad una chiamata ben definita Nello specifico a fronte di una chiamata con michelecapra Ritorna una lista con una canzone.
Nella assert quindi veirifico che dopo la chiamata ci sia una canzone nella lista delle canzoni
In questo caso per poter ottenere il test rosso Non devo fare nulla
In questo caso per poter ottenere il test rosso Non devo fare nulla
Per passarlo invece devo aggiornare il backfield track con il risultato della chiamata al servizio
Per passarlo invece devo aggiornare il backfield track con il risultato della chiamata al servizio
Altra storia cliccando sulla canzone voglio vedere il dettaglio
Per poter navigare da una pagina all’altra abbiamo bisogno del NavigationService Ma come disaccoppiarlo dal nostro ViewModel?
Bellisima implementazione già testata di Laurent Bugnion Per gestire la navigazione tra le pagine
In questo caso viene aggiunta una dipendenza nuova del nostro oggetto
A differenza del prcedente comando Passo la canzone con un id preciso
In questa parte del viewmodel Non cambia nulla
Per poter compilare dobbiamo aggiungere un nuovo parametro al costruttore
Per poter compilare dobbiamo aggiungere un nuovo parametro al costruttore
Per poter passare il test Teniamo il NavigationService da parte in una proprietà privata
E implementiamo la chiamata effettiva
Passiamo il test
niente
Rinomino il parametro passato a OnShowDetail da obj in track
Rifaccio andare I test
Si può fare anche del refactor sulle suite di test
inizializzazione del viewmodel e e dei mock dei servizi si ripete
Allora ho deciso di creare delle variabili private per la suite di test
E di sfruttare un metodo messo a disposizione da Nunit Il Setup viene chiamato prima dell’esecuzione di ogni test
Punto di contatto tra il viewmodel e la vista Infatti possiamo sfruttare la proprietà Datacontext per potere metter li il viewmodel
In questo modo che è il più grezzo possiblie viene inizializzato il datacontext nella vista quando la si naviga
Ssfruttiamo il binding delle proprietà per poter collegare le proprietà del viewmodel con gli oggetti sulla vista
Lo stesso discorso viene fatto con la lista delle canzoni
Lo stesso discorso viene fatto con la lista delle canzoni
Lo stesso discorso viene fatto con la lista delle canzoni
Lo stesso discorso viene fatto con la lista delle canzoni