Usar código auxiliar para aislar partes de la aplicación entre sí para las pruebas unitarias

Los tipos de código auxiliar son una tecnología importante proporcionada por el marco de Microsoft Fakes, lo que permite aislar fácilmente el componente sometido a pruebas de otros componentes que se usan. Un código auxiliar actúa como un pequeño fragmento de código que reemplaza a otro componente durante las pruebas. Una ventaja clave del uso de códigos auxiliares es la capacidad de obtener resultados coherentes para facilitar la escritura de pruebas. Incluso si los demás componentes aún no son totalmente funcionales, se pueden seguir ejecutando pruebas mediante códigos auxiliares.

Para aplicar los códigos auxiliares eficazmente, se recomienda diseñar el componente de forma que dependa eminentemente de interfaces, y no de clases concretas de otras partes de la aplicación. Este método de diseño fomenta el desacoplamiento y reduce la probabilidad de que los cambios ocurridos en una parte requiera realizar modificaciones en otra. En lo que respecta a las pruebas, este patrón de diseño permite sustituir una implementación de código auxiliar de un componente real, lo que facilita un aislamiento eficaz y la realización de unas pruebas precisas en el componente de destino.

Por ejemplo, vamos a ver el diagrama que ilustra los componentes implicados:

Diagram of Real and Stub classes of StockAnalyzer.

En este diagrama, el componente sometido a pruebas es StockAnalyzer, que normalmente se basa en otro componente denominado RealStockFeed. Sin embargo, RealStockFeed dificulta la realización de las pruebas porque devuelve resultados diferentes cada vez que se llama a sus métodos. Esta variabilidad hace que sea complicado realizar unas pruebas de StockAnalyzer coherentes y confiables.

Para superar este obstáculo durante las pruebas, podemos adoptar la práctica de inserción de dependencias, que consiste en escribir el código de forma que no se mencionen expresamente clases en otro componente de la aplicación. En su lugar, se define una interfaz que puedan implementar el otro componente y un código auxiliar para las pruebas.

Este es un ejemplo de cómo usar la inserción de dependencias en el código:

public int GetContosoPrice(IStockFeed feed) => feed.GetSharePrice("COOO");

Limitaciones del código auxiliar

Revise las siguientes limitaciones para los códigos auxiliares.

Creación de un código auxiliar: guía paso a paso

Comencemos con un ejemplo motivador, el que aparece en el diagrama de antes.

Creación de una biblioteca de clases

Siga los pasos siguientes para crear una biblioteca de clases.

  1. Abra Visual Studio y cree un proyecto de biblioteca de clases.

    Screenshot of Class Library project in Visual Studio.

  2. Configure los atributos del proyecto:

    • Establezca el nombre del proyecto en StockAnalysis.
    • Establezca el nombre de la solución en StubsTutorial.
    • Establezca la plataforma de destino del proyecto en .NET 8.0.
  3. Elimine el archivo predeterminado Class1.cs.

  4. Agregue un nuevo archivo denominado IStockFeed.cs y copie en él la siguiente definición de interfaz:

    // IStockFeed.cs
    public interface IStockFeed
    {
        int GetSharePrice(string company);
    }
    
  5. Agregue otro nuevo archivo denominado StockAnalyzer.cs y copie en él la siguiente definición de clase:

    // StockAnalyzer.cs
    public class StockAnalyzer
    {
        private IStockFeed stockFeed;
        public StockAnalyzer(IStockFeed feed)
        {
            stockFeed = feed;
        }
        public int GetContosoPrice()
        {
            return stockFeed.GetSharePrice("COOO");
        }
    }
    

Creación de un proyecto de prueba

Cree el proyecto de prueba para el ejercicio.

  1. Haga clic con el botón derecho en la solución y agregue un nuevo proyecto denominado MSTest Test Project.

  2. Establezca el nombre del proyecto en TestProject.

  3. Establezca la plataforma de destino del proyecto en .NET 8.0.

    Screenshot of Test project in Visual Studio.

Adición de un ensamblado de Fakes

Agregue el ensamblado de Fakes del proyecto.

  1. Agregue una referencia de proyecto a StockAnalyzer.

    Screenshot of the command Add Project Reference.

  2. Agregue el ensamblado de Fakes.

    1. En el Explorador de soluciones, busque la referencia de ensamblado:

      • Para un proyecto de .NET Framework anterior (que no sea de estilo SDK), expanda el nodo Referencias del proyecto de pruebas unitarias.

      • Para un proyecto de estilo SDK que tenga como destino .NET Framework, .NET Core o .NET 5.0 y versiones posteriores, expanda el nodo Dependencias para buscar el ensamblado que quiere emular en Ensamblados, Proyectos o Paquetes.

      • Si está trabajando en Visual Basic, seleccione Mostrar todos los archivos en la barra de herramientas del Explorador de soluciones para ver el nodo Referencias.

    2. Seleccione el ensamblado que contiene las definiciones de clases para las que quiere crear stubs.

    3. En el menú contextual, seleccione Agregar ensamblado de Fakes.

      Screenshot of the command Add Fakes Assembly.

Crear una prueba unitaria

Ahora cree la prueba unitaria.

  1. Modifique el archivo predeterminado UnitTest1.cs para agregar la siguiente definición Test Method.

    [TestClass]
    class UnitTest1
    {
        [TestMethod]
        public void TestContosoPrice()
        {
            // Arrange:
            int priceToReturn = 345;
            string companyCodeUsed = "";
            var componentUnderTest = new StockAnalyzer(new StockAnalysis.Fakes.StubIStockFeed()
            {
                GetSharePriceString = (company) =>
                {
                    // Store the parameter value:
                    companyCodeUsed = company;
                    // Return the value prescribed by this test:
                    return priceToReturn;
                }
            });
    
            // Act:
            int actualResult = componentUnderTest.GetContosoPrice();
    
            // Assert:
            // Verify the correct result in the usual way:
            Assert.AreEqual(priceToReturn, actualResult);
    
            // Verify that the component made the correct call:
            Assert.AreEqual("COOO", companyCodeUsed);
        }
    }
    

    El toque mágico aquí lo pone la clase StubIStockFeed. Para cada interfaz del ensamblado al que se hace referencia, el mecanismo de Microsoft Fakes genera una clase de código auxiliar. El nombre de la clase de código auxiliar se deriva del nombre de la interfaz, con "Fakes.Stub" como prefijo y los nombres de los tipos de parámetros anexados.

    El código auxiliar también se genera para captadores y establecedores de propiedades, para los eventos y para métodos genéricos. Para obtener más información, vea Usar stubs para aislar las partes de la aplicación entre sí para las pruebas unitarias.

    Screenshot of Solution Explorer showing all files.

  2. Abra el Explorador de pruebas y ejecute la prueba.

    Screenshot of Test Explorer.

Códigos auxiliares para las diferentes clases de miembros de tipo

Hay códigos auxiliares para las diferentes clases de miembros de tipo.

Métodos

En el ejemplo provisto, los métodos se pueden procesar con código auxiliar asociando un delegado a una instancia de la clase de código auxiliar. El nombre del tipo de código auxiliar se deriva de los nombres del método y los parámetros. Por ejemplo, analicemos la siguiente interfaz IStockFeed y su método GetSharePrice correspondiente:

// IStockFeed.cs
interface IStockFeed
{
    int GetSharePrice(string company);
}

Asociamos un código auxiliar a GetSharePrice mediante GetSharePriceString:

// unit test code
var componentUnderTest = new StockAnalyzer(new StockAnalysis.Fakes.StubIStockFeed()
        {
            GetSharePriceString = (company) =>
            {
                // Store the parameter value:
                companyCodeUsed = company;
                // Return the value prescribed by this test:
                return priceToReturn;
            }
        });

Si no se proporciona el código auxiliar de un método, Fakes genera una función que devuelve el default value del tipo de valor devuelto. Para los números, el valor predeterminado es 0. Para los tipos de clase, el valor predeterminado es null en C# o Nothing en Visual Basic.

Propiedades

Los captadores y establecedores de propiedades se muestran como delegados independientes, y pueden procesarse con código auxiliar individualmente. Por ejemplo, considere la propiedad Value de IStockFeedWithProperty:

interface IStockFeedWithProperty
{
    int Value { get; set; }
}

Para usar código auxiliar al captador y al establecedor de Value y simular una propiedad automática, puede usar el siguiente código:

// unit test code
int i = 5;
var stub = new StubIStockFeedWithProperty();
stub.ValueGet = () => i;
stub.ValueSet = (value) => i = value;

Si no proporciona métodos de código auxiliar del establecedor o del captador de una propiedad, Fakes genera código auxiliar que almacena valores, de modo que la propiedad de código auxiliar se comporte como una simple variable.

Eventos

Los eventos se exponen como campos delegados, lo que permite generar cualquier evento de código auxiliar simplemente invocando el campo de respaldo de evento. Veamos la interfaz siguiente que se procesa con stub:

interface IStockFeedWithEvents
{
    event EventHandler Changed;
}

Para generar el evento Changed, se invoca simplemente el delegado de respaldo:

// unit test code
var withEvents = new StubIStockFeedWithEvents();
// raising Changed
withEvents.ChangedEvent(withEvents, EventArgs.Empty);

Métodos genéricos

Se pueden procesar métodos genéricos con código auxiliar si se proporciona un delegado para cada instancia del método que se quiera. Por ejemplo, dada la siguiente interfaz con un método genérico:

interface IGenericMethod
{
    T GetValue<T>();
}

Puede procesar la creación de una instancia de GetValue<int> con código auxiliar del siguiente modo:

[TestMethod]
public void TestGetValue()
{
    var stub = new StubIGenericMethod();
    stub.GetValueOf1<int>(() => 5);

    IGenericMethod target = stub;
    Assert.AreEqual(5, target.GetValue<int>());
}

Si el código llama a GetValue<T> con cualquier otra creación de instancia, el código auxiliar simplemente ejecutará el comportamiento.

Códigos auxiliares de clases virtuales

En los ejemplos anteriores, el código auxiliar se han generado a partir de interfaces. Con todo, también puede generar código auxiliar desde una clase que tenga miembros virtuales o abstractos. Por ejemplo:

// Base class in application under test
public abstract class MyClass
{
    public abstract void DoAbstract(string x);
    public virtual int DoVirtual(int n)
    {
        return n + 42;
    }

    public int DoConcrete()
    {
        return 1;
    }
}

En el código auxiliar generado a partir de esta clase, puede establecer métodos delegados para DoAbstract() y DoVirtual(), pero no DoConcrete().

// unit test
var stub = new Fakes.MyClass();
stub.DoAbstractString = (x) => { Assert.IsTrue(x>0); };
stub.DoVirtualInt32 = (n) => 10 ;

Si no proporciona un delegado de un método virtual, Fakes puede proporcionar el comportamiento predeterminado o llamar al método en la clase base. Para hacer que se llame al método base, establezca la propiedad CallBase:

// unit test code
var stub = new Fakes.MyClass();
stub.CallBase = false;
// No delegate set - default delegate:
Assert.AreEqual(0, stub.DoVirtual(1));

stub.CallBase = true;
// No delegate set - calls the base:
Assert.AreEqual(43,stub.DoVirtual(1));

Cambio del comportamiento predeterminado del código auxiliar

Cada tipo de código auxiliar generado contiene una instancia de la interfaz IStubBehavior mediante la propiedad IStub.InstanceBehavior. Se llama a este comportamiento cuando un cliente llama a un miembro sin ningún delegado personalizado asociado. Si el comportamiento no está establecido, se utiliza la instancia devuelta por la propiedad StubsBehaviors.Current. De forma predeterminada, esta propiedad devuelve un comportamiento que genera una excepción NotImplementedException.

El comportamiento se puede cambiar en cualquier momento estableciendo la propiedad InstanceBehavior en cualquier instancia de código auxiliar. Por ejemplo, el siguiente fragmento cambia el comportamiento de forma que el código auxiliar no hace nada, o bien devuelve el valor predeterminado del tipo de valor devuelto default(T):

// unit test code
var stub = new StockAnalysis.Fakes.StubIStockFeed();
// return default(T) or do nothing
stub.InstanceBehavior = StubsBehaviors.DefaultValue;

El comportamiento también se puede cambiar globalmente en todos los objetos de código auxiliar en los que el comportamiento no está establecido mediante la propiedad StubsBehaviors.Current:

// Change default behavior for all stub instances where the behavior has not been set.
StubBehaviors.Current = BehavedBehaviors.DefaultValue;