Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
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
Öppna Visual Studio och skapa ett
Class Library
projektAnge projektnamn
HexFileReader
Ange lösningsnamnet
ShimsTutorial
.Ange projektets målramverk till .NET Framework 4.8
Ta bort standardfilen
Class1.cs
Lägg till en ny fil
HexFile.cs
och lägg till följande klassdefinition:
Skapa ett testprojekt
Högerklicka på lösningen och lägg till ett nytt projekt
MSTest Test Project
Ange projektnamn
TestProject
Ange projektets målramverk till .NET Framework 4.8
Lägg till Fakes Assembly
Lägga till en projektreferens till
HexFileReader
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 avSystem.IO.File.ReadAllLines
.På snabbmenyn väljer du Lägg till Fakes Assembly.
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
Ändra standardfilen
UnitTest1.cs
för att lägga till följandeTestMethod
[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
Ö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 ShimsContext
s 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.