Izolace aplikace pro testování jednotek pomocí překrytí
Typy shim, jedna ze dvou klíčových technologií využívaných rozhraním Microsoft Fakes Framework, jsou instrumentální při izolování součástí vaší aplikace během testování. Fungují tak, že zachytí a odpojí volání na konkrétní metody, které pak můžete směrovat na vlastní kód v rámci testu. Tato funkce umožňuje spravovat výsledky těchto metod a zajistit tak konzistentní a předvídatelné výsledky během každého volání bez ohledu na vnější podmínky. Tato úroveň kontroly zjednodušuje proces testování a pomáhá dosáhnout spolehlivějších a přesnějších výsledků.
Používejte překrytí , když potřebujete vytvořit hranici mezi kódem a sestaveními, které nejsou součástí vašeho řešení. Pokud je cílem izolovat komponenty vašeho řešení od sebe navzájem, doporučuje se použití zástupných procedur .
(Podrobnější popis zástupné procedury najdete v tématu K izolaci částí aplikace pro testování jednotek použijte zástupné procedury.)
Omezení přechádků
Je důležité si uvědomit, že shimy mají svá omezení.
Shimy nelze použít pro všechny typy z určitých knihoven v základní třídě .NET, konkrétně mscorlib a System v rozhraní .NET Framework a v system.RUNTIME v .NET Core nebo .NET 5 nebo novější. Toto omezení je třeba vzít v úvahu během fáze plánování a návrhu testů, aby byla zajištěna úspěšná a účinná strategie testování.
Vytvoření šimů: Podrobný průvodce
Předpokládejme, že vaše komponenta obsahuje volání:System.IO.File.ReadAllLines
// Code under test:
this.Records = System.IO.File.ReadAllLines(path);
Vytvoření knihovny tříd
Otevření sady Visual Studio a vytvoření
Class Library
projektuNastavení názvu projektu
HexFileReader
Nastavte název
ShimsTutorial
řešení .Nastavení cílové architektury projektu na .NET Framework 4.8
Odstranění výchozího souboru
Class1.cs
Přidejte nový soubor
HexFile.cs
a přidejte následující definici třídy:
Vytvoření testovacího projektu
Klikněte pravým tlačítkem na řešení a přidejte nový projekt.
MSTest Test Project
Nastavení názvu projektu
TestProject
Nastavení cílové architektury projektu na .NET Framework 4.8
Přidat sestavení fakes
Přidání odkazu na projekt
HexFileReader
Přidat sestavení fakes
V Průzkumník řešení,
Pro starší projekt rozhraní .NET Framework (styl bez sady SDK) rozbalte uzel Reference projektu testů jednotek.
V případě projektu ve stylu sady SDK, který cílí na rozhraní .NET Framework, .NET Core nebo .NET 5 nebo novější, rozbalte uzel Závislosti a vyhledejte sestavení, které chcete zfalšovat v rámci sestavení, projektů nebo balíčků.
Pokud pracujete v jazyce Visual Basic, vyberte Zobrazit všechny soubory na panelu nástrojů Průzkumník řešení a zobrazte uzel Reference.
Vyberte sestavení
System
, které obsahuje definici .System.IO.File.ReadAllLines
V místní nabídce vyberte Přidat falešné sestavení.
Vzhledem k tomu, že sestavení vede k některým upozorněním a chybám, protože se ne všechny typy dají použít s přešívanými daty, budete muset upravit obsah Fakes\mscorlib.fakes
, který je vyloučí.
<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>
Vytvoření testu jednotek
Upravte výchozí soubor
UnitTest1.cs
tak, aby přidal následující: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); } }
Tady je Průzkumník řešení zobrazující všechny soubory.
Otevřete Průzkumníka testů a spusťte test.
Je důležité správně vyhodit každý kontext přecházení. Zpravidla zavolejte ShimsContext.Create
vnitřní část using
příkazu, aby se zajistilo správné vymazání registrovaných shimů. Můžete například zaregistrovat překrytí pro testovací metodu, která nahradí DateTime.Now
metodu delegátem, který vždy vrátí první leden 2000. Pokud zapomenete vymazat zaregistrované převrácení v testovací metodě, zbytek testovacího spuštění by vždy vrátil první leden 2000 jako DateTime.Now
hodnotu. To může být překvapivý a matoucí.
Konvence vytváření názvů pro třídy shim
Názvy tříd shim se skládají předponou Fakes.Shim
původního názvu typu. Názvy parametrů jsou připojeny k názvu metody. (Nemusíte přidávat žádné odkazy na sestavení System.Fakes.)
System.IO.File.ReadAllLines(path);
System.IO.Fakes.ShimFile.ReadAllLinesString = (path) => new string[] { "Hello", "World", "Shims" };
Principy fungování shims
Shims funguje tak, že do základu kódu testované aplikace zavádí objížďky . Kdykoli dojde k volání původní metody, systém Fakes zasáhne přesměrování tohoto volání, což způsobí spuštění vlastního kódu shim místo původní metody.
Je důležité si uvědomit, že tyto obchádky se vytvářejí a odebírají dynamicky za běhu. Objížďky by měly být vždy vytvořeny v rámci životnosti ShimsContext
. Když je shimsContext odstraněn, všechny aktivní shimy, které byly vytvořeny v něm, jsou také odebrány. Pro efektivní správu je doporučeno zapouzdřovat vytváření objížďek v rámci using
příkazu.
Shimy pro různé druhy metod
Shims podporuje různé typy metod.
Statické metody
Při přešívání statických metod jsou vlastnosti, které obsahují shimy, umístěny v typu shim. Tyto vlastnosti mají pouze setter, který se používá k připojení delegáta k cílové metodě. Pokud například máme třídu volanou MyClass
statickou metodou MyMethod
:
//code under test
public static class MyClass {
public static int MyMethod() {
...
}
}
Překrytí můžeme připojit tak MyMethod
, aby neustále vracela hodnotu 5:
// unit test code
ShimMyClass.MyMethod = () => 5;
Metody instancí (pro všechny instance)
Stejně jako statické metody lze metody instancí také přešít pro všechny instance. Vlastnosti, které obsahují tyto shimy, se umístí do vnořeného typu s názvem AllInstances, aby se zabránilo nejasnostem. Pokud máme třídu MyClass
s metodou MyMethod
instance:
// code under test
public class MyClass {
public int MyMethod() {
...
}
}
Můžeme připojit překrytí tak MyMethod
, aby konzistentně vrátil 5 bez ohledu na instanci:
// unit test code
ShimMyClass.AllInstances.MyMethod = () => 5;
Vygenerovaná struktura ShimMyClass
typu by se zobrazila takto:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
public static class AllInstances {
public static Func<MyClass, int>MyMethod {
set {
...
}
}
}
}
V tomto scénáři fakes předá instanci modulu runtime jako první argument delegáta.
Metody instance (instance s jedním modulem runtime)
Metody instance lze také přešívat pomocí různých delegátů v závislosti na příjemce hovoru. To umožňuje stejné metodě instance vykazovat různé chování na instanci typu. Vlastnosti, které obsahují tyto shimy, jsou metody instance samotného typu shim. Každý typ vytvoření instance shim je propojený s nezpracovanou instancí přešikovaného typu.
Například zadanou třídu MyClass
s metodou MyMethod
instance:
// code under test
public class MyClass {
public int MyMethod() {
...
}
}
Můžeme vytvořit dva typy překrytí, MyMethod
aby první konzistentně vrátil hodnotu 5 a druhý konzistentně vrátil hodnotu 10:
// unit test code
var myClass1 = new ShimMyClass()
{
MyMethod = () => 5
};
var myClass2 = new ShimMyClass { MyMethod = () => 10 };
Vygenerovaná struktura ShimMyClass
typu by se zobrazila takto:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
public Func<int> MyMethod {
set {
...
}
}
public MyClass Instance {
get {
...
}
}
}
Skutečná instance typu shimmed je přístupná prostřednictvím vlastnosti Instance:
// unit test code
var shim = new ShimMyClass();
var instance = shim.Instance;
Typ shim zahrnuje také implicitní převod na přešikovaný typ, který umožňuje použít přímo typ shim:
// unit test code
var shim = new ShimMyClass();
MyClass instance = shim; // implicit cast retrieves the runtime instance
Konstruktory
Konstruktory nejsou výjimkou shimming; Dají se také přešít a připojit typy shim k objektům, které budou vytvořeny v budoucnu. Například každý konstruktor je reprezentován jako statická metoda s názvem Constructor
, v rámci typu shim. Představme si třídu MyClass
s konstruktorem, který přijímá celé číslo:
public class MyClass {
public MyClass(int value) {
this.Value = value;
}
...
}
Typ shim pro konstruktor lze nastavit tak, aby bez ohledu na hodnotu předanou konstruktoru vrátila každá budoucí instance hodnotu -5 při vyvolání getter Value:
// unit test code
ShimMyClass.ConstructorInt32 = (@this, value) => {
var shim = new ShimMyClass(@this) {
ValueGet = () => -5
};
};
Každý typ překrytí zveřejňuje dva typy konstruktorů. Výchozí konstruktor by se měl použít, když je potřeba nová instance, zatímco konstruktor, který jako argument přebírá instanci shimmed, by měl být použit pouze v přešimování konstruktoru:
// unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }
Strukturu vygenerovaného typu pro ShimMyClass
lze znázorní takto:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass>
{
public static Action<MyClass, int> ConstructorInt32 {
set {
...
}
}
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }
...
}
Přístup k základním členům
Vlastnosti shim základních členů lze dosáhnout vytvořením shim pro základní typ a vstupem podřízené instance do konstruktoru základní třídy shim.
Představte si například třídu MyBase
s metodou MyMethod
instance a podtypem MyChild
:
public abstract class MyBase {
public int MyMethod() {
...
}
}
public class MyChild : MyBase {
}
Převlekáním MyBase
je možné nastavit spuštěním nového ShimMyBase
shimu:
// unit test code
var child = new ShimMyChild();
new ShimMyBase(child) { MyMethod = () => 5 };
Je důležité poznamenat, že při předání jako parametr do základního konstruktoru shim je podřízený typ shim implicitně převeden na podřízenou instanci.
Struktura vygenerovaného typu pro ShimMyChild
a ShimMyBase
může se podobat následujícímu kódu:
// 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 { ... } }
}
Statické konstruktory
Typy překrytí zpřístupňují statickou metodu StaticConstructor
pro překrytí statického konstruktoru typu. Vzhledem k tomu, že statické konstruktory se spouštějí jenom jednou, je nutné před přístupem k libovolnému členu typu zajistit konfiguraci přešikování.
Finalizační metody
Finalizační metody nejsou podporovány v fakes.
Privátní metody
Generátor kódu Fakes vytváří vlastnosti shim pro privátní metody, které mají viditelné typy pouze v podpisu, tj. typy parametrů a návratový typ viditelné.
Rozhraní vazeb
Když shimmed typ implementuje rozhraní, generátor kódu generuje metodu, která umožňuje vytvořit vazbu všech členů z daného rozhraní najednou.
Například s ohledem na třídu MyClass
, která implementuje IEnumerable<int>
:
public class MyClass : IEnumerable<int> {
public IEnumerator<int> GetEnumerator() {
...
}
...
}
Implementace třídy MyClass můžete převtěžovat IEnumerable<int>
voláním metody Bind:
// unit test code
var shimMyClass = new ShimMyClass();
shimMyClass.Bind(new List<int> { 1, 2, 3 });
Vygenerovaná struktura ShimMyClass
typu se podobá následujícímu kódu:
// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
public ShimMyClass Bind(IEnumerable<int> target) {
...
}
}
Změna výchozího chování
Každý vygenerovaný typ shim zahrnuje instanci IShimBehavior
rozhraní, která je přístupná prostřednictvím ShimBase<T>.InstanceBehavior
vlastnosti. Toto chování se vyvolá pokaždé, když klient zavolá člena instance, který nebyl explicitně přešaován.
Ve výchozím nastavení, pokud není nastaveno žádné konkrétní chování, používá instanci vrácenou statickou ShimBehaviors.Current
vlastností, která obvykle vyvolá NotImplementedException
výjimku.
Toto chování můžete kdykoli upravit úpravou InstanceBehavior
vlastnosti pro libovolnou instanci shim. Například následující fragment kódu změní chování tak, aby buď nedělaly nic, nebo vrátily výchozí hodnotu návratového typu , tj default(T)
. :
// unit test code
var shim = new ShimMyClass();
//return default(T) or do nothing
shim.InstanceBehavior = ShimBehaviors.DefaultValue;
Nastavením statické ShimBehaviors.Current
vlastnosti můžete také globálně změnit chování všech přešikovaných instancí , kde InstanceBehavior
vlastnost nebyla explicitně definována:
// unit test code
// change default shim for all shim instances where the behavior has not been set
ShimBehaviors.Current = ShimBehaviors.DefaultValue;
Identifikace interakcí s externími závislostmi
Chcete-li zjistit, kdy váš kód komunikuje s externími systémy nebo závislostmi (označovanými jako environment
), můžete pomocí přechytů přiřadit konkrétní chování všem členům typu. To zahrnuje statické metody. Když nastavíte ShimBehaviors.NotImplemented
chování statické Behavior
vlastnosti typu shim, jakýkoli přístup k členu tohoto typu, který nebyl explicitně převrácen, vyvolá NotImplementedException
výjimku . To může sloužit jako užitečný signál během testování, který značí, že se váš kód pokouší o přístup k externímu systému nebo závislosti.
Tady je příklad nastavení v kódu testu jednotek:
// unit test code
// Assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.Behavior = ShimBehaviors.NotImplemented;
Pro usnadnění je také k dispozici zkrácená metoda pro dosažení stejného efektu:
// Shorthand to assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.BehaveAsNotImplemented();
Vyvolání původních metod z metod shim
Při provádění metody shim může být potřeba provést původní metodu. Můžete například chtít zapsat text do systému souborů po ověření názvu souboru předaného metodě.
Jedním z přístupů k řešení této situace je zapouzdření volání původní metody pomocí delegáta, ShimsContext.ExecuteWithoutShims()
jak je znázorněno v následujícím kódu:
// unit test code
ShimFile.WriteAllTextStringString = (fileName, content) => {
ShimsContext.ExecuteWithoutShims(() => {
Console.WriteLine("enter");
File.WriteAllText(fileName, content);
Console.WriteLine("leave");
});
};
Alternativně můžete shim nullify, volat původní metodu a pak obnovit 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;
Zpracování souběžnosti pomocí typů shim
Typy překrytí fungují napříč všemi vlákny v rámci domény AppDomain a nemají spřažení vláken. Tato vlastnost je zásadní, abyste měli na paměti, pokud plánujete využít spouštěč testů, který podporuje souběžnost. Stojí za zmínku, že testy zahrnující typy shim nelze spustit souběžně, i když toto omezení nevynucuje modul runtime Fakes.
Shimming System.Environment
Pokud chcete třídu přemístit System.Environment , budete muset soubor upravit mscorlib.fakes
. Za element Assembly přidejte následující obsah:
<ShimGeneration>
<Add FullName="System.Environment"/>
</ShimGeneration>
Jakmile provedete tyto změny a znovu sestavíte řešení, jsou nyní k dispozici metody a vlastnosti ve System.Environment
třídě, které je možné převedit. Tady je příklad, jak můžete metodě přiřadit chování GetCommandLineArgsGet
:
System.Fakes.ShimEnvironment.GetCommandLineArgsGet = ...
Provedením těchto úprav jste otevřeli možnost řídit a testovat, jak váš kód komunikuje s proměnnými systémového prostředí, což je základní nástroj pro komplexní testování jednotek.