Compartir a través de


Pruebas unitarias

Sugerencia

Este contenido es un extracto del libro electrónico "Patrones de aplicaciones empresariales con .NET MAUI", disponible en Documentación de .NET o como un PDF descargable y gratuito que se puede leer sin conexión.

Miniatura de la portada del libro electrónico

Las aplicaciones multiplataforma experimentan problemas similares a las aplicaciones basadas en escritorio y web. Los usuarios móviles variarán en función de sus dispositivos, conectividad de red, disponibilidad de servicios y otros diversos factores. Por lo tanto, las aplicaciones multiplataforma se deben probar tal y como se usarían en el mundo real para mejorar su calidad, confiabilidad y rendimiento. Se deben realizar muchos tipos de pruebas en una aplicación, incluidas las pruebas unitarias, las pruebas de integración y las pruebas de interfaz de usuario. Las pruebas unitarias son la forma más común y esencial para crear aplicaciones de alta calidad.

Una prueba unitaria toma una pequeña unidad de la aplicación, normalmente un método, lo aísla del resto del código y comprueba que se comporta según lo previsto. Su objetivo es comprobar que cada unidad de funcionalidad funciona según lo previsto, para que los errores no se propaguen en toda la aplicación. Detectar un error allí donde se produce es más eficaz que observar el efecto de un error indirectamente en un punto secundario de error.

Las pruebas unitarias tienen un mayor efecto en la calidad del código cuando son parte integral del flujo de trabajo de desarrollo de software. Las pruebas unitarias pueden actuar como documentación de diseño y especificaciones funcionales para una aplicación. En cuanto se haya escrito un método, se deben escribir pruebas unitarias que comprueben el comportamiento del método en respuesta a casos de datos de entrada estándar, límites e incorrectos, y que comprueben las suposiciones explícitas o implícitas realizadas por el código. Como alternativa, con el desarrollo controlado por pruebas, las pruebas unitarias se escriben antes que el código. Para obtener más información sobre el desarrollo controlado por pruebas y cómo implementarlo, consulte Tutorial: Desarrollo controlado por pruebas mediante el Explorador de pruebas..

Nota

Las pruebas unitarias son muy eficaces frente a la regresión. Es decir, una funcionalidad que solía funcionar, pero que ha sido perturbada por una actualización incorrecta.

Las pruebas unitarias suelen usar el patrón organizar-actuar-afirmar:

Paso Descripción
Organizar Inicializa los objetos y establece el valor de los datos que se pasan al método objeto de las pruebas.
Actuar Invoca al método objeto de las pruebas con los argumentos necesarios.
Assert Comprueba si la acción del método objeto de las pruebas se comporta de la forma prevista.

Este patrón garantiza que las pruebas unitarias sean legibles, autodescriptivas y coherentes.

Inserción de dependencias y pruebas unitarias

Una de las motivaciones para adoptar una arquitectura de acoplamiento flexible es que facilita las pruebas unitarias. Uno de los tipos registrados con el servicio de inserción de dependencias es la interfaz IAppEnvironmentService. En el ejemplo de código siguiente, se muestra un esquema de esta clase:

public class OrderDetailViewModel : ViewModelBase
{
    private IAppEnvironmentService _appEnvironmentService;

    public OrderDetailViewModel(
        IAppEnvironmentService appEnvironmentService,
        IDialogService dialogService, INavigationService navigationService, ISettingsService settingsService)
        : base(dialogService, navigationService, settingsService)
    {
        _appEnvironmentService = appEnvironmentService;
    }
}

La clase OrderDetailViewModel tiene una dependencia del tipo IAppEnvironmentService, que el contenedor de inserción de dependencias resuelve cuando crea una instancia de un objeto OrderDetailViewModel. Sin embargo, en lugar de crear un objeto IAppEnvironmentService que use servidores, dispositivos y configuraciones reales para hacer las pruebas unitarias de la clase OrderDetailViewModel, reemplaza el objeto IAppEnvironmentService por un objeto ficticio con fines de prueba. Un objeto ficticio es uno que tiene la misma firma que un objeto o una interfaz, pero que se crea de forma específica para ayudar con las pruebas unitarias. Se usa a menudo con la inserción de dependencias para proporcionar implementaciones específicas de interfaces para probar diferentes escenarios de datos y flujos de trabajo.

Este enfoque permite pasar el objeto IAppEnvironmentService a la clase OrderDetailViewModel en tiempo de ejecución y, en interés de la capacidad de prueba, permite pasar una clase ficticia a la clase OrderDetailViewModel en tiempo de pruebas. La principal ventaja de este enfoque es que permite ejecutar pruebas unitarias sin necesidad de recursos pesados, como las características de la plataforma en tiempo de ejecución, los servicios web o las bases de datos.

Prueba de aplicaciones MVVM

Probar modelos y ver modelos de aplicaciones MVVM es idéntico a probar cualquier otra clase y se usan las mismas herramientas y técnicas; esto incluye características como las pruebas unitarias y la simulación. Sin embargo, algunos patrones típicos para modelar y ver las clases del modelo se pueden beneficiar de técnicas específicas de las pruebas unitarias.

Sugerencia

Pruebe una cosa con cada prueba unitaria. A medida que se expande la complejidad de una prueba, hace que la comprobación de esa prueba sea más difícil. Al limitar una prueba unitaria a un único problema, podemos garantizar que nuestras pruebas sean más repetibles, aisladas y tengan un tiempo de ejecución menor. Consulte Procedimientos recomendados para pruebas unitarias con .NET para conocer más procedimientos recomendados.

No se sienta tentado a hacer que prueba unitaria ejercite más de un aspecto del comportamiento de la unidad. Si lo hace, las pruebas son difíciles de leer y actualizar. También puede provocar confusión al interpretar un error.

La aplicación multiplataforma eShop usa MSTest para realizar las pruebas unitarias, que admite dos tipos diferentes de pruebas unitarias:

Tipo de prueba Atributo Descripción
TestMethod TestMethod Define el método de prueba real que se va a ejecutar.
DataSource DataSource Pruebas que solo son verdaderas para un conjunto determinado de datos.

Las pruebas unitarias incluidas con la aplicación multiplataforma eShop son TestMethod, por lo que cada método de la prueba unitaria está decorado con el atributo TestMethod. Además de MSTest hay otros marcos de pruebas disponibles, como NUnit y xUnit.

Prueba de la funcionalidad asincrónica

Al implementar el patrón MVVM, los modelos de vista suelen invocar operaciones en servicios, a menudo de forma asincrónica. Las pruebas del código que invoca estas operaciones suelen usar simulaciones como sustitución de los servicios reales. En el ejemplo de código siguiente, se muestra cómo probar la funcionalidad asincrónica pasando un servicio ficticio a un modelo de vista:

[TestMethod]
public async Task OrderPropertyIsNotNullAfterViewModelInitializationTest()
{
    // Arrange
    var orderService = new OrderMockService();
    var orderViewModel = new OrderDetailViewModel(orderService);

    // Act
    var order = await orderService.GetOrderAsync(1, GlobalSetting.Instance.AuthToken);
    await orderViewModel.InitializeAsync(order);

    // Assert
    Assert.IsNotNull(orderViewModel.Order);
}

Esta prueba unitaria comprueba que la propiedad Order de la instancia de OrderDetailViewModel tenga un valor después de invocar al método InitializeAsync. Se invoca al método InitializeAsync cuando se navega a la vista correspondiente del modelo de vista. Para más información sobre la navegación, consulte Navegación.

Cuando se crea la instancia de OrderDetailViewModel, se espera que se especifique una instancia de IOrderService como argumento. Sin embargo, OrderService recupera datos de un servicio web. Por lo tanto, se especifica una instancia de OrderMockService, una versión ficticia de la clase OrderService, como argumento para el constructor OrderDetailViewModel. A continuación, se recuperan datos ficticios en lugar de comunicarse con un servicio web cuando se invoca al método InitializeAsync del modelo de vista, que usa operaciones de IOrderService.

Prueba de implementaciones de INotifyPropertyChanged

La implementación de la interfaz INotifyPropertyChanged permite que las vistas reaccionen ante los cambios que se originan en los modelos de vista y los modelos. Estos cambios no se limitan a los datos que se muestran en los controles; también se usan para controlar la vista, como los estados del modelo de vista que hacen que se inicien las animaciones o se deshabiliten los controles.

Las propiedades que se pueden actualizar directamente mediante la prueba unitaria se pueden probar adjuntando un controlador de eventos al evento PropertyChanged y comprobando si se genera el evento después de establecer un nuevo valor para la propiedad. En el ejemplo de código siguiente, se muestra una prueba de este tipo:

[TestMethod]
public async Task SettingOrderPropertyShouldRaisePropertyChanged()
{
    var invoked = false;
    var orderService = new OrderMockService();
    var orderViewModel = new OrderDetailViewModel(orderService);

    orderViewModel.PropertyChanged += (sender, e) =>
    {
        if (e.PropertyName.Equals("Order"))
            invoked = true;
    };
    var order = await orderService.GetOrderAsync(1, GlobalSetting.Instance.AuthToken);
    await orderViewModel.InitializeAsync(order);

    Assert.IsTrue(invoked);
}

Esta prueba unitaria invoca al método InitializeAsync de la clase OrderViewModel, lo que hace que se actualice su propiedad Order. Se superará la prueba unitaria, siempre que se genere el evento PropertyChanged para la propiedad Order.

Prueba de la comunicación basada en mensajes

Los modelos de vista que usan la clase MessagingCenter para comunicarse entre clases de acoplamiento flexible se pueden probar de forma unitaria mediante la suscripción al mensaje enviado por el código objeto de las pruebas, como se muestra en el ejemplo de código siguiente:

[TestMethod]
public void AddCatalogItemCommandSendsAddProductMessageTest()
{
    var messageReceived = false;
    var catalogService = new CatalogMockService();
    var catalogViewModel = new CatalogViewModel(catalogService);

    MessagingCenter.Subscribe<CatalogViewModel, CatalogItem>(
        this, MessageKeys.AddProduct, (sender, arg) =>
    {
        messageReceived = true;
    });
    catalogViewModel.AddCatalogItemCommand.Execute(null);

    Assert.IsTrue(messageReceived);
}

Esta prueba unitaria comprueba que CatalogViewModel publique el mensaje AddProduct en respuesta a la ejecución de su método AddCatalogItemCommand. Dado que la clase MessagingCenter admite suscripciones de mensajes de multidifusión, la prueba unitaria puede suscribirse al mensaje AddProduct y ejecutar un delegado de devolución de llamada en respuesta a su recepción. Este delegado de devolución de llamada, especificado como una expresión lambda, establece un campo booleano utilizado por la instrucción Assert para comprobar el comportamiento de la prueba.

Prueba del control de excepciones

También se pueden escribir pruebas unitarias que comprueben que se generen excepciones específicas para acciones o entradas no válidas, como se muestra en el ejemplo de código siguiente:

[TestMethod]
public void InvalidEventNameShouldThrowArgumentExceptionText()
{
    var behavior = new MockEventToCommandBehavior
    {
        EventName = "OnItemTapped"
    };
    var listView = new ListView();

    Assert.Throws<ArgumentException>(() => listView.Behaviors.Add(behavior));
}

Esta prueba unitaria producirá una excepción porque el control ListView no tiene un evento llamado OnItemTapped. El método Assert.Throws<T> es un método genérico donde T es el tipo de la excepción esperada. El argumento pasado al método Assert.Throws<T> es una expresión lambda que producirá la excepción. Por lo tanto, se superará la prueba unitaria siempre que la expresión lambda produzca una excepción ArgumentException.

Sugerencia

Evite escribir pruebas unitarias que examinen las cadenas de los mensajes de excepción. Las cadenas de los mensajes de excepción pueden cambiar con el tiempo, por lo que las pruebas unitarias que dependen de su presencia se consideran frágiles.

Prueba de la validación

Hay dos aspectos para probar la implementación de la validación: probar que las reglas de validación se hayan implementado correctamente y probar que la clase ValidatableObject<T> se comporta según lo previsto.

La lógica de validación suele ser sencilla de probar, ya que normalmente es un proceso autocontenido en el que la salida depende de la entrada. Debe haber pruebas de los resultados de invocar al método Validate en cada propiedad que tenga al menos una regla de validación asociada, como se muestra en el ejemplo de código siguiente:

[TestMethod]
public void CheckValidationPassesWhenBothPropertiesHaveDataTest()
{
    var mockViewModel = new MockViewModel();
    mockViewModel.Forename.Value = "John";
    mockViewModel.Surname.Value = "Smith";

    var isValid = mockViewModel.Validate();

    Assert.IsTrue(isValid);
}

Esta prueba unitaria comprueba que la validación se realice correctamente cuando las dos propiedades ValidatableObject<T> de la instancia de MockViewModel tienen datos.

Además de comprobar que la validación se realice correctamente, las pruebas unitarias de la validación también deben comprobar los valores de las propiedades Value, IsValid y Errors de cada instancia de ValidatableObject<T> para comprobar que la clase funciona según lo previsto. En el ejemplo de código siguiente, se muestra una prueba unitaria que hace esto:

[TestMethod]
public void CheckValidationFailsWhenOnlyForenameHasDataTest()
{
    var mockViewModel = new MockViewModel();
    mockViewModel.Forename.Value = "John";

    bool isValid = mockViewModel.Validate();

    Assert.IsFalse(isValid);
    Assert.IsNotNull(mockViewModel.Forename.Value);
    Assert.IsNull(mockViewModel.Surname.Value);
    Assert.IsTrue(mockViewModel.Forename.IsValid);
    Assert.IsFalse(mockViewModel.Surname.IsValid);
    Assert.AreEqual(mockViewModel.Forename.Errors.Count(), 0);
    Assert.AreNotEqual(mockViewModel.Surname.Errors.Count(), 0);
}

Esta prueba unitaria comprueba que se produce un error en la validación cuando la propiedad Surname de MockViewModel no tiene ningún dato y las propiedades Value, IsValid y Errors de cada instancia de ValidatableObject<T> se han establecido correctamente.

Resumen

Una prueba unitaria toma una pequeña unidad de la aplicación, normalmente un método, lo aísla del resto del código y comprueba que se comporta según lo previsto. Su objetivo es comprobar que cada unidad de funcionalidad funciona según lo previsto, para que los errores no se propaguen en toda la aplicación.

Se puede aislar el comportamiento de un objeto sometido a prueba reemplazando los objetos dependientes por objetos ficticios que simulan el comportamiento de los objetos dependientes. Esto permite ejecutar pruebas unitarias sin necesidad de recursos pesados, como las características de la plataforma en tiempo de ejecución, los servicios web o las bases de datos.

Probar modelos y modelos de vista de aplicaciones MVVM es idéntico a probar cualquier otra clase y se pueden usar las mismas herramientas y técnicas.