Xamarin.Forms
Test unitario
Ricerca…
Test dei modelli di visualizzazione
Prima di iniziare ...
In termini di livelli applicativi, ViewModel è una classe contenente tutte le regole e le regole aziendali che fanno sì che l'app esegua ciò che dovrebbe in base ai requisiti. È anche importante renderlo il più indipendente possibile riducendo i riferimenti all'interfaccia utente, al livello dati, alle funzioni native e alle chiamate API ecc. Tutto ciò rende la tua VM testabile.
In breve, il tuo ViewModel:
- Non dovrebbe dipendere da classi di UI (viste, pagine, stili, eventi);
- Non usare dati statici di altre classi (il più possibile);
- Dovrebbe implementare la logica di business e preparare i dati da inserire nell'interfaccia utente;
- Dovrebbe utilizzare altri componenti (database, HTTP, specifici dell'interfaccia utente) tramite le interfacce che vengono risolte utilizzando Iniezione delle dipendenze.
Il tuo ViewModel potrebbe avere anche le proprietà di altri tipi di VM. Ad esempio
ContactsPageViewModel
avrà propery del tipo di raccolta comeObservableCollection<ContactListItemViewModel>
Requisiti aziendali
Diciamo che abbiamo la seguente funzionalità da implementare:
As an unauthorized user
I want to log into the app
So that I will access the authorized features
Dopo aver chiarito la user story, abbiamo definito i seguenti scenari:
Scenario: trying to log in with valid non-empty creds
Given the user is on Login screen
When the user enters 'user' as username
And the user enters 'pass' as password
And the user taps the Login button
Then the app shows the loading indicator
And the app makes an API call for authentication
Scenario: trying to log in empty username
Given the user is on Login screen
When the user enters ' ' as username
And the user enters 'pass' as password
And the user taps the Login button
Then the app shows an error message saying 'Please, enter correct username and password'
And the app doesn't make an API call for authentication
Resteremo solo con questi due scenari. Ovviamente, ci dovrebbero essere molti più casi e dovresti definirli tutti prima della vera codifica, ma è abbastanza per noi ora avere familiarità con i test unitari dei modelli di vista.
Seguiamo il classico approccio TDD e iniziamo con la scrittura di una classe vuota sottoposta a test. Quindi scriveremo dei test e li renderemo verdi implementando la funzionalità aziendale.
Classi comuni
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Servizi
Ti ricordi che il nostro modello di visualizzazione non deve utilizzare direttamente le classi UI e HTTP? Dovresti invece definirli come astrazioni e non dipendere dai dettagli di implementazione .
/// <summary>
/// Provides authentication functionality.
/// </summary>
public interface IAuthenticationService
{
/// <summary>
/// Tries to authenticate the user with the given credentials.
/// </summary>
/// <param name="userName">UserName</param>
/// <param name="password">User's password</param>
/// <returns>true if the user has been successfully authenticated</returns>
Task<bool> Login(string userName, string password);
}
/// <summary>
/// UI-specific service providing abilities to show alert messages.
/// </summary>
public interface IAlertService
{
/// <summary>
/// Show an alert message to the user.
/// </summary>
/// <param name="title">Alert message title</param>
/// <param name="message">Alert message text</param>
Task ShowAlert(string title, string message);
}
Costruire lo stub ViewModel
Ok, avremo la classe di pagina per la schermata di accesso, ma iniziamo con ViewModel prima:
public class LoginPageViewModel : BaseViewModel
{
private readonly IAuthenticationService authenticationService;
private readonly IAlertService alertService;
private string userName;
private string password;
private bool isLoading;
private ICommand loginCommand;
public LoginPageViewModel(IAuthenticationService authenticationService, IAlertService alertService)
{
this.authenticationService = authenticationService;
this.alertService = alertService;
}
public string UserName
{
get
{
return userName;
}
set
{
if (userName!= value)
{
userName= value;
OnPropertyChanged();
}
}
}
public string Password
{
get
{
return password;
}
set
{
if (password != value)
{
password = value;
OnPropertyChanged();
}
}
}
public bool IsLoading
{
get
{
return isLoading;
}
set
{
if (isLoading != value)
{
isLoading = value;
OnPropertyChanged();
}
}
}
public ICommand LoginCommand => loginCommand ?? (loginCommand = new Command(Login));
private void Login()
{
authenticationService.Login(UserName, Password);
}
}
Abbiamo definito due proprietà string
e un comando da associare all'interfaccia utente. Non descriveremo come costruire una classe di pagina, markup XAML e associare ViewModel a questo argomento in quanto non hanno nulla di specifico.
Come creare un'istanza LoginPageViewModel?
Penso che probabilmente stavi creando le macchine virtuali solo con il costruttore. Ora come puoi vedere la nostra VM dipende da 2 servizi che vengono iniettati come parametri del costruttore quindi non puoi fare solo var viewModel = new LoginPageViewModel()
. Se non hai familiarità con Dependency Injection è il momento migliore per conoscerlo. Un corretto test unitario è impossibile senza conoscere e seguire questo principio.
test
Ora scriviamo alcuni test in base ai casi d'uso sopra elencati. Prima di tutto è necessario creare un nuovo assembly (solo una libreria di classi o selezionare un progetto di test speciale se si desidera utilizzare gli strumenti di test dell'unità Microsoft). ProjectName.Tests
nome a ProjectName.Tests
e aggiungi un riferimento al tuo progetto PCL originale.
In questo esempio userò NUnit e Moq ma puoi continuare con qualsiasi lib di prova a tua scelta. Non ci sarà nulla di speciale con loro.
Ok, questa è la classe di test:
[TestFixture]
public class LoginPageViewModelTest
{
}
Test di scrittura
Ecco i metodi di prova per i primi due scenari. Prova a mantenere 1 metodo di prova per 1 risultato previsto e non a controllare tutto in un test. Ciò ti aiuterà a ricevere rapporti più chiari su ciò che è fallito nel codice.
[TestFixture]
public class LoginPageViewModelTest
{
private readonly Mock<IAuthenticationService> authenticationServiceMock =
new Mock<IAuthenticationService>();
private readonly Mock<IAlertService> alertServiceMock =
new Mock<IAlertService>();
[TestCase("user", "pass")]
public void LogInWithValidCreds_LoadingIndicatorShown(string userName, string password)
{
LoginPageViewModel model = CreateViewModelAndLogin(userName, password);
Assert.IsTrue(model.IsLoading);
}
[TestCase("user", "pass")]
public void LogInWithValidCreds_AuthenticationRequested(string userName, string password)
{
CreateViewModelAndLogin(userName, password);
authenticationServiceMock.Verify(x => x.Login(userName, password), Times.Once);
}
[TestCase("", "pass")]
[TestCase(" ", "pass")]
[TestCase(null, "pass")]
public void LogInWithEmptyuserName_AuthenticationNotRequested(string userName, string password)
{
CreateViewModelAndLogin(userName, password);
authenticationServiceMock.Verify(x => x.Login(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
[TestCase("", "pass", "Please, enter correct username and password")]
[TestCase(" ", "pass", "Please, enter correct username and password")]
[TestCase(null, "pass", "Please, enter correct username and password")]
public void LogInWithEmptyUserName_AlertMessageShown(string userName, string password, string message)
{
CreateViewModelAndLogin(userName, password);
alertServiceMock.Verify(x => x.ShowAlert(It.IsAny<string>(), message));
}
private LoginPageViewModel CreateViewModelAndLogin(string userName, string password)
{
var model = new LoginPageViewModel(
authenticationServiceMock.Object,
alertServiceMock.Object);
model.UserName = userName;
model.Password = password;
model.LoginCommand.Execute(null);
return model;
}
}
E qui andiamo:
Ora l'obiettivo è scrivere un'implementazione corretta per il metodo di Login
di ViewModel e il gioco è fatto.
Implementazione della logica aziendale
private async void Login()
{
if (String.IsNullOrWhiteSpace(UserName) || String.IsNullOrWhiteSpace(Password))
{
await alertService.ShowAlert("Warning", "Please, enter correct username and password");
}
else
{
IsLoading = true;
bool isAuthenticated = await authenticationService.Login(UserName, Password);
}
}
E dopo aver eseguito nuovamente i test:
Ora puoi continuare a coprire il tuo codice con nuovi test rendendolo più stabile e sicuro per la regressione.