Udostępnij za pośrednictwem


Używanie podkładek do izolowania aplikacji na potrzeby testów jednostkowych

Typy podkładek, jedna z dwóch kluczowych technologii używanych przez platformę Microsoft Fakes Framework, mają kluczowe znaczenie w izolowaniu składników aplikacji podczas testowania. Działają one przez przechwytywanie i przekierowywanie wywołań do określonych metod, które można następnie kierować do kodu niestandardowego w ramach testu. Ta funkcja umożliwia zarządzanie wynikiem tych metod, zapewniając, że wyniki są spójne i przewidywalne podczas każdego wywołania, niezależnie od warunków zewnętrznych. Ten poziom kontroli usprawnia proces testowania i pomaga w osiągnięciu bardziej niezawodnych i dokładnych wyników.

Zastosuj podkładki , gdy musisz utworzyć granicę między kodem a zestawami, które nie stanowią części rozwiązania. Gdy celem jest odizolowanie składników rozwiązania od siebie, zalecane jest użycie wycinków.

(Aby uzyskać bardziej szczegółowy opis wycinków, zobacz Użyj wycinków, aby odizolować części aplikacji od siebie na potrzeby testów jednostkowych).

Ograniczenia podkładek

Należy pamiętać, że podkładki mają swoje ograniczenia.

Podkładki nie mogą być używane we wszystkich typach z niektórych bibliotek w klasie bazowej .NET, w szczególności mscorlib i System w programie .NET Framework oraz w środowisku System.Runtime na platformie .NET Core lub .NET 5+. To ograniczenie należy wziąć pod uwagę podczas etapu planowania i projektowania testów, aby zapewnić pomyślną i skuteczną strategię testowania.

Tworzenie podkładki: przewodnik krok po kroku

Załóżmy, że składnik zawiera wywołania elementu System.IO.File.ReadAllLines:

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

Tworzenie biblioteki klas

  1. Otwieranie programu Visual Studio i tworzenie Class Library projektu

    Zrzut ekranu przedstawiający projekt NetFramework Class Library w programie Visual Studio.

  2. Ustawianie nazwy projektu HexFileReader

  3. Ustaw nazwę ShimsTutorialrozwiązania .

  4. Ustaw platformę docelową projektu na .NET Framework 4.8

  5. Usuwanie pliku domyślnego Class1.cs

  6. Dodaj nowy plik HexFile.cs i dodaj następującą definicję klasy:

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

Tworzenie projektu testowego

  1. Kliknij rozwiązanie prawym przyciskiem myszy i dodaj nowy projekt MSTest Test Project

  2. Ustawianie nazwy projektu TestProject

  3. Ustaw platformę docelową projektu na .NET Framework 4.8

    Zrzut ekranu przedstawiający projekt NetFramework Test w programie Visual Studio.

Dodawanie zestawu fakes

  1. Dodawanie odwołania do projektu HexFileReader

    Zrzut ekranu przedstawiający polecenie Dodaj odwołanie do projektu.

  2. Dodawanie zestawu fakes

    • W Eksplorator rozwiązań,

      • W przypadku starszego projektu .NET Framework (bez zestawu SDK) rozwiń węzeł Odwołania projektu testów jednostkowych .

      • W przypadku projektu w stylu zestawu SDK przeznaczonego dla platformy .NET Framework, .NET Core lub .NET 5+rozwiń węzeł Zależności , aby znaleźć zestaw, który chcesz sfałszować w obszarze Zestawy, Projekty lub Pakiety.

      • Jeśli pracujesz w języku Visual Basic, wybierz pozycję Pokaż wszystkie pliki na pasku narzędzi Eksplorator rozwiązań, aby wyświetlić węzeł Odwołania.

    • Wybierz zestaw System zawierający definicję .System.IO.File.ReadAllLines

    • W menu skrótów wybierz pozycję Dodaj zestaw fakes.

    Zrzut ekranu polecenia Dodaj zestaw Fakes.

Ponieważ tworzenie powoduje wyświetlenie niektórych ostrzeżeń i błędów, ponieważ nie wszystkie typy mogą być używane z podkładkami, należy zmodyfikować zawartość elementu Fakes\mscorlib.fakes , aby je wykluczyć.

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

Tworzenie testu jednostkowego

  1. Zmodyfikuj plik domyślny, aby dodać następujący kod UnitTest1.csTestMethod

    [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);
        }
    }
    

    Poniżej przedstawiono Eksplorator rozwiązań pokazujące wszystkie pliki

    Zrzut ekranu przedstawiający Eksplorator rozwiązań wszystkich plików.

  2. Otwórz Eksploratora testów i uruchom test.

Ważne jest, aby prawidłowo usunąć każdy kontekst podkładki. Jako reguła kciuka wywołaj ShimsContext.Create wewnętrzną instrukcję using , aby zapewnić prawidłowe czyszczenie zarejestrowanych podkładek. Na przykład można zarejestrować podkładkę dla metody testowej, która zastępuje DateTime.Now metodę delegatem, który zawsze zwraca pierwszy ze stycznia 2000 r. Jeśli zapomnisz wyczyścić zarejestrowane podkładki w metodzie testowej, pozostała część przebiegu testu zawsze zwróci pierwszy ze stycznia 2000 r. jako DateTime.Now wartość. Może to być zaskakujące i mylące.


Konwencje nazewnictwa dla klas podkładek

Nazwy klas podkładki składają się z prefiksu Fakes.Shim na oryginalną nazwę typu. Nazwy parametrów są dołączane do nazwy metody. (Nie musisz dodawać żadnego odwołania do zestawu do pliku System.Fakes).

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

Opis sposobu działania podkładek

Podkładki działają przez wprowadzenie objazdów do bazy kodu testowanej aplikacji. Za każdym razem, gdy jest wywołanie oryginalnej metody, system Fakes interweniuje w celu przekierowania tego wywołania, powodując wykonanie niestandardowego kodu podkładki zamiast oryginalnej metody.

Należy pamiętać, że te objazdy są tworzone i usuwane dynamicznie w czasie wykonywania. Objazdy powinny być zawsze tworzone w ciągu cyklu życia ShimsContextobiektu . Po usunięciu podkładki ShimsContext wszystkie aktywne podkładki, które zostały w nim utworzone, również zostaną usunięte. Aby efektywnie zarządzać tym rozwiązaniem, zaleca się hermetyzowanie tworzenia objazdów w ramach using instrukcji.


Podkładki dla różnych rodzajów metod

Podkładki obsługują różne typy metod.

Metody statyczne

Podczas podkładania metod statycznych właściwości przechowujące podkładki są przechowywane w typie podkładki. Te właściwości mają tylko metodę ustawiającą, która służy do dołączania delegata do metody docelowej. Jeśli na przykład mamy klasę o nazwie MyClass ze statyczną metodą MyMethod:

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

Możemy dołączyć podkładkę do MyMethod takiej, że stale zwraca 5:

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

Metody wystąpienia (dla wszystkich wystąpień)

Podobnie jak metody statyczne, metody wystąpień mogą być również podkładane dla wszystkich wystąpień. Właściwości, które przechowują te podkładki, są umieszczane w zagnieżdżonym typie o nazwie AllInstances, aby zapobiec nieporozumieniu. Jeśli mamy klasę MyClass z metodą MyMethodwystąpienia :

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

Możemy dołączyć podkładkę tak MyMethod , aby stale zwracała wartość 5, niezależnie od wystąpienia:

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

Wygenerowana struktura ShimMyClass typów będzie wyglądać następująco:

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

W tym scenariuszu platforma Fakes przekazuje wystąpienie środowiska uruchomieniowego jako pierwszy argument delegata.

Metody wystąpienia (pojedyncze wystąpienie środowiska uruchomieniowego)

Metody wystąpień mogą być również podkładane przy użyciu różnych delegatów, w zależności od odbiornika wywołania. Dzięki temu ta sama metoda wystąpienia może wykazywać różne zachowania na wystąpienie typu. Właściwości, które przechowują te podkładki, to metody wystąpienia samego typu podkładki. Każdy wystąpienie typu podkładki jest połączony z nieprzetworzonym wystąpieniem typu shimmed.

Na przykład nadana klasie MyClass z metodą MyMethodwystąpienia :

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

Możemy utworzyć dwa typy podkładek w taki MyMethod sposób, aby pierwszy konsekwentnie zwracał wartość 5, a drugi stale zwraca wartość 10:

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

Wygenerowana struktura ShimMyClass typów będzie wyglądać następująco:

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

Dostęp do rzeczywistego wystąpienia typu shimmed można uzyskać za pośrednictwem właściwości Wystąpienie:

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

Typ podkładki zawiera również niejawną konwersję na typ podkładki, co umożliwia bezpośrednie użycie typu podkładki:

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

Konstruktory

Konstruktory nie są wyjątkiem od podkładania; można je też zagnieżdżyć w celu dołączenia typów podkładek do obiektów, które zostaną utworzone w przyszłości. Na przykład każdy konstruktor jest reprezentowany jako metoda statyczna o nazwie Constructor, w obrębie typu podkładki. Rozważmy klasę MyClass z konstruktorem, który akceptuje liczbę całkowitą:

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

Typ podkładki dla konstruktora można skonfigurować tak, aby niezależnie od wartości przekazanej do konstruktora każde przyszłe wystąpienie zwracało wartość -5 po wywołaniu metody getter wartości:

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

Każdy typ podkładki uwidacznia dwa typy konstruktorów. Konstruktor domyślny powinien być używany, gdy potrzebne jest nowe wystąpienie, natomiast konstruktor, który przyjmuje shimmed wystąpienia jako argument, powinien być używany tylko w podkładkach konstruktora:

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

Struktura wygenerowanego typu ShimMyClass dla elementu można zilustrować w następujący sposób:

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

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

Uzyskiwanie dostępu do składowych podstawowych

Właściwości podkładki składowych bazowych można uzyskać, tworząc podkładkę dla typu podstawowego i wpisując wystąpienie podrzędne w konstruktorze klasy podkładki bazowej.

Rozważmy na przykład klasę MyBase z metodą MyMethod wystąpienia i podtypem MyChild:

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

public class MyChild : MyBase {
}

Podkładkę można skonfigurować, inicjując nową ShimMyBase podkładkęMyBase:

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

Należy pamiętać, że w przypadku przekazania jako parametru do podstawowego konstruktora podkładki podrzędny typ podkładki jest niejawnie konwertowany na wystąpienie podrzędne.

Struktura wygenerowanego typu ShimMyChild i ShimMyBase może zostać porównana do następującego kodu:

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

Konstruktory statyczne

Typy podkładek uwidaczniają statyczną metodę StaticConstructor podkładki statycznego konstruktora typu. Ponieważ konstruktory statyczne są wykonywane tylko raz, należy upewnić się, że podkładka jest skonfigurowana przed uzyskaniem dostępu do dowolnego elementu członkowskiego typu.

Finalizatory

Finalizatory nie są obsługiwane w fakes.

Metody prywatne

Generator kodu Fakes tworzy właściwości podkładki dla metod prywatnych, które mają tylko widoczne typy w podpisie, czyli typy parametrów i zwracany typ widoczny.

Interfejsy powiązań

Gdy typ shimmed implementuje interfejs, generator kodu emituje metodę, która umożliwia powiązanie wszystkich elementów członkowskich z tego interfejsu jednocześnie.

Na przykład biorąc pod uwagę klasę MyClass , która implementuje IEnumerable<int>element :

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

Implementacje elementu w usłudze IEnumerable<int> MyClass można podkładać, wywołując metodę Bind:

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

Wygenerowana struktura ShimMyClass typów przypomina następujący kod:

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

Zmienianie zachowania domyślnego

Każdy wygenerowany typ podkładki zawiera wystąpienie interfejsu IShimBehavior , dostępne za pośrednictwem ShimBase<T>.InstanceBehavior właściwości . To zachowanie jest wywoływane za każdym razem, gdy klient wywołuje członka wystąpienia, który nie został jawnie podkładany.

Domyślnie jeśli nie ustawiono żadnego konkretnego zachowania, używa wystąpienia zwróconego przez właściwość statyczną ShimBehaviors.Current NotImplementedException , co zwykle zgłasza wyjątek.

To zachowanie można zmodyfikować w dowolnym momencie, dostosowując InstanceBehavior właściwość dla dowolnego wystąpienia podkładki. Na przykład poniższy fragment kodu zmienia zachowanie tak, aby nie robił nic lub zwracał wartość domyślną typu zwracanego — tj. default(T):

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

Można również globalnie zmienić zachowanie dla wszystkich shimmed wystąpień — gdzie InstanceBehavior właściwość nie została jawnie zdefiniowana — ustawiając właściwość statyczną ShimBehaviors.Current :

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

Identyfikowanie interakcji z zależnościami zewnętrznymi

Aby ułatwić określenie, kiedy kod wchodzi w interakcje z systemami zewnętrznymi lub zależnościami (określanymi jako ), environmentmożna użyć podkładek do przypisania określonego zachowania do wszystkich elementów członkowskich typu. Obejmuje to metody statyczne. Ustawiając ShimBehaviors.NotImplemented zachowanie właściwości statycznej Behavior typu podkładki, każdy dostęp do elementu członkowskiego tego typu, który nie został jawnie podkładany, zgłosi element NotImplementedException. Może to służyć jako przydatny sygnał podczas testowania, co oznacza, że kod próbuje uzyskać dostęp do systemu zewnętrznego lub zależności.

Oto przykład sposobu konfigurowania go w kodzie testu jednostkowego:

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

Dla wygody dostępna jest również metoda skrócona, aby osiągnąć ten sam efekt:

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

Wywoływanie oryginalnych metod z metod podkładki

Mogą wystąpić scenariusze, w których może być konieczne wykonanie oryginalnej metody podczas wykonywania metody podkładki. Na przykład możesz chcieć napisać tekst w systemie plików po zweryfikowaniu nazwy pliku przekazanej do metody .

Jednym z podejść do obsługi tej sytuacji jest hermetyzowanie wywołania oryginalnej metody przy użyciu delegata i ShimsContext.ExecuteWithoutShims(), jak pokazano w poniższym kodzie:

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

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

Alternatywnie można unieważnić podkładki, wywołać oryginalną metodę, a następnie przywrócić podkładki.

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

Obsługa współbieżności za pomocą typów podkładek

Typy podkładek działają we wszystkich wątkach w domenie AppDomain i nie mają koligacji wątku. Ta właściwość ma kluczowe znaczenie, jeśli planujesz korzystać z modułu uruchamiającego testy obsługującego współbieżność. Warto zauważyć, że testy obejmujące typy podkładek nie mogą być uruchamiane współbieżnie, chociaż to ograniczenie nie jest wymuszane przez środowisko uruchomieniowe Fakes.

Shimming System.Environment

Jeśli chcesz podkładić klasę System.Environment , musisz wprowadzić pewne modyfikacje w mscorlib.fakes pliku. Po elemecie Assembly dodaj następującą zawartość:

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

Po wprowadzeniu tych zmian i ponownym utworzeniu rozwiązania metody i właściwości w System.Environment klasie są teraz dostępne do shimmed. Oto przykład sposobu przypisywania zachowania do GetCommandLineArgsGet metody :

System.Fakes.ShimEnvironment.GetCommandLineArgsGet = ...

Wprowadzając te modyfikacje, otwarto możliwość kontrolowania i testowania interakcji kodu ze zmiennymi środowiskowymi systemowymi, czyli podstawowego narzędzia do kompleksowego testowania jednostkowego.