Condividi tramite


Usare gli shim per isolare l'app per gli unit test

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

  1. Aprire Visual Studio e creare un Class Library progetto

    Screenshot del progetto Libreria di classi NetFramework in Visual Studio.

  2. Impostare il nome del progetto HexFileReader

  3. Impostare il nome della soluzione ShimsTutorial.

  4. Impostare il framework di destinazione del progetto su .NET Framework 4.8

  5. Eliminare il file predefinito Class1.cs

  6. Aggiungere un nuovo file HexFile.cs e aggiungere la definizione di classe seguente:

    // HexFile.cs
    public class HexFile
    {
        public string[] Records { get; private set; }
    
        public HexFile(string path)
        {
            this.Records = System.IO.File.ReadAllLines(path);
        }
    }
    

Creare un progetto di test

  1. Fare clic con il pulsante destro del mouse sulla soluzione e aggiungere un nuovo progetto MSTest Test Project

  2. Impostare il nome del progetto TestProject

  3. Impostare il framework di destinazione del progetto su .NET Framework 4.8

    Screenshot del progetto NetFramework Test in Visual Studio.

aggiungere l'assembly Fakes

  1. Aggiungere un riferimento al progetto a HexFileReader

    Screenshot del comando Aggiungi riferimento al progetto.

  2. 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 di System.IO.File.ReadAllLines.

    • Nel menu di scelta rapida selezionare Aggiungi Fakes Assembly.

    Screenshot del comando

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

  1. Modificare il file UnitTest1.cs predefinito per aggiungere quanto segue TestMethod

    [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

    Schermata di Esplora Soluzioni che mostra tutti i file.

  2. 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 MyMethodstatico :

//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.