Buscar..


Probando los modelos de vista.

Antes que empecemos...

En términos de capas de aplicación, su ViewModel es una clase que contiene toda la lógica empresarial y las reglas que hacen que la aplicación haga lo que debe de acuerdo con los requisitos. También es importante hacerlo lo más independiente posible, reduciendo las referencias a la IU, la capa de datos, las funciones nativas y las llamadas a la API, etc. Todo esto hace que su máquina virtual sea verificable.
En resumen, su ViewModel:

  • No debe depender de las clases de UI (vistas, páginas, estilos, eventos);
  • No debe usar datos estáticos de otras clases (tanto como pueda);
  • Debe implementar la lógica de negocios y preparar los datos que se deben en la interfaz de usuario;
  • Debe utilizar otros componentes (base de datos, HTTP, específicos de la interfaz de usuario) a través de interfaces que se resuelven utilizando la inyección de dependencia.

Su ViewModel también puede tener propiedades de otros tipos de máquinas virtuales. Por ejemplo, ContactsPageViewModel tendrá una propiedad de tipo de colección como ObservableCollection<ContactListItemViewModel>

Requisitos de negocio

Digamos que tenemos la siguiente funcionalidad para implementar:

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

Después de aclarar la historia del usuario definimos los siguientes escenarios:

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

Nos quedaremos solo con estos dos escenarios. Por supuesto, debería haber muchos más casos y debería definirlos todos antes de la codificación real, pero ahora es suficiente para que nos familiaricemos con las pruebas unitarias de los modelos de vista.

Sigamos el enfoque clásico de TDD y comencemos escribiendo una clase vacía que se está probando. Luego, escribiremos pruebas y las haremos verdes implementando la funcionalidad empresarial.

Clases comunes

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

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

Servicios

¿Recuerda que nuestro modelo de vista no debe utilizar las clases de UI y HTTP directamente? Debe definirlos como abstracciones en lugar de depender de los detalles de la implementación .

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

Construyendo el trozo de ViewModel

Ok, vamos a tener la clase de página para la pantalla de inicio de sesión, pero comencemos con ViewModel primero:

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

Definimos dos propiedades de string y un comando para enlazar en la interfaz de usuario. No describiremos cómo construir una clase de página, el marcado XAML y vincularlo con ViewModel en este tema, ya que no tienen nada específico.

¿Cómo crear una instancia LoginPageViewModel?

Creo que probablemente estabas creando las máquinas virtuales solo con el constructor. Ahora, como puede ver, nuestra máquina virtual depende de que se inyecten 2 servicios como parámetros de constructor, por lo que no podemos simplemente hacer var viewModel = new LoginPageViewModel() . Si no está familiarizado con la inyección de dependencia , es el mejor momento para aprender sobre él. La prueba de unidad adecuada es imposible sin conocer y seguir este principio.

Pruebas

Ahora vamos a escribir algunas pruebas de acuerdo con los casos de uso mencionados anteriormente. En primer lugar, debe crear un nuevo conjunto (solo una biblioteca de clases o seleccionar un proyecto de prueba especial si desea usar las herramientas de prueba de unidad de Microsoft). Asígnele un nombre similar a ProjectName.Tests y agregue una referencia a su proyecto PCL original.

En este ejemplo voy a usar NUnit y Moq, pero puede continuar con las libs de prueba que desee. No habrá nada especial con ellos.

Ok, esa es la clase de prueba:

[TestFixture]
public class LoginPageViewModelTest
{
}

Pruebas de escritura

Aquí están los métodos de prueba para los dos primeros escenarios. Intente mantener 1 método de prueba por 1 resultado esperado y no verifique todo en una prueba. Eso le ayudará a recibir informes más claros sobre lo que ha fallado en el código.

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

Y aquí vamos:

introduzca la descripción de la imagen aquí

Ahora el objetivo es escribir la implementación correcta para el método de Login de Login de ViewModel y eso es todo.

Implementación de la lógica de negocios.

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

Y después de ejecutar las pruebas de nuevo:

introduzca la descripción de la imagen aquí

Ahora puede seguir cubriendo su código con nuevas pruebas, lo que lo hace más estable y seguro para la regresión.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow