Udostępnij za pośrednictwem


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

Typy shim, jedna z dwóch kluczowych technologii używanych przez platformę Microsoft Fakes Framework, są niezbędne do izolacji 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 pozwala na zarządzanie wynikami tych metod, zapewniając, że będą one spójne i przewidywalne przy każdym wywołaniu, 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żywanie wycinków do izolowania 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 shima: 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.

Dodaj zestaw Fakes

  1. Dodaj odwołanie do projektu HexFileReader

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

  2. Dodaj zestaw Fakes

    • W Eksploratorze rozwiązań

      • W przypadku starszego projektu .NET Framework (nienależącego do zestawu SDK), rozwiń węzeł Odwołania w jednostkowym projekcie testów.

      • 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 opcję 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 przedstawiający polecenie 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);
        }
    }
    

    Oto Eksplorator rozwiązań przedstawiający wszystkie pliki

    Zrzut ekranu pokazujący wszystkie pliki w eksploratorze rozwiązań.

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

Ważne jest, aby prawidłowo rozlokować każdy kontekst nakładki. Jako ogólna zasada, wywołaj ShimsContext.Create wewnątrz instrukcji using, aby zapewnić prawidłowe czyszczenie zarejestrowanych shimów. Na przykład można zarejestrować shim dla metody testowej, która zastępuje DateTime.Now metodę delegatem, który zawsze zwraca pierwszy stycznia 2000 r. Jeśli zapomnisz wyczyścić zarejestrowany shim w metodzie testowej, reszta przebiegu testu zawsze będzie zwracała pierwszy stycznia 2000 jako wartość DateTime.Now. Może to być zaskakujące i mylące.


Konwencje nazewnictwa dla klas shim

Nazwy klas "shim" są tworzone przez dodanie prefiksu Fakes.Shim do oryginalnej nazwy typu. Nazwy parametrów są dołączane do nazwy metody. (Nie musisz dodawać żadnego odwołania do zestawu System.Fakes.)

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

Zrozumienie działania podkładek

Shimy działają poprzez wprowadzenie modyfikacji przepływu w bazie kodu aplikacji testowanej. Za każdym razem, gdy następuje wywołanie oryginalnej metody, system Fakes interweniuje, aby przekierować to wywołanie, co powoduje wykonanie twojego niestandardowego kodu zastępczego 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 w taki sposób, aby stale zwracała 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ż zarządzane dla wszystkie wystąpienia. 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ą MyMethod wystąpienia:

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

Możemy dołączyć interfejs MyMethod, aby zawsze zwracał wartość 5, niezależnie od instancji.

// 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ż shimowane przy użyciu różnych delegatów, w zależności od odbiorcy wywołania. Dzięki temu ta sama metoda wystąpienia może wykazywać różne zachowania dla każdego wystąpienia typu. Właściwości, które przechowują te podkładki, to metody wystąpienia samego typu podkładki. Każde wystąpienie typu shim jest połączone z surowym wystąpieniem typu shimmowanego.

Na przykład, biorąc pod uwagę klasę MyClass z metodą instancji MyMethod:

// 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 typie shim. 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 -5 po wywołaniu modułu pobierającego wartość:

// 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 potrzebna jest nowa instancja, natomiast konstruktor, który przyjmuje dostosowaną instancję jako argument, powinien być używany tylko w adaptacjach konstruktora.

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

Struktura wygenerowanego typu dla ShimMyClass 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 elementów bazowych można uzyskać, tworząc podkładkę dla typu podstawowego i umieszczając instancję podrzędną 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ę MyBase można skonfigurować, inicjując nową podkładkę ShimMyBase.

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

Należy pamiętać, że gdy typ podrzędnej podkładki jest przekazywany jako parametr do podstawowego konstruktora podkładki, jest on niejawnie konwertowany na instancję podrzędną.

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 Shim udostępniają statyczną metodę StaticConstructor do podkładki konstruktora statycznego typu. Ponieważ konstruktory statyczne są wykonywane tylko raz, należy upewnić się, że warstwa pośrednia jest skonfigurowana przed uzyskaniem dostępu do dowolnego członu typu.

Finalizatory

Finalizatory nie są obsługiwane w Fakes.

Metody prywatne

Generator kodu Fakes tworzy właściwości typu shim dla prywatnych metod, które mają tylko widoczne typy w podpisie, to znaczy widoczne typy parametrów oraz typ zwracany.

Interfejsy wiązania

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

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

Implementacje IEnumerable<int> w MyClass można przechwycić, 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 szumu zawiera wystąpienie interfejsu IShimBehavior, dostępne za pośrednictwem właściwości ShimBase<T>.InstanceBehavior. To zachowanie jest wywoływane za każdym razem, gdy klient wywołuje członka wystąpienia, który nie został jawnie obłożony shimem.

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

To zachowanie można zmieniać w dowolnym momencie, dostosowując właściwość InstanceBehavior dla dowolnego wystąpienia shima. 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 environment), można użyć shimów do przypisania określonego zachowania do wszystkich elementów członkowskich typu. Obejmuje to metody statyczne. Ustawiając zachowanie ShimBehaviors.NotImplemented dla statycznej właściwości Behavior typu shim, każdy dostęp do członka tego typu, który nie został jawnie obsłużony w kodzie, spowoduje zgłoszenie 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 Shim

Mogą wystąpić scenariusze, w których możesz potrzebować wykonać oryginalną metodę podczas wykonywania metody typu shim. 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 anulować shim, wywołać oryginalną metodę, a następnie przywrócić 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;

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

Typy shim działają we wszystkich wątkach w domenie AppDomain i nie mają powiązania z wątkiem. Ta właściwość ma kluczowe znaczenie, jeśli planujesz korzystać z narzędzia do uruchamiania testów 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.

Implementacja warstwy pośredniej dla System.Environment

Jeśli chcesz modyfikować klasę System.Environment, musisz wprowadzić pewne modyfikacje w pliku mscorlib.fakes. 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.