Szukaj…


Testowanie modeli widoku

Zanim zaczniemy...

Pod względem warstw aplikacji ViewModel jest klasą zawierającą całą logikę biznesową i reguły, dzięki którym aplikacja robi to, co powinna, zgodnie z wymaganiami. Ważne jest również, aby uczynić go tak niezależnym, jak to możliwe, redukując odniesienia do interfejsu użytkownika, warstwy danych, funkcji natywnych i wywołań API itp. Wszystko to sprawia, że twoja maszyna wirtualna jest testowalna.
Krótko mówiąc, Twój ViewModel:

  • Nie powinien zależeć od klas interfejsu użytkownika (widoki, strony, style, zdarzenia);
  • Nie należy używać danych statycznych innych klas (tak dużo, jak to możliwe);
  • Powinien wdrożyć logikę biznesową i przygotować dane, które powinny być w interfejsie użytkownika;
  • Powinien używać innych komponentów (bazy danych, HTTP, interfejsu użytkownika) za pośrednictwem interfejsów rozwiązywanych za pomocą wstrzykiwania zależności.

Twój ViewModel może mieć także właściwości innych typów maszyn wirtualnych. Na przykład ContactsPageViewModel będzie miał właściwość typu kolekcji, taką jak ObservableCollection<ContactListItemViewModel>

Wymagania biznesowe

Załóżmy, że mamy do wdrożenia następującą funkcjonalność:

As an unauthorized user
I want to log into the app
So that I will access the authorized features

Po wyjaśnieniu historii użytkownika zdefiniowaliśmy następujące scenariusze:

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

Pozostaniemy tylko z tymi dwoma scenariuszami. Oczywiście, powinno być znacznie więcej przypadków i powinieneś zdefiniować je wszystkie przed faktycznym kodowaniem, ale teraz wystarczy nam zapoznać się z testowaniem jednostkowym modeli widoku.

Postępujmy zgodnie z klasycznym podejściem TDD i zacznijmy od napisania testowanej pustej klasy. Następnie napiszemy testy i nadamy im zielony charakter poprzez wdrożenie funkcjonalności biznesowej.

Wspólne klasy

public abstract class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Usługi

Czy pamiętasz, że nasz model widoku nie może bezpośrednio wykorzystywać klas interfejsu użytkownika i klas HTTP? Zamiast tego powinieneś zdefiniować je jako abstrakcje i nie polegać na szczegółach implementacji .

/// <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);
}

Budowanie kodu pośredniczącego ViewModel

Ok, będziemy mieć klasę strony dla ekranu logowania, ale zacznijmy od 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);
    }
}

Zdefiniowaliśmy dwie właściwości string i polecenie do powiązania z interfejsem użytkownika. W tym temacie nie opiszemy, jak zbudować klasę strony, znaczniki XAML i powiązać z nią ViewModel, ponieważ nie mają one nic konkretnego.

Jak utworzyć instancję LoginPageViewModel?

Myślę, że prawdopodobnie tworzyłeś maszyny wirtualne tylko za pomocą konstruktora. Teraz, jak widać, nasza maszyna wirtualna zależy od wstrzyknięcia 2 usług jako parametrów konstruktora, więc nie można po prostu zrobić var viewModel = new LoginPageViewModel() . Jeśli nie jesteś zaznajomiony z Dependency Injection, to najlepszy moment, aby się o tym dowiedzieć. Właściwe testowanie jednostkowe jest niemożliwe bez znajomości i przestrzegania tej zasady.

Testy

Teraz napiszmy kilka testów zgodnie z powyższymi przypadkami użycia. Przede wszystkim musisz utworzyć nowy zestaw (tylko bibliotekę klas lub wybrać specjalny projekt testowy, jeśli chcesz korzystać z narzędzi do testowania jednostkowego Microsoft). Nazwij to coś jak ProjectName.Tests i dodaj odniesienie do oryginalnego projektu PCL.

W tym przykładzie użyję NUnit i Moq, ale możesz kontynuować dowolne liby testowe według własnego wyboru. Nie będzie z nimi nic specjalnego.

Ok, to klasa testowa:

[TestFixture]
public class LoginPageViewModelTest
{
}

Testy pisemne

Oto metody testowe dla pierwszych dwóch scenariuszy. Spróbuj zachować 1 metodę testową na 1 oczekiwany wynik i nie sprawdzaj wszystkiego w jednym teście. Pomoże Ci to otrzymywać wyraźniejsze raporty o tym, co nie działa w kodzie.

[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;
    }
}

I zaczynamy:

wprowadź opis zdjęcia tutaj

Teraz celem jest napisanie poprawnej implementacji metody Login ViewModel i to wszystko.

Implementacja logiki biznesowej

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);
    }
}

A po ponownym uruchomieniu testów:

wprowadź opis zdjęcia tutaj

Teraz możesz nadal pokrywać swój kod nowymi testami, dzięki czemu jest bardziej stabilny i bezpieczny dla regresji.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow