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

  1. Otevření sady Visual Studio a vytvoření Class Library projektu

    Snímek obrazovky projektu Knihovny tříd NetFramework v sadě Visual Studio

  2. Nastavení názvu projektu HexFileReader

  3. Nastavte název ShimsTutorialřešení .

  4. Nastavení cílové architektury projektu na .NET Framework 4.8

  5. Odstranění výchozího souboru Class1.cs

  6. Přidejte nový soubor HexFile.cs a přidejte následující definici třídy:

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

Vytvoření testovacího projektu

  1. Klikněte pravým tlačítkem na řešení a přidejte nový projekt. MSTest Test Project

  2. Nastavení názvu projektu TestProject

  3. Nastavení cílové architektury projektu na .NET Framework 4.8

    Snímek obrazovky s projektem NetFramework Test v sadě Visual Studio

Přidat sestavení fakes

  1. Přidání odkazu na projekt HexFileReader

    Snímek obrazovky s příkazem Přidat odkaz na projekt

  2. 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í.

    Screnshot příkazu Add Fakes Assembly.

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

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

    Snímek obrazovky Průzkumník řešení zobrazující všechny soubory

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

// 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 MyMethodinstance:

// 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á NotImplementedExceptionvý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.