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 wie ObservableCollection<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:

Geben Sie hier die Bildbeschreibung ein

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:

Geben Sie hier die Bildbeschreibung ein

Jetzt können Sie Ihren Code weiterhin mit neuen Tests abdecken, um ihn stabiler und regressionssicherer zu machen.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow