Uso de correcciones de compatibilidad (shim) para aislar la aplicación para pruebas unitarias
Los tipos de correcciones de compatibilidad (shim), una de las dos tecnologías clave del marco Microsoft Fakes, son fundamentales a la hora de aislar los componentes de la aplicación durante las pruebas. Funcionan interceptando y desviando llamadas a métodos específicos, que luego se pueden dirigir al código personalizado dentro de la prueba. Esta característica permite administrar el resultado de estos métodos, lo que garantiza que los resultados sean coherentes y predecibles durante cada llamada, independientemente de las condiciones externas. Este nivel de control simplifica el proceso de prueba y ayuda a lograr unos resultados más confiables y precisos.
Use correcciones de compatibilidad (shim) cuando necesite establecer un límite entre el código y los ensamblados que no forman parte de la solución. Si el objetivo es aislar los componentes de la solución entre sí, se recomienda usar códigos auxiliares.
(Para obtener una descripción más detallada, vea Uso de código auxiliar para aislar partes de la aplicación entre sí en pruebas unitarias).
Limitaciones de las correcciones de compatibilidad (shim)
Es importante saber que las correcciones de compatibilidad tienen sus limitaciones.
Las correcciones de compatibilidad (shim) no se pueden usar en todos los tipos de algunas bibliotecas de la clase base de .NET, concretamente, en mscorlib y System en .NET Framework, y en System.Runtime en .NET Core o .NET 5+. Esta limitación debe tenerse en cuenta durante la fase de diseño y planeamiento de pruebas para que la estrategia de pruebas sea correcta y eficaz.
Creación de correcciones de compatibilidad (shim): guía paso a paso
Supongamos que el componente contiene las llamadas a System.IO.File.ReadAllLines
:
// Code under test:
this.Records = System.IO.File.ReadAllLines(path);
Creación de una biblioteca de clases
Abra Visual Studio y cree un proyecto
Class Library
.Establezca el nombre de proyecto
HexFileReader
.Establezca el nombre de solución
ShimsTutorial
.Establezca la plataforma de destino del proyecto en .NET Framework 4.8.
Elimine el archivo predeterminado
Class1.cs
.Agregue un nuevo archivo
HexFile.cs
y agregue la siguiente definición de clase:
Creación de un proyecto de prueba
Haga clic con el botón derecho en la solución y agregue un nuevo proyecto
MSTest Test Project
.Establezca el nombre de proyecto
TestProject
.Establezca la plataforma de destino del proyecto en .NET Framework 4.8.
Agregar un ensamblado de Fakes
Agregue una referencia de proyecto a
HexFileReader
.Agregar un ensamblado de Fakes
En el Explorador de soluciones:
Para un proyecto de .NET Framework anterior (que no sea de estilo SDK), expanda el nodo Referencias del proyecto de pruebas unitarias.
En un proyecto de estilo SDK que tenga como destino .NET Framework, .NET Core o .NET 5+, expanda el nodo Dependencias para buscar el ensamblado que quiere imitar 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.
Seleccione el ensamblado
System
que contiene la definición deSystem.IO.File.ReadAllLines
.En el menú contextual, seleccione Agregar ensamblado de Fakes.
Como la compilación genera algunas advertencias y errores porque no todos los tipos se pueden usar con correcciones de compatibilidad (shim), tendrá que modificar el contenido de Fakes\mscorlib.fakes
para excluirlos.
<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>
Crear una prueba unitaria
Modifique el archivo predeterminado
UnitTest1.cs
para agregar el siguienteTestMethod
.[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); } }
Aquí tenemos el Explorador de soluciones con todos los archivos.
Abra el Explorador de pruebas y ejecute la prueba.
Esto es fundamental para eliminar correctamente el contexto de cada corrección de compatibilidad. Como regla general, llame a ShimsContext.Create
dentro de una instrucción using
para asegurarse de borrar correctamente las correcciones de compatibilidad (shim) registradas. Por ejemplo, puede registrar una corrección de compatibilidad para un método de prueba que reemplaza el método DateTime.Now
con un delegado que siempre devuelve el uno de enero de 2000. Si se olvida de borrar la corrección de compatibilidad (shim) registrada en el método de prueba, el resto de la ejecución de prueba devolverá siempre el uno de enero de 2000 como valor de DateTime.Now
. Esto puede tener efectos inesperados y sorprendentes.
Convenciones de nomenclatura de las clases de correcciones de compatibilidad (shim)
Los nombres de clase Shim se componen anteponiendo Fakes.Shim
al nombre de tipo original. Los nombres de parámetro se anexan al nombre del método. (No es necesario agregar referencias de ensamblado a System.Fakes).
System.IO.File.ReadAllLines(path);
System.IO.Fakes.ShimFile.ReadAllLinesString = (path) => new string[] { "Hello", "World", "Shims" };
Descripción del funcionamiento de las correcciones de compatibilidad (shim)
Las correcciones de compatibilidad (shim) funcionan incluyendo desvíos en el código base de la aplicación que se está probando. Siempre que haya una llamada al método original, el sistema de Fakes interviene para redirigir esa llamada, lo que hace que se ejecute el código de correcciones de compatibilidad (shim) personalizado en lugar del método original.
Es importante decir que estos desvíos se crean y quitan dinámicamente en tiempo de ejecución. Los desvíos siempre deben crearse mientras dure un ShimsContext
. Cuando ese ShimsContext se elimine, también se quitarán las correcciones de compatibilidad (shim) activas que se crearon mientras estaba vigente. Para controlar esto eficazmente, se recomienda encapsular la creación de desvíos dentro de una instrucción using
.
Correcciones de compatibilidad (shim) para diferentes tipos de métodos
Las correcciones de compatibilidad (shim) admiten varios tipos de métodos.
Métodos estáticos
Cuando se usan correcciones de compatibilidad (shim) en métodos estáticos, las propiedades que contengan correcciones de compatibilidad (shim) se colocan dentro de un tipo de correcciones de compatibilidad (shim). Estas propiedades solo poseen un establecedor, que se usa para asociar un delegado al método de destino. Por ejemplo, si tenemos una clase llamada MyClass
con un método estático MyMethod
:
//code under test
public static class MyClass {
public static int MyMethod() {
...
}
}
Podemos asociar una corrección de compatibilidad (shim) a MyMethod
de modo que devuelva constantemente 5:
// unit test code
ShimMyClass.MyMethod = () => 5;
Métodos de instancia (para todas las instancias)
Al igual que sucede con los métodos estáticos, los métodos de instancia también se pueden procesar con correcciones de compatibilidad (shim) para todas las instancias. Las propiedades que contienen estas correcciones de compatibilidad (shim) se colocan en un tipo anidado denominado AllInstances para evitar confusiones. Si tenemos una clase MyClass
con un método de instancia MyMethod
:
// code under test
public class MyClass {
public int MyMethod() {
...
}
}
Podemos asociar una corrección de compatibilidad (shim) a MyMethod
de forma que siempre devuelva 5, independientemente de la instancia:
// unit test code
ShimMyClass.AllInstances.MyMethod = () => 5;
La estructura de tipo generada de ShimMyClass
tendría el siguiente aspecto:
// 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 procesar con correcciones de compatibilidad (shim) usando diferentes delegados, en función del receptor de la llamada. Esto permite que el mismo método de instancia se comporte de distinta forma con cada instancia del tipo. Las propiedades que contienen estas correcciones de compatibilidad (shim) son métodos de instancia del propio tipo de corrección de compatibilidad. Cada instancia del tipo de corrección de compatibilidad (shim) está ligada a una instancia sin procesar de un tipo corregido de la corrección de compatibilidad (shim) en cuestión.
Por ejemplo, dada una clase MyClass
con un método de instancia MyMethod
:
// code under test
public class MyClass {
public int MyMethod() {
...
}
}
Se pueden crear dos tipos de correcciones de compatibilidad (shim) para MyMethod
, de modo que el primero devuelva constantemente 5 y el segundo, 10:
// unit test code
var myClass1 = new ShimMyClass()
{
MyMethod = () => 5
};
var myClass2 = new ShimMyClass { MyMethod = () => 10 };
La estructura de tipo generada de ShimMyClass
tendría el siguiente aspecto:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
public Func<int> MyMethod {
set {
...
}
}
public MyClass Instance {
get {
...
}
}
}
Puede tener acceso a la instancia real del tipo corregido para compatibilidad a través de la propiedad Instance:
// unit test code
var shim = new ShimMyClass();
var instance = shim.Instance;
El tipo de corrección de compatibilidad (shim) incluye también una conversión implícita al tipo corregido para compatibilidad, por lo que, en general, se puede usar directamente el tipo de corrección de compatibilidad (shim):
// unit test code
var shim = new ShimMyClass();
MyClass instance = shim; // implicit cast retrieves the runtime instance
Constructores
Los constructores no son una excepción al procesamiento con correcciones de compatibilidad (shim); también se pueden procesar de esta manera para asociar tipos de correcciones de compatibilidad (shim) a los objetos que se crearán en el futuro. Por ejemplo, cada constructor se representa como un método estático, denominado Constructor
, dentro del tipo de corrección de compatibilidad (shim). Vamos a analizar una clase MyClass
con un constructor que acepta un entero:
public class MyClass {
public MyClass(int value) {
this.Value = value;
}
...
}
Se puede configurar un tipo de corrección de compatibilidad (shim) para el constructor de forma que, independientemente del valor pasado al constructor, cada instancia futura devuelva -5 cuando se invoque el captador ValueGet:
// unit test code
ShimMyClass.ConstructorInt32 = (@this, value) => {
var shim = new ShimMyClass(@this) {
ValueGet = () => -5
};
};
Cada tipo de corrección de compatibilidad (shim) expone dos tipos de constructores. El constructor predeterminado se debe usar cuando se necesita una nueva instancia, mientras el constructor que toma como argumento una instancia procesada con correcciones de compatibilidad (shim) se debe usar únicamente en correcciones de compatibilidad (shim) de constructor:
// unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }
La estructura del tipo generado para ShimMyClass
se puede ilustrar de la siguiente manera:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass>
{
public static Action<MyClass, int> ConstructorInt32 {
set {
...
}
}
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }
...
}
Acceso a miembros base
Para tener acceso a las propiedades de correcciones de compatibilidad (shim) de los miembros base, hay que crear una corrección de compatibilidad (shim) para el tipo base y pasar la instancia secundaria al constructor de la clase base de la corrección de compatibilidad (shim).
Por ejemplo, si tenemos una clase MyBase
con un método de instancia MyMethod
y un subtipo MyChild
:
public abstract class MyBase {
public int MyMethod() {
...
}
}
public class MyChild : MyBase {
}
Puede configurar una corrección de compatibilidad (shim) de MyBase
iniciando una corrección de compatibilidad (shim) ShimMyBase
nueva:
// unit test code
var child = new ShimMyChild();
new ShimMyBase(child) { MyMethod = () => 5 };
Es importante reseñar que, cuando se pasa como parámetro al constructor de correcciones de compatibilidad (shim) base, el tipo de corrección de compatibilidad (shim) secundaria se convierte implícitamente en la instancia secundaria.
La estructura del tipo generado para ShimMyChild
y ShimMyBase
puede ser 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 de corrección de compatibilidad exponen un método estático StaticConstructor
realizar correcciones de compatibilidad del constructor estático de un tipo. Dado que los constructores estáticos se ejecutan una sola vez, debe asegurarse de que la corrección de compatibilidad esté configurada antes de que se tenga acceso a cualquier miembro del tipo.
Finalizadores
Los finalizadores no se admiten en Fakes.
Métodos privados
El generador de código de Fakes crea las propiedades de corrección de compatibilidad para los métodos privados que solo tienen tipos visibles en la firma, es decir, tipos de parámetros y tipo de valor devuelto visibles.
Interfaces de enlace
Cuando un tipo corregido para compatibilidad implementa una interfaz, el generador de código emite un método que le permite enlazar a la vez todos los miembros de esa interfaz.
Por ejemplo, dada una clase MyClass
que implementa IEnumerable<int>
:
public class MyClass : IEnumerable<int> {
public IEnumerator<int> GetEnumerator() {
...
}
...
}
Se pueden realizar correcciones de compatibilidad (shim) de las implementaciones de IEnumerable<int>
de MyClass llamando al método Bind:
// unit test code
var shimMyClass = new ShimMyClass();
shimMyClass.Bind(new List<int> { 1, 2, 3 });
La estructura del tipo generado 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 corrección de compatibilidad (shim) generado incluye una instancia de la interfaz IShimBehavior
, accesible a través de la propiedad ShimBase<T>.InstanceBehavior
. Este comportamiento se invoca siempre que un cliente llama a un miembro de instancia que no se ha procesado explícitamente con correcciones de compatibilidad (shim).
Si no se ha establecido ningún comportamiento específico, se usa de forma predeterminada la instancia devuelta por la propiedad estática ShimBehaviors.Current
, que suele producir una excepción NotImplementedException
.
Puede modificar este comportamiento en cualquier momento ajustando la propiedad InstanceBehavior
de cualquier instancia de correcciones de compatibilidad (shim). Por ejemplo, el siguiente fragmento de código modifica el comportamiento para no hacer nada o para devolver el valor predeterminado del tipo de valor devuelto, es decir, 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 procesadas con correcciones de compatibilidad (shim) —donde la propiedad InstanceBehavior
no se ha definido explícitamente— estableciendo la propiedad estática ShimBehaviors.Current
:
// unit test code
// change default shim for all shim instances where the behavior has not been set
ShimBehaviors.Current = ShimBehaviors.DefaultValue;
Identificación de interacciones con dependencias externas
Para ayudar a identificar cuándo el código interactúa con dependencias o sistemas externos (denominados environment
), se pueden usar correcciones de compatibilidad (shim) para asignar un comportamiento específico a todos los miembros de un tipo determinado. Esto incluye los métodos estáticos. Al establecer el comportamiento ShimBehaviors.NotImplemented
en la propiedad estática Behavior
del tipo de correcciones de compatibilidad (shim), cualquier acceso a un miembro de ese tipo que no se haya corregido explícitamente producirá una excepción NotImplementedException
. Esto puede servir como una señal útil durante las pruebas que indique que el código está intentando acceder a una dependencia o sistema externo.
Este es un ejemplo de cómo configurar esto en el código de prueba unitaria:
// unit test code
// Assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.Behavior = ShimBehaviors.NotImplemented;
Para mayor comodidad, también se proporciona un método abreviado que permite lograr el mismo efecto:
// Shorthand to assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.BehaveAsNotImplemented();
Invocación de métodos originales desde métodos de correcciones de compatibilidad (shim)
Podrían haber situaciones en las que puede que tenga que ejecutar el método original durante la ejecución del método de correcciones de compatibilidad (shim). Por ejemplo, podríamos querer escribir texto en el sistema de archivos después de haber validado el nombre de archivo que se ha pasado al método.
Un método para controlar esta situación es encapsular una llamada al método original mediante un delegado y ShimsContext.ExecuteWithoutShims()
, como se muestra en el código siguiente:
// unit test code
ShimFile.WriteAllTextStringString = (fileName, content) => {
ShimsContext.ExecuteWithoutShims(() => {
Console.WriteLine("enter");
File.WriteAllText(fileName, content);
Console.WriteLine("leave");
});
};
Como alternativa, se puede anular la corrección de compatibilidad (shim), llamar al método original y, tras ello, restaurar la corrección de compatibilidad (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;
Cómo controlar la simultaneidad de tipos de correcciones de compatibilidad (shim)
Los tipos de correcciones de compatibilidad (shim) funcionan en todos los subprocesos dentro de AppDomain y carecen afinidad de subproceso. Tener en cuenta esta propiedad es fundamental si tiene previsto usar un ejecutor de pruebas que admita la simultaneidad. Vale la pena señalar que no se pueden ejecutar simultáneamente pruebas donde se usan tipos de correcciones de compatibilidad (shim), aunque esta restricción no procede del runtime de Fakes.
Procesamiento de System.Environment con correcciones de compatibilidad (shim)
Si quiere procesar la clase System.Environment con correcciones de compatibilidad (shim), deberá realizar algunas modificaciones en el archivo mscorlib.fakes
. Agregue el siguiente contenido después del elemento Assembly:
<ShimGeneration>
<Add FullName="System.Environment"/>
</ShimGeneration>
Una vez que estos cambios se hayan realizado y la solución se haya recompilado, los métodos y las propiedades de la clase System.Environment
sí estarán disponibles para procesarse con correcciones de compatibilidad (shim). Este es un ejemplo de cómo asignar un comportamiento al método GetCommandLineArgsGet
:
System.Fakes.ShimEnvironment.GetCommandLineArgsGet = ...
Al realizar estas modificaciones, ha abierto la posibilidad de controlar y probar el modo en que el código interactúa con las variables de entorno del sistema, algo esencial para llevar a cabo pruebas unitarias completas.