Usare gli shim per isolare l'app per gli unit test
I tipi shim, una delle due tecnologie chiave usate da Microsoft Fakes Framework, sono fondamentali per isolare i componenti dell'app durante i test. Funzionano intercettando e deviando le chiamate a metodi specifici, che è quindi possibile indirizzare al codice personalizzato all'interno del test. Questa funzionalità consente di gestire il risultato di questi metodi, assicurandosi che i risultati siano coerenti e prevedibili durante ogni chiamata, indipendentemente dalle condizioni esterne. Questo livello di controllo semplifica il processo di test e aiuta a ottenere risultati più affidabili e accurati.
Usare gli shim quando è necessario creare un limite tra il codice e gli assembly che non fanno parte della soluzione. Quando l'obiettivo è isolare i componenti della soluzione l'uno dall'altro, è consigliabile usare stub .
Per una descrizione più dettagliata degli stub, vedere Usare gli stub per isolare le parti dell'applicazione l'una dall'altra per il testing unità.
Limitazioni degli shim
È importante notare che gli shim presentano limitazioni.
Gli shim non possono essere usati in tutti i tipi di determinate librerie nella classe di base .NET, in particolare mscorlib e system in .NET Framework e in System.Runtime in .NET Core o .NET 5+. Questo vincolo deve essere preso in considerazione durante la fase di pianificazione e progettazione dei test per garantire una strategia di test efficace ed efficace.
Creazione di uno shim: guida dettagliata
Si supponga che il componente contenga delle chiamate a System.IO.File.ReadAllLines
:
// Code under test:
this.Records = System.IO.File.ReadAllLines(path);
Creare una libreria di classi
Aprire Visual Studio e creare un
Class Library
progettoImpostare il nome del progetto
HexFileReader
Impostare il nome
ShimsTutorial
della soluzione .Impostare il framework di destinazione del progetto su .NET Framework 4.8
Eliminare il file predefinito
Class1.cs
Aggiungere un nuovo file
HexFile.cs
e aggiungere la definizione di classe seguente:
Creare un progetto di test
Fare clic con il pulsante destro del mouse sulla soluzione e aggiungere un nuovo progetto
MSTest Test Project
Impostare il nome del progetto
TestProject
Impostare il framework di destinazione del progetto su .NET Framework 4.8
Aggiungere l'assembly Fakes
Aggiungere un riferimento al progetto a
HexFileReader
Aggiungere l'assembly Fakes
In Esplora soluzioni,
Per un progetto .NET Framework precedente (stile non SDK), espandere il nodo Riferimenti del progetto di unit test.
Per un progetto in stile SDK destinato a .NET Framework, .NET Core o .NET 5+, espandere il nodo Dipendenze per trovare l'assembly che si vuole simulare in Assembly, Progetti o Pacchetti.
Se si usa Visual Basic, selezionare Mostra tutti i file nella barra degli strumenti Esplora soluzioni per visualizzare il nodo Riferimenti.
Selezionare l'assembly
System
contenente la definizione diSystem.IO.File.ReadAllLines
.Nel menu di scelta rapida selezionare Aggiungi assembly Fakes.
Poiché la compilazione genera alcuni avvisi ed errori perché non tutti i tipi possono essere usati con shim, è necessario modificare il contenuto di Fakes\mscorlib.fakes
per escluderli.
<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>
Creare uno unit test
Modificare il file
UnitTest1.cs
predefinito per aggiungere quanto segueTestMethod
[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); } }
Ecco il Esplora soluzioni che mostra tutti i file
Aprire Esplora test ed eseguire il test.
È fondamentale eliminare correttamente ogni contesto shim. Come regola generale, chiamare l'interno ShimsContext.Create
di un'istruzione using
per garantire una corretta cancellazione degli shim registrati. Ad esempio, si potrebbe registrare uno shim per un metodo di test che sostituisce il metodo DateTime.Now
con un delegato che restituisce sempre il primo gennaio 2000. Se si dimentica di cancellare lo shim registrato nel metodo di test, il resto dell'esecuzione del test restituirà sempre il primo gennaio 2000 come DateTime.Now
valore. Ciò potrebbe sorprendere e confondere.
Convenzioni di denominazione per le classi Shim
I nomi delle classi shim vengono creati aggiungendo un prefisso Fakes.Shim
al nome del tipo originale. I nomi dei parametri vengono aggiunti al nome del metodo. Non è necessario aggiungere riferimenti di assembly a System.Fakes.
System.IO.File.ReadAllLines(path);
System.IO.Fakes.ShimFile.ReadAllLinesString = (path) => new string[] { "Hello", "World", "Shims" };
Informazioni sul funzionamento degli shim
Gli shim operano introducendo deviazioni nella codebase dell'applicazione sottoposta a test. Ogni volta che viene eseguita una chiamata al metodo originale, il sistema Fakes interviene per reindirizzare tale chiamata, causando l'esecuzione del codice shim personalizzato anziché del metodo originale.
È importante notare che queste deviazioni vengono create e rimosse in modo dinamico in fase di esecuzione. Le deviazioni devono essere sempre create entro la durata di un oggetto ShimsContext
. Quando shimsContext viene eliminato, vengono rimossi anche tutti gli shim attivi creati all'interno di esso. Per gestirlo in modo efficiente, è consigliabile incapsulare la creazione di deviazioni all'interno di un'istruzione using
.
Shim per tipi di metodi differenti
Gli shim supportano vari tipi di metodi.
Metodi statici
Quando i metodi statici shimming, le proprietà che contengono shim vengono alloggiate all'interno di un tipo shim. Queste proprietà possiedono solo un setter, che viene usato per associare un delegato al metodo di destinazione. Ad esempio, se è disponibile una classe denominata MyClass
con un metodo MyMethod
statico :
//code under test
public static class MyClass {
public static int MyMethod() {
...
}
}
È possibile collegare uno shim a MyMethod
in modo che restituisca costantemente 5:
// unit test code
ShimMyClass.MyMethod = () => 5;
Metodi di istanza (per tutte le istanze)
Analogamente ai metodi statici, i metodi di istanza possono anche essere sottoposti a shim per tutte le istanze. Le proprietà che contengono questi shim vengono inserite in un tipo annidato denominato AllInstances per evitare confusione. Se è disponibile una classe MyClass
con un metodo MyMethod
di istanza :
// code under test
public class MyClass {
public int MyMethod() {
...
}
}
È possibile collegare uno shim a MyMethod
in modo che restituisca in modo coerente 5, indipendentemente dall'istanza:
// unit test code
ShimMyClass.AllInstances.MyMethod = () => 5;
La struttura del tipo generato di ShimMyClass
viene visualizzata nel modo seguente:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
public static class AllInstances {
public static Func<MyClass, int>MyMethod {
set {
...
}
}
}
}
In questo scenario Fakes passa l'istanza di runtime come primo argomento del delegato.
Metodi di istanza (Istanza di runtime singolo)
I metodi di istanza possono anche essere sottoposti a shim usando delegati diversi, a seconda del ricevitore della chiamata. In questo modo lo stesso metodo di istanza può presentare comportamenti diversi per ogni istanza del tipo. Le proprietà che contengono questi shim sono metodi di istanza del tipo shim stesso. Ogni tipo shim di cui è stata creata un'istanza è collegato a un'istanza non elaborata di un tipo sottoposto a shim.
Si consideri, ad esempio, una classe MyClass
con un metodo di istanza MyMethod
:
// code under test
public class MyClass {
public int MyMethod() {
...
}
}
È possibile creare due tipi shim per in MyMethod
modo che il primo restituisca costantemente 5 e il secondo restituisce 10 in modo coerente:
// unit test code
var myClass1 = new ShimMyClass()
{
MyMethod = () => 5
};
var myClass2 = new ShimMyClass { MyMethod = () => 10 };
La struttura del tipo generato di ShimMyClass
viene visualizzata nel modo seguente:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
public Func<int> MyMethod {
set {
...
}
}
public MyClass Instance {
get {
...
}
}
}
È possibile accedere all'istanza effettiva del tipo sottoposto a shim tramite la proprietà Instance:
// unit test code
var shim = new ShimMyClass();
var instance = shim.Instance;
Il tipo shim include anche una conversione implicita nel tipo sottoposto a shim, consentendo di usare direttamente il tipo shim:
// unit test code
var shim = new ShimMyClass();
MyClass instance = shim; // implicit cast retrieves the runtime instance
Costruttori
I costruttori non fanno eccezione a shimming; possono anche essere sottoposti a shim per associare tipi shim a oggetti che verranno creati in futuro. Ad esempio, ogni costruttore viene rappresentato come metodo statico, denominato Constructor
, all'interno del tipo shim. Si consideri una classe MyClass
con un costruttore che accetta un numero intero:
public class MyClass {
public MyClass(int value) {
this.Value = value;
}
...
}
È possibile configurare un tipo shim per il costruttore in modo che, indipendentemente dal valore passato al costruttore, ogni istanza futura restituirà -5 quando viene richiamato il getter Value:
// unit test code
ShimMyClass.ConstructorInt32 = (@this, value) => {
var shim = new ShimMyClass(@this) {
ValueGet = () => -5
};
};
Ogni tipo shim espone due tipi di costruttori. Il costruttore predefinito deve essere usato quando è necessaria una nuova istanza, mentre il costruttore che accetta un'istanza sottoposta a shim come argomento deve essere usato solo negli shim del costruttore:
// unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }
La struttura del tipo generato per ShimMyClass
può essere illustrata nel modo seguente:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass>
{
public static Action<MyClass, int> ConstructorInt32 {
set {
...
}
}
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }
...
}
Accesso ai membri di base
È possibile raggiungere le proprietà shim dei membri di base creando uno shim per il tipo di base e immettendo l'istanza figlio nel costruttore della classe shim di base.
Si consideri, ad esempio, una classe MyBase
con un metodo MyMethod
di istanza e un sottotipo MyChild
:
public abstract class MyBase {
public int MyMethod() {
...
}
}
public class MyChild : MyBase {
}
Uno shim di MyBase
può essere configurato avviando un nuovo ShimMyBase
shim:
// unit test code
var child = new ShimMyChild();
new ShimMyBase(child) { MyMethod = () => 5 };
È importante notare che quando viene passato come parametro al costruttore shim di base, il tipo shim figlio viene convertito in modo implicito nell'istanza figlio.
La struttura del tipo generato per ShimMyChild
e ShimMyBase
può essere simile al codice seguente:
// 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 { ... } }
}
Costruttori statici
I tipi shim espongono un metodo statico StaticConstructor
per sottoporre a shim il costruttore statico di un tipo. Poiché i costruttori statici vengono eseguiti una sola volta, è necessario assicurarsi che lo shim sia configurato prima di accedere a qualsiasi membro del tipo.
Finalizzatori
I finalizzatori non sono supportati in Fakes.
Metodi privati
Il generatore di codice Fakes crea le proprietà dello shim per i metodi privati che hanno solo tipi visibili nella firma, ovvero i tipi di parametri e il tipo restituito visibile.
Interfacce di binding
Quando un tipo sottoposto a shim implementa un'interfaccia, il generatore di codice genera un metodo che consente di associare contemporaneamente tutti i membri di tale interfaccia.
Si consideri, ad esempio, una classe MyClass
che implementa IEnumerable<int>
:
public class MyClass : IEnumerable<int> {
public IEnumerator<int> GetEnumerator() {
...
}
...
}
È possibile eseguire lo shim delle implementazioni di IEnumerable<int>
in MyClass chiamando il metodo Bind:
// unit test code
var shimMyClass = new ShimMyClass();
shimMyClass.Bind(new List<int> { 1, 2, 3 });
La struttura del tipo generato di ShimMyClass
è simile al codice seguente:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
public ShimMyClass Bind(IEnumerable<int> target) {
...
}
}
Modifica comportamento predefinito
Ogni tipo shim generato include un'istanza dell'interfaccia IShimBehavior
accessibile tramite la ShimBase<T>.InstanceBehavior
proprietà . Questo comportamento viene richiamato ogni volta che un client chiama un membro dell'istanza che non è stato sottoposto a shim in modo esplicito.
Per impostazione predefinita, se non è stato impostato alcun comportamento specifico, usa l'istanza restituita dalla proprietà statica ShimBehaviors.Current
, che in genere genera un'eccezione NotImplementedException
.
È possibile modificare questo comportamento in qualsiasi momento modificando la InstanceBehavior
proprietà per qualsiasi istanza shim. Ad esempio, il frammento di codice seguente modifica il comportamento in modo da non eseguire alcuna operazione o restituire il valore predefinito del tipo restituito, default(T)
ad esempio :
// unit test code
var shim = new ShimMyClass();
//return default(T) or do nothing
shim.InstanceBehavior = ShimBehaviors.DefaultValue;
È anche possibile modificare a livello globale il comportamento per tutte le istanze di shim, in cui la InstanceBehavior
proprietà non è stata definita in modo esplicito, impostando la proprietà statica ShimBehaviors.Current
:
// unit test code
// change default shim for all shim instances where the behavior has not been set
ShimBehaviors.Current = ShimBehaviors.DefaultValue;
Identificazione delle interazioni con dipendenze esterne
Per identificare quando il codice interagisce con sistemi esterni o dipendenze (detto environment
), è possibile usare gli shim per assegnare un comportamento specifico a tutti i membri di un tipo. Sono inclusi i metodi statici. Impostando il ShimBehaviors.NotImplemented
comportamento sulla proprietà statica Behavior
del tipo shim, qualsiasi accesso a un membro di quel tipo che non è stato sottoposto esplicitamente a shim genererà un'eccezione NotImplementedException
. Questo può fungere da segnale utile durante il test, a indicare che il codice sta tentando di accedere a un sistema esterno o a una dipendenza.
Ecco un esempio di come configurare questa opzione nel codice di unit test:
// unit test code
// Assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.Behavior = ShimBehaviors.NotImplemented;
Per praticità, viene fornito anche un metodo abbreviato per ottenere lo stesso effetto:
// Shorthand to assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.BehaveAsNotImplemented();
Richiamo dei metodi originali dai metodi shim
Potrebbero essere presenti scenari in cui potrebbe essere necessario eseguire il metodo originale durante l'esecuzione del metodo shim. Ad esempio, è possibile scrivere testo nel file system dopo aver convalidato il nome file passato al metodo .
Un approccio per gestire questa situazione consiste nell'incapsulare una chiamata al metodo originale usando un delegato e ShimsContext.ExecuteWithoutShims()
, come illustrato nel codice seguente:
// unit test code
ShimFile.WriteAllTextStringString = (fileName, content) => {
ShimsContext.ExecuteWithoutShims(() => {
Console.WriteLine("enter");
File.WriteAllText(fileName, content);
Console.WriteLine("leave");
});
};
In alternativa, è possibile annullare lo shim, chiamare il metodo originale e quindi ripristinare lo 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;
Gestione della concorrenza con tipi shim
I tipi shim operano in tutti i thread all'interno di AppDomain e non possiedono l'affinità di thread. Questa proprietà è fondamentale da tenere presente se si prevede di usare un runner di test che supporta la concorrenza. Vale la pena notare che i test che coinvolgono tipi shim non possono essere eseguiti simultaneamente, anche se questa restrizione non viene applicata dal runtime Fakes.
Shimming System.Environment
Se vuoi eseguire lo shim della System.Environment classe, dovrai apportare alcune modifiche al mscorlib.fakes
file. Dopo l'elemento Assembly aggiungere il contenuto seguente:
<ShimGeneration>
<Add FullName="System.Environment"/>
</ShimGeneration>
Dopo aver apportato queste modifiche e ricompilato la soluzione, i metodi e le proprietà nella System.Environment
classe sono ora disponibili per essere sottoposti a shim. Ecco un esempio di come assegnare un comportamento al GetCommandLineArgsGet
metodo :
System.Fakes.ShimEnvironment.GetCommandLineArgsGet = ...
Apportando queste modifiche, è stata aperta la possibilità di controllare e testare il modo in cui il codice interagisce con le variabili di ambiente di sistema, uno strumento essenziale per unit test completi.