Dela via


Använda shims för att isolera din app för enhetstestning

Shim-typer, en av de två viktiga teknikerna som används av Microsoft Fakes Framework, bidrar till att isolera komponenterna i din app under testningen. De fungerar genom att fånga upp och omdirigera anrop till specifika metoder, som du sedan kan dirigera till anpassad kod i testet. Med den här funktionen kan du hantera resultatet av dessa metoder, vilket säkerställer att resultaten är konsekventa och förutsägbara under varje anrop, oavsett externa villkor. Den här kontrollnivån effektiviserar testningsprocessen och hjälper dig att uppnå mer tillförlitliga och korrekta resultat.

Använd shims när du behöver skapa en gräns mellan din kod och sammansättningar som inte ingår i din lösning. När syftet är att isolera komponenter i din lösning från varandra, rekommenderas användning av stubs .

(En mer detaljerad beskrivning av stubs finns i Använda stubs för att isolera delar av ditt program från varandra för enhetstestning.)

Shimsbegränsningar

Det är viktigt att notera att shims har sina begränsningar.

Shims kan inte användas på alla typer från vissa bibliotek i .NET-basklassen, särskilt mscorlib och System i .NET Framework och i System.Runtime i .NET Core eller .NET 5+. Den här begränsningen bör beaktas under testplanerings- och designfasen för att säkerställa en lyckad och effektiv teststrategi.

Skapa en shim: En steg-för-steg-guide

Anta att komponenten innehåller anrop till System.IO.File.ReadAllLines:

// Code under test:
this.Records = System.IO.File.ReadAllLines(path);

Skapa ett klassbibliotek

  1. Öppna Visual Studio och skapa ett Class Library projekt

    Skärmbild av netframework-klassbiblioteksprojektet i Visual Studio.

  2. Ange projektnamn HexFileReader

  3. Ange lösningsnamnet ShimsTutorial.

  4. Ange projektets målramverk till .NET Framework 4.8

  5. Ta bort standardfilen Class1.cs

  6. Lägg till en ny fil HexFile.cs och lägg till följande klassdefinition:

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

Skapa ett testprojekt

  1. Högerklicka på lösningen och lägg till ett nytt projekt MSTest Test Project

  2. Ange projektnamn TestProject

  3. Ange projektets målramverk till .NET Framework 4.8

    Skärmbild av NetFramework Test-projektet i Visual Studio.

Lägg till Fakes Assembly

  1. Lägga till en projektreferens till HexFileReader

    Skärmbild av kommandot Lägg till projektreferens.

  2. Lägg till förfalskningssammansättning

    • I Solution Explorer,

      • För ett äldre .NET Framework-projekt (icke-SDK-format) expanderar du noden Referenser för ditt enhetstestprojekt.

      • För ett SDK-projekt med inriktning på .NET Framework, .NET Core eller .NET 5+, expanderar du noden Beroenden för att hitta den sammansättning som du vill förfalska under Sammansättningar, Projekt eller Paket.

      • Om du arbetar i Visual Basic väljer du Visa alla filer i verktygsfältet i Solution Explorer för att se noden Referenser .

    • Välj den sammansättning System som innehåller definitionen av System.IO.File.ReadAllLines.

    • På snabbmenyn väljer du Lägg till Fakes Assembly.

    Skärmbild av kommandot Lägg till förfalskningssammansättning.

Eftersom kompilering resulterar i vissa varningar och fel eftersom inte alla typer kan användas med shims, måste du ändra innehållet i Fakes\mscorlib.fakes för att exkludera dem.

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

Skapa ett enhetstest

  1. Ändra standardfilen UnitTest1.cs för att lägga till följande 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);
        }
    }
    

    Här är Solution Explorer som visar alla filer

    Skärmbild av Solution Explorer som visar alla filer.

  2. Öppna Test Explorer och kör testet.

Det är viktigt att hantera varje shim-kontext korrekt. Som en tumregel bör du anropa ShimsContext.Create inuti en using-instruktion för att säkerställa korrekt rensning av de registrerade shims. Du kan till exempel registrera en shim för en testmetod som ersätter den DateTime.Now metoden med en delegering som alltid returnerar den första januari 2000. Om du glömmer att rensa den registrerade shim i testmetoden kommer resten av testkörningen alltid att returnera den första januari 2000 som DateTime.Now-värde. Detta kan vara överraskande och förvirrande.


Namngivningskonventioner för Shim-klasser

Shim-klassnamn skapas genom att ett prefix, representerat som Fakes.Shim, läggs till det ursprungliga typnamnet. Parameternamn läggs till i metodnamnet. (Du behöver inte lägga till någon sammansättningsreferens till System.Fakes.)

    System.IO.File.ReadAllLines(path);
    System.IO.Fakes.ShimFile.ReadAllLinesString = (path) => new string[] { "Hello", "World", "Shims" };

Förstå hur Shims fungerar

Shims fungerar genom att introducera omvägar i kodbasen för programmet som testas. När ett anrop till den ursprungliga metoden görs ingriper Fakes-systemet för att omdirigera detta, så att den anpassade shim-koden körs istället för den ursprungliga metoden.

Det är viktigt att observera att dessa omvägar skapas och tas bort dynamiskt vid körning. Omvägar bör alltid skapas under en ShimsContexts livslängd. När ShimsContext tas bort tas även alla aktiva shims som skapades i den bort. För att hantera detta effektivt rekommenderar vi att du kapslar in skapandet av omvägar i en using -instruktion.


Shims för olika typer av metoder

Shims stöder olika typer av metoder.

Statiska metoder

När statiska metoder används finns egenskaper som innehåller shims inom en shim-typ. Dessa egenskaper har endast en setter, som används för att koppla en delegate till målmetoden. Om vi till exempel har en klass som heter MyClass med en statisk metod MyMethod:

//code under test
public static class MyClass {
    public static int MyMethod() {
        ...
    }
}

Vi kan koppla en shim till MyMethod så att den ständigt returnerar 5:

// unit test code
ShimMyClass.MyMethod = () => 5;

Instansmetoder (för alla instanser)

Precis som med statiska metoder kan instansmetoder också justeras för alla instanser. Egenskaperna som innehåller dessa shims placeras i en inbäddad typ med namnet AllInstances för att undvika förväxling. Om vi har en klass MyClass med en instansmetod MyMethod:

// code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

Vi kan koppla en shim till MyMethod så att den konsekvent returnerar 5, oavsett instans:

// unit test code
ShimMyClass.AllInstances.MyMethod = () => 5;

Den genererade typstrukturen ShimMyClass för visas på följande sätt:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public static class AllInstances {
        public static Func<MyClass, int>MyMethod {
            set {
                ...
            }
        }
    }
}

I det här scenariot skickar Fakes körningsinstansen som det första argumentet för delegaten.

Instansmetoder (enkel körningsinstans)

Instansmetoder kan också användas med olika ombud, beroende på samtalets mottagare. På så sätt kan samma instansmetod uppvisa olika beteenden per instans av typen. Egenskaperna som innehåller dessa shims är instansmetoder av själva shim-typen. Varje instansierad shim-typ är länkad till en rå instans av en shimad typ.

Till exempel med en klass MyClass med en instansmetod MyMethod:

// code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

Vi kan skapa två shimtyper för MyMethod så att den första konsekvent returnerar 5 och den andra konsekvent returnerar 10.

// unit test code
var myClass1 = new ShimMyClass()
{
    MyMethod = () => 5
};
var myClass2 = new ShimMyClass { MyMethod = () => 10 };

Den genererade typstrukturen ShimMyClass för visas på följande sätt:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public Func<int> MyMethod {
        set {
            ...
        }
    }
    public MyClass Instance {
        get {
            ...
        }
    }
}

Den faktiska instansen av typen shimmed kan nås via instansegenskapen:

// unit test code
var shim = new ShimMyClass();
var instance = shim.Instance;

Shim-typen innehåller också en implicit konvertering till den shimmade typen, så att du kan använda shim-typen direkt.

// unit test code
var shim = new ShimMyClass();
MyClass instance = shim; // implicit cast retrieves the runtime instance

Konstruktörer

Konstruktorer är inget undantag från shimming; de kan också justeras för att bifoga shims av typer till objekt som kommer att skapas i framtiden. Till exempel representeras varje konstruktor som en statisk metod med namnet Constructor, inom shim-typen. Nu ska vi överväga en klass MyClass med en konstruktor som accepterar ett heltal:

public class MyClass {
    public MyClass(int value) {
        this.Value = value;
    }
    ...
}

En shim-typ för konstruktorn kan konfigureras så att varje framtida instans, oavsett vilket värde som skickas till konstruktorn, returnerar -5 när värde getter anropas:

// unit test code
ShimMyClass.ConstructorInt32 = (@this, value) => {
    var shim = new ShimMyClass(@this) {
        ValueGet = () => -5
    };
};

Varje shim-typ exponerar två typer av konstruktorer. Standardkonstruktorn ska användas när en ny instans behövs, medan konstruktorn som tar en shimmed-instans som argument endast ska användas i konstruktor-shims:

// unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }

Strukturen för den genererade typen för ShimMyClass kan illustreras på följande sätt:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass>
{
    public static Action<MyClass, int> ConstructorInt32 {
        set {
            ...
        }
    }

    public ShimMyClass() { }
    public ShimMyClass(MyClass instance) : base(instance) { }
    ...
}

Åtkomst till basmedlemmar

Shim-egenskaper för basmedlemmar kan nås genom att skapa en shim för bastypen och mata in den underordnade instansen i konstruktorn för bas-shim-klassen.

Överväg till exempel en klass MyBase med en instansmetod MyMethod och en undertyp MyChild:

public abstract class MyBase {
    public int MyMethod() {
        ...
    }
}

public class MyChild : MyBase {
}

En shim av MyBase kan konfigureras genom att initiera en ny ShimMyBase shim:

// unit test code
var child = new ShimMyChild();
new ShimMyBase(child) { MyMethod = () => 5 };

Observera att när den överförs som en parameter till bas shim-konstruktorn omvandlas barnshim-typen implicit till barninstansen.

Strukturen för den genererade typen för ShimMyChild och ShimMyBase kan liknas vid följande kod:

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

Statiska konstruktorer

Shim-typer exponerar en statisk metod StaticConstructor för att shimma den statiska konstruktorn av en typ. Eftersom statiska konstruktorer endast körs en gång måste du se till att shimmen har konfigurerats innan någon medlem av typen används.

Finalisatorer

Finalizers stöds inte i Fakes.

Privata metoder

Kodgeneratorn Fakes skapar shim-egenskaper för privata metoder som bara har synliga typer i signaturen, dvs. både parametertyper och returtyp är synliga.

Bindningsgränssnitt

När en shimmed-typ implementerar ett gränssnitt genererar kodgeneratorn en metod som gör att den kan binda alla medlemmar från gränssnittet samtidigt.

Till exempel med en klass MyClass som implementerar IEnumerable<int>:

public class MyClass : IEnumerable<int> {
    public IEnumerator<int> GetEnumerator() {
        ...
    }
    ...
}

Du kan shimma implementeringarna av IEnumerable<int> i MyClass genom att anropa metoden Bind:

// unit test code
var shimMyClass = new ShimMyClass();
shimMyClass.Bind(new List<int> { 1, 2, 3 });

Den genererade typstrukturen ShimMyClass liknar följande kod:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public ShimMyClass Bind(IEnumerable<int> target) {
        ...
    }
}

Ändra standardbeteende

Varje genererad shim-typ innehåller en instans av IShimBehavior gränssnittet som är tillgänglig via egenskapen ShimBase<T>.InstanceBehavior . Det här beteendet anropas när en klient anropar en instansmedlem som inte uttryckligen har patchats.

Om inget specifikt beteende har angetts använder den som standard den instans som returneras av den statiska ShimBehaviors.Current egenskapen, vilket vanligtvis utlöser ett NotImplementedException undantag.

Du kan ändra det här beteendet när som helst genom att justera InstanceBehavior egenskapen för någon shim-instans. Följande kodfragment ändrar till exempel beteendet för att antingen inte göra någonting eller returnera standardvärdet för returtypen, default(T)dvs. :

// unit test code
var shim = new ShimMyClass();
//return default(T) or do nothing
shim.InstanceBehavior = ShimBehaviors.DefaultValue;

Du kan också ändra beteendet globalt för alla shimmed-instanser, där egenskapen InstanceBehavior inte har definierats uttryckligen, genom att ange den statiska ShimBehaviors.Current egenskapen:

// unit test code
// change default shim for all shim instances where the behavior has not been set
ShimBehaviors.Current = ShimBehaviors.DefaultValue;

Identifiera interaktioner med externa beroenden

För att identifiera när koden interagerar med externa system eller beroenden (kallas för environment), kan du använda shims för att tilldela ett specifikt beteende till alla medlemmar av en typ. Detta omfattar statiska metoder. Genom att ange ShimBehaviors.NotImplemented-beteendet för den statiska Behavior-egenskapen hos shim-typen, kommer all åtkomst till en medlem av den typen som inte uttryckligen har shimats att generera en NotImplementedException. Detta kan fungera som en användbar signal under testningen, vilket indikerar att koden försöker komma åt ett externt system eller beroende.

Här är ett exempel på hur du konfigurerar detta i enhetstestkoden:

// unit test code
// Assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.Behavior = ShimBehaviors.NotImplemented;

För enkelhetens skull tillhandahålls också en kortfattad metod för att uppnå samma effekt:

// Shorthand to assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.BehaveAsNotImplemented();

Anropa ursprungliga metoder inifrån Shim-metoder

Det kan finnas scenarier där du kan behöva köra den ursprungliga metoden under körningen av shim-metoden. Du kanske till exempel vill skriva text till filsystemet när du har verifierat filnamnet som skickades till metoden.

En metod för att hantera den här situationen är att kapsla in ett anrop till den ursprungliga metoden med hjälp av ett ombud och ShimsContext.ExecuteWithoutShims(), vilket visas i följande kod:

// unit test code
ShimFile.WriteAllTextStringString = (fileName, content) => {
  ShimsContext.ExecuteWithoutShims(() => {

      Console.WriteLine("enter");
      File.WriteAllText(fileName, content);
      Console.WriteLine("leave");
  });
};

Alternativt kan du nullifiera shimen, anropa den ursprungliga metoden och sedan återställa shimen.

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

Hantera samtidighet med Shim-typer

Shim-typer fungerar i alla trådar i AppDomain och har inte trådtillhörighet. Den här egenskapen är viktig att tänka på om du planerar att använda en testlöpare som stöder samtidighet. Det är värt att notera att tester som involverar shimtyper inte kan köras samtidigt, trots att denna begränsning inte tillämpas av Fakes runtime-miljön.

Shimming System.Environment

Om du vill använda en shim för System.Environment-klassen måste du göra vissa ändringar i mscorlib.fakes-filen. Lägg till följande innehåll efter sammansättningselementet:

<ShimGeneration>
    <Add FullName="System.Environment"/>
</ShimGeneration>

När du har gjort dessa ändringar och återskapat lösningen är metoderna och egenskaperna i System.Environment klassen nu tillgängliga för shimmed. Här är ett exempel på hur du kan tilldela metoden ett beteende GetCommandLineArgsGet :

System.Fakes.ShimEnvironment.GetCommandLineArgsGet = ...

Genom att göra dessa ändringar har du öppnat möjligheten att styra och testa hur koden interagerar med systemmiljövariabler, ett viktigt verktyg för omfattande enhetstestning.