Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
I tipi shim, una delle due tecnologie chiave usate dal Microsoft Fakes Framework, sono fondamentali per isolare i componenti dell'app durante il processo di 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 Utilizzare gli stub per isolare le parti dell'applicazione l'una dall'altra per il testing unitario.
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 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 della soluzione
ShimsTutorial
.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 di 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 Fakes Assembly.
Poiché la compilazione genera alcuni avvisi ed errori perché non tutti i tipi possono essere usati con shims, dovrai 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 un 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 Solution Explorer che mostra tutti i file
Aprire Esplora test ed eseguire il test.
È fondamentale smaltire correttamente ogni contesto shim. Come regola generale, chiamare ShimsContext.Create
all'interno di un'istruzione using
per garantire una corretta rimozione degli shim registrati. Ad esempio, è possibile registrare uno shim per un metodo di test che sostituisce il DateTime.Now
metodo 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. Questo potrebbe essere sorprendente e confuso.
Convenzioni di denominazione per le classi Shim
I nomi delle classi shim vengono creati aggiungendo il prefisso Fakes.Shim
al nome del tipo originale. I nomi dei parametri vengono aggiunti al nome del metodo. Non è necessario aggiungere alcun riferimento all'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
.
Spessori per diversi tipi di metodologie
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 metodo setter, che viene utilizzato per assegnare un delegato al metodo mirato. 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 adattati per tutte le istanze. Le proprietà che contengono questi shim vengono inserite in un tipo annidato denominato AllInstances per evitare confusione. Se abbiamo 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 delegate.
Metodi di istanza (Istanza di singolo ambiente runtime)
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 d'istanza del tipo di shim stesso. Ogni tipo di shim istanziato è collegato a un'istanza grezza di una tipologia sottoposta a shim.
Ad esempio, data una classe MyClass
con un metodo di istanza MyMethod
:
// code under test
public class MyClass {
public int MyMethod() {
...
}
}
È possibile creare due tipi di shim per MyMethod
in modo che il primo restituisca costantemente 5 e il secondo restituisca costantemente 10.
// 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 verso il tipo soggetto 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 restituisca -5 quando viene richiamato il getter Value:
// unit test code
ShimMyClass.ConstructorInt32 = (@this, value) => {
var shim = new ShimMyClass(@this) {
ValueGet = () => -5
};
};
Ogni tipo di intercalare espone due tipi di costruttori. Il costruttore predefinito deve essere usato quando è necessaria una nuova istanza, mentre il costruttore che accetta un'istanza schermata 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 accedere alle proprietà shim dei membri base creando uno shim per il tipo di base e passando l'istanza figlia al 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 {
}
È possibile configurare uno shim di MyBase
avviando un nuovo shim ShimMyBase
.
// 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 StaticConstructor
statico per effettuare lo shim del costruttore statico di un tipo. Poiché i costruttori statici vengono eseguiti solo una volta, è necessario assicurarsi che il shim sia configurato prima che qualsiasi membro del tipo venga accesso.
Finalizzatori
I finalizzatori non sono supportati all'interno di Fakes.
Metodi privati
Il generatore di codice Fakes crea proprietà shim per i metodi privati che hanno nella firma solo tipi visibili, cioè tipi di parametro e tipo di ritorno.
Interfacce di binding
Quando un tipo sottoposto a shim implementa un'interfaccia, il generatore di codice emette un metodo che permette di collegare tutti i membri di quell'interfaccia in una sola volta.
Ad esempio, data 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 di shim generato include un'istanza dell'interfaccia IShimBehavior
accessibile tramite la proprietà ShimBase<T>.InstanceBehavior
. Questo comportamento viene richiamato ogni volta che un client chiama un membro dell'istanza che non è stato esplicitamente modificato con shim.
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 proprietà InstanceBehavior
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 di tutte le istanze con shim, in cui la proprietà InstanceBehavior
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 comportamento ShimBehaviors.NotImplemented
sulla proprietà statica Behavior
del tipo shim, qualsiasi accesso a un membro di quel tipo che non è stato esplicitamente sottoposto 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 disattivare lo shim, chiamare il metodo originale e poi 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 di shim operano attraverso tutti i thread all'interno dell'AppDomain e non sono legati a nessun thread in particolare. Questa proprietà è importante da tenere a mente se si intende usare un runner di test che supporta la concorrenza. È importante notare che i test che coinvolgono tipi shim non possono essere eseguiti simultaneamente, anche se questa restrizione non viene applicata dal runtime Fakes.
Inserimento di codici di sistema in 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à della classe System.Environment
sono ora disponibili per essere shimati. 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.