Xamarin.Forms
Unit Testing
Suche…
Testen der Ansichtsmodelle
Bevor wir anfangen...
In Bezug auf die Anwendungsebenen ist ViewModel eine Klasse, die alle Geschäftslogik und Regeln enthält, sodass die App den Anforderungen gemäß den Anforderungen entspricht. Es ist auch wichtig, es so unabhängig wie möglich zu machen, indem die Verweise auf Benutzeroberfläche, Datenschicht, native Funktionen und API-Aufrufe usw. reduziert werden. All dies macht Ihre VM testbar.
Kurz gesagt, Ihr ViewModel:
- Sollte nicht von UI-Klassen abhängen (Ansichten, Seiten, Stile, Ereignisse);
- Verwenden Sie keine statischen Daten einer anderen Klasse (so viel wie möglich).
- Sollte die Geschäftslogik implementieren und die Daten auf der Benutzeroberfläche vorbereiten;
- Sollte andere Komponenten (Datenbank, HTTP, UI-spezifisch) über Schnittstellen verwenden, die mithilfe von Dependency Injection aufgelöst werden.
Ihr ViewModel verfügt möglicherweise auch über Eigenschaften anderer VMs-Typen. Für
ContactsPageViewModel
gibt es beispielsweise einen Collection-Typ wieObservableCollection<ContactListItemViewModel>
Geschäftsanforderungen
Angenommen, wir haben die folgende Funktionalität, die implementiert werden muss:
As an unauthorized user
I want to log into the app
So that I will access the authorized features
Nach der Klärung der User Story haben wir folgende Szenarien definiert:
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
Wir werden nur bei diesen beiden Szenarien bleiben. Natürlich sollte es viel mehr Fälle geben, und Sie sollten alle vor der eigentlichen Codierung definieren, aber es ist für uns jetzt schon genug, um uns mit dem Komponententest von Ansichtsmodellen vertraut zu machen.
Folgen wir dem klassischen TDD-Ansatz und beginnen Sie mit dem Schreiben einer leeren Klasse, die getestet wird. Dann schreiben wir Tests und machen sie grün, indem wir die Business-Funktionalität implementieren.
Gemeinsame Klassen
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Dienstleistungen
Erinnern Sie sich, dass unser Ansichtsmodell UI- und HTTP-Klassen nicht direkt verwenden darf? Sie sollten sie stattdessen als Abstraktionen definieren und sich nicht auf Implementierungsdetails verlassen .
/// <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);
}
Erstellen des ViewModel-Stubs
Ok, wir haben die Seitenklasse für den Anmeldebildschirm, aber beginnen wir zuerst mit ViewModel:
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);
}
}
Wir haben zwei string
Eigenschaften und einen Befehl definiert, der an die Benutzeroberfläche gebunden werden soll. Es wird nicht beschrieben, wie Sie eine Seitenklasse, ein XAML-Markup erstellen und ViewModel in diesem Thema daran binden, da sie nichts Spezifisches enthalten.
Wie erstelle ich eine LoginPageViewModel-Instanz?
Ich denke, Sie haben die VMs wahrscheinlich nur mit Konstruktor erstellt. Wie Sie sehen, ist unsere VM nun darauf angewiesen, dass 2 Services als Konstruktorparameter var viewModel = new LoginPageViewModel()
kann var viewModel = new LoginPageViewModel()
nicht einfach verwendet werden. Wenn Sie mit Dependency Injection nicht vertraut sind, ist es der beste Moment, um etwas darüber zu erfahren. Eine ordnungsgemäße Prüfung der Einheiten ist nicht möglich, ohne diesen Grundsatz zu kennen und zu befolgen.
Tests
Jetzt schreiben wir einige Tests gemäß den oben aufgeführten Anwendungsfällen. Zunächst müssen Sie eine neue Assembly erstellen (nur eine Klassenbibliothek oder ein spezielles Testprojekt auswählen, wenn Sie Microsoft-Komponententools verwenden möchten). Nennen Sie es etwas wie ProjectName.Tests
und fügen Sie Ihrem ursprünglichen PCL-Projekt einen Verweis hinzu.
In diesem Beispiel werde ich NUnit und Moq verwenden, aber Sie können mit allen Testlibs Ihrer Wahl fortfahren. Es wird nichts Besonderes mit ihnen geben.
Ok, das ist die Testklasse:
[TestFixture]
public class LoginPageViewModelTest
{
}
Tests schreiben
Hier sind die Testmethoden für die ersten beiden Szenarien. Versuchen Sie, 1 Testmethode pro 1 erwartetem Ergebnis beizubehalten und nicht alles in einem Test zu überprüfen. Dies hilft Ihnen, klarere Berichte darüber zu erhalten, was im Code fehlgeschlagen ist.
[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;
}
}
Und es geht los:
Nun ist es das Ziel, die korrekte Implementierung für die Login
Methode von ViewModel zu schreiben, und das war's.
Implementierung der Geschäftslogik
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);
}
}
Und nachdem die Tests erneut ausgeführt wurden:
Jetzt können Sie Ihren Code weiterhin mit neuen Tests abdecken, um ihn stabiler und regressionssicherer zu machen.