Lenguaje
Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Shim, una de las dos tecnologías clave utilizadas por Microsoft Fakes Framework, son fundamentales para aislar los componentes de la aplicación durante las pruebas. Funcionan interceptando y desviando llamadas a métodos específicos, que luego puedes redirigir a código personalizado en tu prueba. Esta característica le permite administrar el resultado de estos métodos para asegurarse de que los resultados son coherentes y predecibles durante cada llamada, independientemente de las condiciones externas. Este nivel de control simplifica el proceso de prueba y ayuda a lograr resultados más confiables y precisos.
Emplee shims cuando necesite establecer una barrera entre su código y los ensamblados que no forman parte de su solución. Cuando el objetivo es aislar entre sí los componentes de la solución, se recomienda usar stubs. Para ver una descripción detallada de los stubs, consulte Use stubs para aislar entre sí partes de su aplicación en las pruebas unitarias.
En este artículo se proporciona una guía paso a paso para usar tipos shim para redirigir llamadas a determinados métodos en el código de pruebas unitarias.
Comprenda las limitaciones de los tipos con shims
Hay algunas limitaciones al trabajar con shims. No se pueden usar en todos los tipos de determinadas bibliotecas de la clase base .NET, en concreto:
- mscorlib y System en .NET Framework
- System.Runtime en .NET Core o .NET 5+
Al diseñar la estrategia de pruebas, planee esta restricción para garantizar pruebas unitarias correctas y eficaces.
Crear un ajuste de compatibilidad (shim)
Supongamos que el componente contiene llamadas al System.IO.File.ReadAllLines método :
// Code under test:
this.Records = System.IO.File.ReadAllLines(path);
Para preparar el componente para las pruebas unitarias, complete los pasos descritos en los procedimientos siguientes.
Creación de una biblioteca de clases
Cree una nueva solución y un proyecto inicial para la biblioteca de clases.
En el Visual Studio Start Window (File>Start Window), cree un proyecto
Class Libraryseleccionando la plantilla para C# o Visual Basic.
Configure el nuevo proyecto:
- Establezca la biblioteca de clases Nombre del proyecto como
HexFileReader. - Establezca el nombre de la solución en
ShimsTutorial. - Establezca el marco Target en .NET Framework 4.8.
Selecciona Crear.
- Establezca la biblioteca de clases Nombre del proyecto como
Una vez que se abra el proyecto, busque el archivo predeterminado
Class1.csen Explorador de soluciones y elimine el archivo.Agregue un archivo denominado
HexFile.csy escriba la siguiente definición de clase:
Adición de un proyecto de prueba unitaria
Agregue otro proyecto a la solución para las pruebas unitarias.
En Explorador de soluciones, haga clic con el botón derecho en la solución
ShimsTutorialy seleccione Add>Nuevo Project.En el Start Window, cree un
Unit Test Projectproject seleccionando la plantilla.
Configure el nuevo proyecto:
- Establezca la prueba unitaria nombre del proyecto en
TestProject. - Establezca el marco Target en .NET Framework 4.8.
Selecciona Crear.
- Establezca la prueba unitaria nombre del proyecto en
Agregar ensamblaje de Fakes
Agregue una referencia al proyecto de HexFileReader.
En Explorador de soluciones, expanda el nodo
TestProject, haga clic con el botón derecho en el nodo References y seleccione Agregar referencia.
En el panel izquierdo de la ventana Administrador de referencias , seleccione la sección Proyectos .
En el panel central, active la casilla del
HexFileReaderproyecto y seleccione Aceptar.
Agregue el ensamblado de Fakes.
En Explorador de soluciones, busque el nodo contenedor para el ensamblado:
Para un proyecto de .NET Framework anterior (de estilo no SDK), expanda el nodo del proyecto de pruebas unitarias y, a continuación, expanda el nodo References.
Para un proyecto de estilo SDK dirigido a .NET Framework, .NET Core o .NET 5+, expanda el nodo Dependencies para localizar el ensamblado que se va a simular en Assemblies, Projects o Packages.
Para Visual Basic, seleccione Show All Files en la barra de herramientas Explorador de soluciones para ver el nodo References.
Seleccione el
Systemensamblado que contiene la definición deSystem.IO.File.ReadAllLines.Haga clic con el botón derecho en el nodo
Systemy seleccione Agregar ensamblado Fakes.
El proceso de compilación genera advertencias y errores para los tipos que no se admiten para su uso con shims.
Cuando se complete la compilación, Explorador de soluciones se actualiza para mostrar un nodo Fakes para el proyecto de prueba unitaria.
Seleccione el
Fakes\mscorlib.fakesarchivo y reemplace el XML para excluir los tipos no admitidos:<Fakes xmlns="http://schemas.microsoft.com/fakes/2011/" Diagnostic="true"> <Assembly Name="mscorlib" Version="4.0.0.0"/> <StubGeneration> <Clear/> </StubGeneration> <ShimGeneration> <Clear/> <Add FullName="System.IO.File"/> <Remove FullName="System.IO.FileStreamAsyncResult"/> <Remove FullName="System.IO.FileSystemEnumerableFactory"/> <Remove FullName="System.IO.FileInfoResultHandler"/> <Remove FullName="System.IO.FileSystemInfoResultHandler"/> <Remove FullName="System.IO.FileStream+FileStreamReadWriteTask"/> <Remove FullName="System.IO.FileSystemEnumerableIterator"/> </ShimGeneration> </Fakes>
Creación de una prueba unitaria
Agregue una prueba unitaria para el proyecto.
Actualice el archivo predeterminado
TestProject\UnitTest1.csproporcionado por la plantilla de proyecto.Busque la siguiente sección de código en el archivo y reemplácela por el fragmento de código proporcionado:
[TestMethod] public void TestMethod1() { }Fragmento de código de reemplazo:
[TestMethod] public void TestFileReadAllLine() { using (ShimsContext.Create()) { // Arrange System.IO.Fakes.ShimFile.ReadAllLinesString = (s) => new string[] { "Hello", "World", "Shims" }; // Act var target = new HexFile("this_file_doesnt_exist.txt"); Assert.AreEqual(3, target.Records.Length); } }Para ver todos los ensamblados de Fakes de la prueba unitaria, seleccione Mostrar todos los archivos en la barra de menús del Explorador de soluciones:
Abra el Explorador de pruebas y ejecute la prueba.
Es fundamental liberar correctamente cada contexto shim. Como norma general, llame al método ShimsContext.Create dentro de una instrucción using para garantizar la correcta eliminación de los shims registrados. Por ejemplo, podría registrar un shim para un método de prueba que reemplace el método DateTime.Now por un delegado que siempre devuelva el 1 de enero de 2000. Si olvida eliminar el shim registrado en el método de prueba, el resto de la ejecución de la prueba siempre devolverá el valor DateTime.Now como 1 de enero de 2000. Este resultado puede ser sorprendente y confuso.
Revise las convenciones de nomenclatura para las clases de compatibilidad
Los nombres de clase Shim usan el Fakes.Shim prefijo seguido del nombre de tipo original. Los nombres de parámetro se anexan al nombre del método. (No tiene que agregar ninguna referencia de ensamblado a System.Fakes).
System.IO.File.ReadAllLines(path);
System.IO.Fakes.ShimFile.ReadAllLinesString = (path) => new string[] { "Hello", "World", "Shims" };
Entender cómo funcionan los shims
Los shims funcionan mediante la introducción de desviaciones en la base de código de la aplicación que se está probando. Cada vez que se llama al método original, el sistema Fakes interviene para redirigir la llamada, haciendo que se ejecute el código shim personalizado en lugar del método original.
Es importante tener en cuenta que estos desvíos se crean y quitan dinámicamente en tiempo de ejecución. Los desvíos siempre deben crearse durante la vida útil de un ShimsContext. Cuando se elimina ShimsContext, también se quitan los shims activos creados en él. Para gestionar este diseño de forma eficiente, se recomienda encapsular la creación de desviaciones dentro de una instrucción using.
Explorar shims para distintos tipos de métodos
Los shims admiten varios tipos de métodos.
Métodos estáticos
Cuando se aplican shims a métodos estáticos, las propiedades que contienen shims se alojan dentro de un tipo shim. Estas propiedades solo tienen un método set, que se utiliza para asociar un delegado al método de destino.
Por ejemplo, para una clase denominada MyClass con un método MyMethodestático :
//Code under test
public static class MyClass {
public static int MyMethod() {
...
}
}
Puede añadir un shim a MyMethod para que devuelva siempre 5:
// Unit test code
ShimMyClass.MyMethod = () => 5;
Métodos de instancia (para todas las instancias)
Al igual que los métodos estáticos, también se puede aplicar un shim a los métodos de instancia en todas las instancias. Las propiedades que agrupan estos ajustes de compatibilidad se encuentran en un tipo anidado denominado AllInstances para evitar confusiones.
Para la clase MyClass con un método MyMethodde instancia :
// Code under test
public class MyClass {
public int MyMethod() {
...
}
}
Puede adjuntar un shim a MyMethod para que devuelva siempre 5, independientemente de la instancia:
// Unit test code
ShimMyClass.AllInstances.MyMethod = () => 5;
En el fragmento de código siguiente se muestra la estructura de tipo generada de ShimMyClass:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
public static class AllInstances {
public static Func<MyClass, int>MyMethod {
set {
...
}
}
}
}
En este escenario, Fakes pasa la instancia en tiempo de ejecución como primer argumento del delegado.
Métodos de instancia (instancia en tiempo de ejecución única)
Los métodos de instancia también se pueden adaptar usando distintos delegados, según el receptor de la llamada. Este enfoque permite que el mismo método de instancia muestre comportamientos diferentes para cada instancia del tipo. Las propiedades que alojan estos shims son métodos de instancia del propio tipo de shim. Cada tipo shim instanciado está vinculado a una instancia no procesada de un tipo adaptado mediante shim.
Por ejemplo, dada una clase MyClass con un método MyMethodde instancia :
// Code under test
public class MyClass {
public int MyMethod() {
...
}
}
Puede crear dos tipos de shims para MyMethod de modo que el primero devuelva siempre 5 y el segundo devuelva siempre 10:
// Unit test code
var myClass1 = new ShimMyClass()
{
MyMethod = () => 5
};
var myClass2 = new ShimMyClass { MyMethod = () => 10 };
En el fragmento de código siguiente se muestra la estructura de tipo generada de ShimMyClass:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
public Func<int> MyMethod {
set {
...
}
}
public MyClass Instance {
get {
...
}
}
}
Se puede acceder a la instancia de tipo shimmed real a través de la Instance propiedad :
// Unit test code
var shim = new ShimMyClass();
var instance = shim.Instance;
El tipo shim también incluye una conversión implícita al tipo adaptado, que le permite usar el tipo shim directamente:
// Unit test code
var shim = new ShimMyClass();
MyClass instance = shim; // Implicit cast retrieves the runtime instance
Explorar constructores shim
Los constructores no son una excepción en lo que respecta al uso de shims. Se les pueden aplicar shims para adjuntar tipos de shim a objetos que se creen en el futuro. Por ejemplo, cada constructor se representa como un método estático denominado Constructor dentro del tipo shim.
Considere una clase MyClass con un constructor que acepte un entero:
public class MyClass {
public MyClass(int value) {
this.Value = value;
}
...
}
Se puede configurar un tipo de shim para este constructor de modo que, independientemente del valor que se pase al constructor, todas las instancias futuras devuelvan -5 cuando se invoque al getter Value:
// Unit test code
ShimMyClass.ConstructorInt32 = (@this, value) => {
var shim = new ShimMyClass(@this) {
ValueGet = () => -5
};
};
Cada tipo de shim expone dos tipos de constructores:
- Cuando necesite una nueva instancia, use el constructor predeterminado.
- Cuando tenga un shim de constructor, use el constructor que acepta como argumento una instancia con shim.
// Unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }
En el código siguiente se muestra el tipo generado para :ShimMyClass
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass>
{
public static Action<MyClass, int> ConstructorInt32 {
set {
...
}
}
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }
...
}
Acceder a los miembros de la clase base
Puede acceder a las propiedades shim de los miembros de la clase base creando un shim para el tipo base. A continuación, pase la instancia hija al constructor de la clase base shim.
Por ejemplo, considere una clase MyBase con un método MyMethod de instancia y un subtipo MyChild:
public abstract class MyBase {
public int MyMethod() {
...
}
}
public class MyChild : MyBase {
}
Se puede configurar un shim de MyBase iniciando un nuevo shim de ShimMyBase:
// unit test code
var child = new ShimMyChild();
new ShimMyBase(child) { MyMethod = () => 5 };
Es importante tener en cuenta que, cuando se pasa el shim derivado como parámetro al constructor del shim base, el tipo del shim derivado se convierte implícitamente en la instancia derivada.
La estructura del tipo generado para ShimMyChild y ShimMyBase es similar al código siguiente:
// Fakes generated code
public class ShimMyChild : ShimBase<MyChild> {
public ShimMyChild() { }
public ShimMyChild(Child child)
: base(child) { }
}
public class ShimMyBase : ShimBase<MyBase> {
public ShimMyBase(Base target) { }
public Func<int> MyMethod
{ set { ... } }
}
Constructores estáticos
Los tipos shim exponen un método estático StaticConstructor para aplicar un shim al constructor estático de un tipo. Dado que los constructores estáticos solo se ejecutan una vez, debe asegurarse de que el shim esté configurado antes de acceder a cualquier miembro del tipo.
Finalizadores
Los finalizadores no son compatibles con Fakes.
Métodos privados
El generador de código de Fakes crea propiedades shim para métodos privados cuya firma solo contiene tipos visibles, es decir, con tipos de parámetro y tipo de valor devuelto visibles.
Interfaces de enlace
Cuando un tipo de shimmed implementa una interfaz, el generador de código emite un método que le permite enlazar todos los miembros de esa interfaz a la vez.
Por ejemplo, dada una clase MyClass que implementa IEnumerable<int>:
public class MyClass : IEnumerable<int> {
public IEnumerator<int> GetEnumerator() {
...
}
...
}
Puede adaptar las implementaciones de IEnumerable<int> en MyClass llamando al método Bind:
// Unit test code
var shimMyClass = new ShimMyClass();
shimMyClass.Bind(new List<int> { 1, 2, 3 });
La estructura de tipo generada de ShimMyClass es similar al código siguiente:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
public ShimMyClass Bind(IEnumerable<int> target) {
...
}
}
Cambio del comportamiento predeterminado
Cada tipo de adaptación generado incluye una instancia de la interfaz IShimBehavior, accesible a través de la propiedad ShimBase<T>.InstanceBehavior. Este comportamiento se activa cada vez que un cliente realiza una llamada a un miembro de instancia para el que no se ha definido explícitamente un shim.
De forma predeterminada, si no se establece ningún comportamiento específico, el diseño usa la instancia devuelta por la propiedad estática ShimBehaviors.Current , que normalmente produce una NotImplementedException excepción.
Puede modificar este comportamiento en cualquier momento ajustando la propiedad InstanceBehavior de cualquier instancia de shim.
Por ejemplo, puede modificar el comportamiento para no hacer nada o devolver el valor predeterminado del tipo de valor devuelto, default(T):
// Unit test code
var shim = new ShimMyClass();
//Return default(T) or do nothing
shim.InstanceBehavior = ShimBehaviors.DefaultValue;
También puede cambiar globalmente el comportamiento de todas las instancias de shimmed, donde la InstanceBehavior propiedad no está definida explícitamente, estableciendo la propiedad estática ShimBehaviors.Current :
// Unit test code
// Change default shim for all shim instances where the behavior isn't set
ShimBehaviors.Current = ShimBehaviors.DefaultValue;
Identificación de interacciones con dependencias externas
Para ayudar a identificar cuándo su código interactúa con sistemas externos o dependencias (denominados environment), puede utilizar shims para asignar un comportamiento específico a todos los miembros de un tipo (incluidos los métodos estáticos). Al establecer el comportamiento ShimBehaviors.NotImplemented en la propiedad estática Behavior del tipo shim, cualquier acceso a un miembro de ese tipo para el que no se haya definido explícitamente un shim genera un NotImplementedException. Este resultado puede servir como una señal útil durante las pruebas, lo que indica que el código está intentando acceder a un sistema externo o dependencia.
Aquí tienes un ejemplo de cómo configurar este tipo de shim en el código de pruebas unitarias:
// Unit test code
// Assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.Behavior = ShimBehaviors.NotImplemented;
Para mayor comodidad, también se proporciona un método abreviado para lograr el mismo efecto:
// Shorthand to assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.BehaveAsNotImplemented();
Invocar métodos originales desde los métodos shim
En algunos casos, puede que necesite ejecutar el método original mientras se ejecuta el método shim. Por ejemplo, es posible que quiera escribir texto en el sistema de archivos después de validar el nombre de archivo pasado al método .
Un enfoque para gestionar esta situación es encapsular una llamada al método original mediante un delegado y el método ShimsContext.ExecuteWithoutShims():
// Unit test code
ShimFile.WriteAllTextStringString = (fileName, content) => {
ShimsContext.ExecuteWithoutShims(() => {
Console.WriteLine("enter");
File.WriteAllText(fileName, content);
Console.WriteLine("leave");
});
};
Como alternativa, puede anular el shim, llamar al método original y, a continuación, restaurar el shim:
// Unit test code
ShimsDelegates.Action<string, string> shim = null;
shim = (fileName, content) => {
try {
Console.WriteLine("enter");
// Remove shim in order to call original method
ShimFile.WriteAllTextStringString = null;
File.WriteAllText(fileName, content);
}
finally
{
// Restore shim
ShimFile.WriteAllTextStringString = shim;
Console.WriteLine("leave");
}
};
// Initialize the shim
ShimFile.WriteAllTextStringString = shim;
Gestionar la concurrencia con tipos shim
Los tipos shim operan en todos los subprocesos del AppDomain y no tienen afinidad con subprocesos. Esta característica es fundamental para tener en cuenta si tiene previsto utilizar un ejecutor de pruebas que admita la simultaneidad. Cabe destacar que las pruebas que implican tipos shim no pueden ejecutarse de forma simultánea, aunque el entorno de ejecución de Fakes no impone esta restricción.
Usar shims con System.Environment
Para crear un shim para la clase System.Environment, debe modificar el archivo mscorlib.fakes.
Busque el \<Assembly> elemento y agregue el siguiente contenido después de la definición del elemento:
<ShimGeneration>
<Add FullName="System.Environment"/>
</ShimGeneration>
Después de realizar cambios y recompilar la solución, los métodos y las propiedades de la clase System.Environment ahora están disponibles para crear shims.
Este es un ejemplo de cómo puede asignar un comportamiento al GetCommandLineArgsGet método :
System.Fakes.ShimEnvironment.GetCommandLineArgsGet = ...
Al realizar estas modificaciones, obtendrá la capacidad de controlar y probar cómo interactúa el código con las variables de entorno del sistema, que es una herramienta esencial para realizar pruebas unitarias completas.