Pruebas unitarias de aplicaciones empresariales

Nota:

Este libro se publicó en la primavera de 2017 y no se ha actualizado desde entonces. Hay mucho en el libro que sigue siendo valioso, pero algunos de los materiales están obsoletos.

Las aplicaciones móviles tienen problemas únicos que las aplicaciones de escritorio y basadas en web no tienen. Los usuarios móviles variarán según los dispositivos que usen, por conectividad de red, por la disponibilidad de los servicios y por una variedad de otros factores. Por lo tanto, las aplicaciones móviles deben probarse ya que se usarán en el mundo real para mejorar su calidad, confiabilidad y rendimiento. Hay muchos tipos de pruebas que se deben realizar en una aplicación, incluidas las pruebas unitarias, las pruebas de integración y las pruebas de interfaz de usuario, y las pruebas unitarias son la forma más común de pruebas.

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 donde se produce es más eficaz que observar el efecto de un error indirectamente en un punto de error secundario.

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. 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. En este escenario, las pruebas unitarias actúan como documentación de diseño y especificaciones funcionales.

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:

  • La sección arrange de un método de prueba unitaria inicializa objetos y establece el valor de los datos que se pasa al método en pruebas.
  • La sección act invoca al método objeto de las pruebas con los argumentos necesarios.
  • La sección assert comprueba si la acción del método en pruebas se comporta de la forma prevista.

Si sigue este patrón, se garantiza que las pruebas unitarias sean legibles 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 Autofac es la clase OrderService. En el ejemplo de código siguiente, se muestra un esquema de esta clase:

public class OrderDetailViewModel : ViewModelBase  
{  
    private IOrderService _ordersService;  

    public OrderDetailViewModel(IOrderService ordersService)  
    {  
        _ordersService = ordersService;  
    }  
    ...  
}

La clase OrderDetailViewModel tiene una dependencia del tipo IOrderService que el contenedor resuelve cuando crea una instancia de un objeto OrderDetailViewModel. Sin embargo, en lugar de crear un objeto OrderService para hacer las pruebas unitarias de la clase OrderDetailViewModel, reemplace el objeto OrderService por un objeto ficticio con fines de prueba. La figura 10-1 ilustra esta relación.

Classes that implement the IOrderService interface

Figura 10-1: Clases que implementan la interfaz IOrderService

Este enfoque permite pasar el objeto OrderService a la clase OrderDetailViewModel en tiempo de ejecución y, en interés de la capacidad de prueba, permite pasar una clase OrderMockService 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 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; 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. 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 móvil eShopOnContainers realiza pruebas unitarias, que admite dos tipos diferentes de pruebas unitarias:

  • Los hechos son pruebas que siempre son verdaderas, que prueban condiciones invariables.
  • Las teorías son pruebas que solo son verdaderas para un conjunto determinado de datos.

Las pruebas unitarias incluidas con la aplicación móvil eShopOnContainers son pruebas de hechos, por lo que cada método de la prueba unitaria está decorado con el atributo [Fact].

Nota:

Las pruebas xUnit se ejecutan mediante un ejecutor de pruebas. Para ejecutar el ejecutor de pruebas, ejecute el proyecto eShopOnContainers.TestRunner para la plataforma necesaria.

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:

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

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

    Assert.NotNull(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 OrderService como argumento. Sin embargo, OrderService recupera datos de un servicio web. Por lo tanto, se especifica una instancia de OrderMockService, que es 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:

[Fact]  
public async Task SettingOrderPropertyShouldRaisePropertyChanged()  
{  
    bool 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.True(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:

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

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

    Assert.True(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 boolean 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:

[Fact]  
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:

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

    bool isValid = mockViewModel.Validate();  

    Assert.True(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:

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

    bool isValid = mockViewModel.Validate();  

    Assert.False(isValid);  
    Assert.NotNull(mockViewModel.Forename.Value);  
    Assert.Null(mockViewModel.Surname.Value);  
    Assert.True(mockViewModel.Forename.IsValid);  
    Assert.False(mockViewModel.Surname.IsValid);  
    Assert.Empty(mockViewModel.Forename.Errors);  
    Assert.NotEmpty(mockViewModel.Surname.Errors);  
}

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 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.