Xamarin.Forms
Enhetstestning
Sök…
Testa visningsmodellerna
Innan vi börjar ...
När det gäller applikationslager är din ViewModel en klass som innehåller all affärslogik och regler som gör att appen gör vad den ska enligt kraven. Det är också viktigt att göra det så mycket oberoende som möjligt och minska referenser till användargränssnitt, datalager, inbyggda funktioner och API-samtal etc. Allt detta gör att din VM kan testas.
Kort sagt, din ViewModel:
- Bör inte bero på UI-klasser (vyer, sidor, stilar, händelser);
- Bör inte använda statiska data från andra klasser (så mycket du kan);
- Bör implementera affärslogiken och förbereda data som ska vara i UI;
- Bör använda andra komponenter (databas, HTTP, UI-specifikt) via gränssnitt som löses med Dependency Injection.
Din ViewModel kan också ha egenskaper för andra VM-typer. Exempel:
ContactsPageViewModel
har lämplig samlingstyp somObservableCollection<ContactListItemViewModel>
Affärskrav
Låt oss säga att vi har följande funktionalitet att implementera:
As an unauthorized user
I want to log into the app
So that I will access the authorized features
Efter att ha klargjort användarhistorien definierade vi följande scenarier:
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
Vi kommer att stanna kvar med bara dessa två scenarier. Naturligtvis borde det finnas mycket fler fall och du bör definiera dem alla innan den faktiska kodningen, men det är ganska nog för oss nu att bekanta oss med enhetstestning av visningsmodeller.
Låt oss följa den klassiska TDD-metoden och börja med att skriva en tom klass som testas. Då skriver vi tester och gör dem gröna genom att implementera affärsfunktionaliteten.
Vanliga klasser
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
tjänster
Kommer du ihåg att vår visningsmodell inte får använda UI- och HTTP-klasser direkt? Du bör definiera dem som abstraktioner istället och inte vara beroende av implementeringsdetaljer .
/// <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);
}
Bygg upp ViewModel-stubben
Okej, vi ska ha sidklassen för inloggningsskärmen, men låt oss börja med ViewModel först:
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);
}
}
Vi definierade två string
och ett kommando som ska vara bundet till UI. Vi kommer inte att beskriva hur man bygger en sidklass, XAML-markering och binder ViewModel till det i det här ämnet eftersom de inte har något specifikt.
Hur skapar jag en LoginPageViewModel-instans?
Jag tror att du förmodligen skapade VM: erna bara med konstruktören. Nu som du kan se vår VM beror på att två tjänster injiceras som konstruktörsparametrar så kan inte bara göra var viewModel = new LoginPageViewModel()
. Om du inte känner till Dependency Injection är det det bästa ögonblicket att lära sig om det. Korrekt enhetstestning är omöjligt utan att veta och följa denna princip.
tester
Låt oss nu skriva några tester enligt de fall som anges ovan. Först måste du skapa en ny enhet (bara ett klassbibliotek eller välja ett speciellt testprojekt om du vill använda Microsoft-enhetstestverktyg). Namnge det som ProjectName.Tests
och lägg till referens till ditt ursprungliga PCL-projekt.
I det här exemplet ska jag använda NUnit och Moq men du kan fortsätta med alla testbibliotek som du väljer. Det blir inget särskilt med dem.
Ok, det är testklassen:
[TestFixture]
public class LoginPageViewModelTest
{
}
Skrivprov
Här är testmetoderna för de två första scenarierna. Försök att hålla en testmetod per 1 förväntat resultat och inte kontrollera allt i ett test. Det hjälper dig att få tydligare rapporter om vad som har misslyckats i koden.
[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;
}
}
Och här går vi:
Nu är målet att skriva korrekt implementering för ViewModels Login
och det är det.
Genomförande av affärslogik
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);
}
}
Och efter att ha kört testen igen:
Nu kan du fortsätta att täcka din kod med nya tester som gör den mer stabil och regressionssäker.