Recherche…


Test des modèles de vue

Avant de commencer...

En termes de couches d'application, votre ViewModel est une classe contenant toute la logique métier et les règles permettant à l'application de faire ce qu'elle doit selon les besoins. Il est également important de le rendre le plus indépendant possible, en réduisant les références à l'interface utilisateur, à la couche de données, aux fonctionnalités natives et aux appels d'API, etc. Tous ces éléments permettent de tester votre machine virtuelle.
En bref, votre ViewModel:

  • Ne devrait pas dépendre des classes de l'interface utilisateur (vues, pages, styles, événements);
  • Ne devrait pas utiliser les données statiques d'une autre classe (autant que vous le pouvez);
  • Doit implémenter la logique métier et préparer les données sur l'interface utilisateur;
  • Devrait utiliser d'autres composants (base de données, HTTP, spécifique à l'interface utilisateur) via des interfaces en cours de résolution à l'aide de l'injection de dépendances.

Votre ViewModel peut également avoir les propriétés d'un autre type de machine virtuelle. Par exemple, ContactsPageViewModel aura la propriété du type de collection comme ObservableCollection<ContactListItemViewModel>

Besoins de l'entreprise

Disons que nous avons les fonctionnalités suivantes à implémenter:

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

Après avoir clarifié la user story, nous avons défini les scénarios suivants:

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

Nous resterons avec seulement ces deux scénarios. Bien sûr, il devrait y avoir beaucoup plus de cas et vous devriez tous les définir avant le codage réel, mais il est assez suffisant pour nous familiariser maintenant avec les tests unitaires des modèles de vue.

Suivons l'approche TDD classique et commençons par écrire une classe vide en cours de test. Ensuite, nous écrirons des tests et les rendrons verts en implémentant la fonctionnalité métier.

Cours communs

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

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

Prestations de service

Vous souvenez-vous que notre modèle de vue ne doit pas utiliser directement les classes UI et HTTP? Vous devez les définir comme des abstractions à la place et ne pas dépendre des détails de l'implémentation .

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

Construire le talon ViewModel

Ok, nous allons avoir la classe de page pour l'écran de connexion, mais commençons par 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);
    }
}

Nous avons défini deux propriétés de string et une commande à lier sur l'interface utilisateur. Nous ne décrirons pas comment créer une classe de page, un balisage XAML et lier ViewModel à cette rubrique car ils n'ont rien de spécifique.

Comment créer une instance LoginPageViewModel?

Je pense que vous créiez probablement les VM uniquement avec constructeur. Maintenant, comme vous pouvez le voir, notre machine virtuelle dépend de 2 services injectés en tant que paramètres de constructeur, alors vous ne pouvez pas simplement faire var viewModel = new LoginPageViewModel() . Si vous n'êtes pas familier avec l' injection de dépendance, c'est le meilleur moment pour en apprendre davantage. Des tests unitaires appropriés sont impossibles sans connaître et suivre ce principe.

Des tests

Maintenant, écrivons quelques tests en fonction des cas d’utilisation listés ci-dessus. Tout d'abord, vous devez créer un nouvel assembly (juste une bibliothèque de classes ou sélectionner un projet de test spécial si vous souhaitez utiliser les outils de test unitaire Microsoft). Nommez-le quelque chose comme ProjectName.Tests et ajoutez une référence à votre projet PCL original.

Dans cet exemple, je vais utiliser NUnit et Moq, mais vous pouvez continuer avec tous les tests de votre choix. Il n'y aura rien de spécial avec eux.

Ok, c'est la classe de test:

[TestFixture]
public class LoginPageViewModelTest
{
}

Tests d'écriture

Voici les méthodes de test pour les deux premiers scénarios. Essayez de conserver 1 méthode de test par 1 résultat attendu et de ne pas tout vérifier en un seul test. Cela vous aidera à recevoir des rapports plus clairs sur ce qui a échoué dans le code.

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

Et c'est reparti:

entrer la description de l'image ici

Maintenant, l'objectif est d'écrire une implémentation correcte pour la méthode de Login de ViewModel et c'est tout.

Implémentation de la logique métier

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

Et après avoir exécuté les tests à nouveau:

entrer la description de l'image ici

Maintenant, vous pouvez continuer à couvrir votre code avec de nouveaux tests le rendant plus stable et sans risque de régression.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow