Język

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 umożliwia kontrolowanie rezultatów działania tych metod, dzięki czemu możesz zapewnić spójne i przewidywalne wyniki 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.

Użyj shimów, gdy musisz utworzyć warstwę pośrednią między swoim kodem a zestawami, które nie są częścią rozwiązania. Gdy celem jest odizolowanie składników rozwiązania od siebie, zalecane jest użycie wycinków . Aby uzyskać szczegółowy opis wycinków, zobacz Używanie wycinków do izolowania części aplikacji od siebie na potrzeby testowania jednostkowego.

Ten artykuł zawiera przewodnik krok po kroku dotyczący używania typów shim do przekierowywania wywołań do określonych metod w kodzie testów jednostkowych.

Poznaj ograniczenia typów przy użyciu shimów

Podczas pracy z podkładkami występują pewne ograniczenia. Nie można ich używać na wszystkich typach z niektórych bibliotek w .NET klasie bazowej, w szczególności:

  • mscorlib i System w strukturze .NET
  • System.Runtime w .NET Core lub .NET 5+

Podczas projektowania strategii testowania zaplanuj to ograniczenie, aby zapewnić pomyślne i skuteczne testowanie jednostkowe.

Utwórz shim

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

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

Aby przygotować składnik do testowania jednostkowego, wykonaj kroki opisane w poniższych procedurach.

Tworzenie biblioteki klas

Utwórz nowe rozwiązanie i początkowy projekt dla biblioteki klas.

  1. W oknie Visual Studio Start (File>Start Window) utwórz projekt Class Library, wybierając szablon języka C# lub Visual Basic.

    Zrzut ekranu szablonu projektu Biblioteka klas dla platformy .NET Framework i języka C# w programie Visual Studio.

    Zrzut ekranu szablonu projektu „Biblioteka klas” dla platformy .NET Framework i języka C# w programie Visual Studio 2022.

  2. Skonfiguruj nowy projekt:

    • Ustaw bibliotekę klas Nazwa projektu na HexFileReader.
    • Ustaw nazwę rozwiązania na ShimsTutorial.
    • Ustaw Target framework na .NET Framework 4.8.

    Wybierz Utwórz.

  3. Po otworzeniu projektu znajdź plik domyślny Class1.cs w Eksplorator rozwiązań i usuń plik.

  4. Dodaj plik o nazwie HexFile.cs i wprowadź 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);
        }
    }
    

Dodawanie projektu testów jednostkowych

Dodaj kolejny projekt do rozwiązania na potrzeby testów jednostkowych.

  1. W Eksplorator rozwiązań kliknij prawym przyciskiem myszy rozwiązanie ShimsTutorial, a następnie wybierz pozycję Add>Nowy Project.

  2. W oknie Start utwórz Unit Test Project project, wybierając szablon.

    Zrzut ekranu szablonu projektu testów jednostkowych firmy Microsoft dla platformy .NET Framework i języka C# w programie Visual Studio.

    Zrzut ekranu szablonu projektu Microsoft Unit Test dla .NET Framework i języka C# w programie Visual Studio 2022.

  3. Skonfiguruj nowy projekt:

    • Ustaw test jednostkowy Project name na TestProject.
    • Ustaw Target framework na .NET Framework 4.8.

    Wybierz Utwórz.

Dodaj zestaw Fakes

Dodaj odwołanie do projektu HexFileReader.

  1. W Eksplorator rozwiązań rozwiń węzeł TestProject, kliknij prawym przyciskiem myszy węzeł References i wybierz Dodaj odwołanie.

    Screenshot pokazujący, jak dodać odwołanie z projektu testów jednostkowych do klasy w Visual Studio.

    Screenshot pokazujący, jak dodać odwołanie z projektu testów jednostkowych do klasy w Visual Studio 2022.

    1. W lewym okienku okna Menedżer odwołań wybierz sekcję Projekty .

    2. W środkowym okienku zaznacz pole wyboru dla HexFileReader projektu i wybierz przycisk OK.

  2. Dodaj zestaw Fakes.

    1. W Eksplorator rozwiązań znajdź węzeł zawierający zestaw:

      • W przypadku starszego projektu .NET Framework (niebędącego typu SDK) rozwiń węzeł projektu testów jednostkowych, a następnie rozwiń węzeł References.

      • W przypadku projektu w stylu SDK ukierunkowanego na platformę .NET Framework, .NET Core lub .NET 5 albo nowszą rozwiń węzeł Dependencies, aby zlokalizować zestaw, który ma być sfingowany, w obszarze Assemblies, Projects lub Packages.

      • W języku Visual Basic na pasku narzędzi Eksplorator rozwiązań wybierz Show All Files, aby wyświetlić węzeł References.

    2. Wybierz złożenie System, które zawiera definicję elementu System.IO.File.ReadAllLines.

    3. Kliknij prawym przyciskiem myszy węzeł System i wybierz polecenie Add Fakes Assembly.

      Screenshot pokazujący, jak dodać zestaw Fakes dla projektu w Visual Studio.

      Screenshot pokazujący, jak dodać zestaw Fakes dla projektu w Visual Studio 2022.

    Proces kompilacji powoduje wyświetlanie ostrzeżeń i błędów dla typów, które nie są obsługiwane do użycia z shimami.

    Po zakończeniu kompilacji Eksplorator rozwiązań odświeża się, aby wyświetlić węzeł Fakes dla projektu testów jednostkowych.

  3. Wybierz plik Fakes\mscorlib.fakes i zastąp kod XML, aby wykluczyć nieobsługiwane typy:

    <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

Dodaj test jednostkowy dla projektu.

  1. Zaktualizuj plik domyślny TestProject\UnitTest1.cs udostępniony przez szablon projektu.

    Znajdź następującą sekcję kodu w pliku i zastąp ją podanym fragmentem kodu:

    [TestMethod]
    public void TestMethod1()
    {
    }
    

    Fragment kodu zastępczego:

    [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);
       }
    }
    
  2. Aby wyświetlić wszystkie zestawy Fakes dla testu jednostkowego, wybierz Pokaż wszystkie pliki na pasku menu Eksplorator rozwiązań:

    Zrzut ekranu przedstawiający okno Eksplorator rozwiązań w programie Visual Studio 2022, pokazujący wszystkie pliki, w tym zestawy Fakes.

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

Ważne jest, aby prawidłowo rozlokować każdy kontekst nakładki. Przyjmuje się, że metodę ShimsContext.Create należy wywoływać wewnątrz instrukcji using, aby zapewnić prawidłowe usuwanie zarejestrowanych shimów. Na przykład można zarejestrować przesłonę dla metody testowej, która zastępuje metodę DateTime.Now delegatem, który zawsze zwraca datę 1 stycznia 2000 r. Jeśli zapomnisz usunąć zarejestrowany shim w metodzie testowej, przez pozostałą część wykonywania testu jako wartość DateTime.Now zawsze będzie zwracana data 1 stycznia 2000 r. Ten wynik może być zaskakujący i mylący.

Przegląd konwencji nazewnictwa dla klas podkładek

Nazwy klas shim używają prefiksu Fakes.Shim, po którym następuje oryginalna nazwa typu. Nazwy parametrów są dołączane do nazwy metody. (Nie musisz dodawać żadnego odwołania do zestawu do System.Fakes.)

System.IO.File.ReadAllLines(path);

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

Dowiedz się, jak działają podkładki regulacyjne

Shimy działają poprzez wprowadzenie modyfikacji przepływu w bazie kodu aplikacji testowanej. Za każdym razem, gdy wywoływana jest oryginalna metoda, system Fakes przechwytuje to wywołanie i przekierowuje je, powodując wykonanie niestandardowego kodu shim zamiast kodu 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 zniszczeniu obiektu ShimsContext wszystkie aktywne shimy utworzone w jego obrębie również zostaną usunięte. Aby efektywnie zarządzać tym projektem, zaleca się hermetyzowanie tworzenia objazdów w ramach using instrukcji.

Poznaj shimy dla różnych typów metod

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

Metody statyczne

Gdy stosujesz shimy do metod statycznych, właściwości przechowujące shimy znajdują się w typie shim. Te właściwości mają tylko metodę ustawiającą, która służy do dołączania delegata do metody docelowej.

Na przykład dla klasy o nazwie MyClass z metodą MyMethodstatyczną :

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

Możesz dołączyć atrapę do MyMethod tak, aby zawsze 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 przechowujące te shimy umieszczono w typie zagnieżdżonym o nazwie AllInstances, aby uniknąć nieporozumień.

Dla klasy MyClass z metodą instancji MyMethod:

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

Możesz dołączyć obiekt shim do MyMethod, tak aby zawsze zwracał wartość 5, niezależnie od instancji:

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

Poniższy fragment kodu przedstawia wygenerowaną strukturę ShimMyClasstypu :

// 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 instancji (pojedyncza instancja środowiska uruchomieniowego)

Metody wystąpień można również podkładać przy użyciu różnych delegatów, w zależności od odbiornika wywołania. Takie podejście umożliwia tej samej metodzie instancji różne zachowanie dla każdej instancji danego 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żna utworzyć dla elementu MyMethod dwa typy atrap, tak aby pierwszy zawsze zwracał 5, a drugi zawsze zwracał 10:

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

Poniższy fragment kodu przedstawia wygenerowaną strukturę ShimMyClasstypu :

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

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

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

Typ shim obejmuje również niejawną konwersję do typu shimowanego, co pozwala bezpośrednio używać typu shim:

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

Poznaj konstruktory shimów

Konstruktory nie są wyjątkiem, jeśli chodzi o stosowanie shimów. Można je podkładać w celu dołączenia typów podkładek do obiektów, które są tworzone w przyszłości. Na przykład każdy konstruktor jest reprezentowany jako metoda statyczna o nazwie Constructor w typie podkładki.

Rozważ klasę MyClass z konstruktorem, który akceptuje liczbę całkowitą:

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

Dla tego konstruktora można skonfigurować typ shim tak, aby niezależnie od wartości przekazanej do konstruktora każda przyszła instancja zwracała wartość -5 po wywołaniu akcesora Value:

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

Każdy typ podkładki uwidacznia dwa typy konstruktorów:

  • Jeśli potrzebujesz nowego wystąpienia, użyj konstruktora domyślnego.
  • Jeśli masz shim konstruktora, użyj konstruktora, który przyjmuje jako argument instancję objętą shimmem.
// Unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }

Poniższy kod ilustruje wygenerowany typ dla elementu ShimMyClass:

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

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

Dostęp do składowych klasy bazowej

Właściwości podkładek składowych podstawowych można uzyskać, tworząc podkładkę dla typu podstawowego. Następnie wprowadź 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ę 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 po przekazaniu podkładki podrzędnej 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 jest podobna 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 shim jest skonfigurowany, zanim uzyska się dostęp do jakiejkolwiek składowej typu.

Finalizatory

Finalizatory nie są obsługiwane przez 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() {
       ...
   }
   ...
}

Możesz zastąpić implementacje IEnumerable<int> w MyClass warstwą pośrednią, 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 uruchamiane za każdym razem, gdy klient wywołuje składnik instancji, który nie został jawnie objęty mechanizmem shim.

Domyślnie, jeśli nie ustawiono żadnego określonego zachowania, implementacja używa wystąpienia zwracanego 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 można zmienić zachowanie, aby nic nie robić lub zwracać wartości domyślnej typu zwracanego: 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 jest jawnie zdefiniowana, ustawiając właściwość statyczną ShimBehaviors.Current :

// Unit test code
// Change default shim for all shim instances where the behavior isn't set
ShimBehaviors.Current = ShimBehaviors.DefaultValue;

Identyfikowanie interakcji z zależnościami zewnętrznymi

Aby pomóc rozpoznać, kiedy kod współdziała z systemami zewnętrznymi lub zależnościami (określanymi zbiorczo jako environment), można użyć shimów, aby nadać określone zachowanie wszystkim składowym typu (w tym metodom statycznym). Ustawienie zachowania ShimBehaviors.NotImplemented we właściwości statycznej Behavior typu shim powoduje, że każda próba uzyskania dostępu do składowej tego typu, która nie jest jawnie objęta mechanizmem shim, skutkuje zgłoszeniem wyjątku NotImplementedException. Ten wynik może 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, jak skonfigurować ten typ atrapy w kodzie testów jednostkowych:

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

W niektórych przypadkach może być konieczne uruchomienie oryginalnej metody podczas wykonywania metody 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() metody:

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

Typy shim działają we wszystkich wątkach w obrębie domeny AppDomain i nie są powiązane z żadnym konkretnym wątkiem. O tej cesze należy koniecznie pamiętać, 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ż środowisko uruchomieniowe Fakes nie wymusza tego ograniczenia.

Używanie podkładek w środowisku System.Environment

Aby zastosować obejście dla klasy System.Environment, należy zmodyfikować plik mscorlib.fakes.

\<Assembly> Znajdź element i dodaj następującą zawartość po definicji elementu:

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

Po wprowadzeniu zmian i ponownym skompilowaniu rozwiązania metody i właściwości w klasie System.Environment są teraz dostępne do tworzenia shimów.

Oto przykład sposobu przypisywania zachowania do GetCommandLineArgsGet metody :

System.Fakes.ShimEnvironment.GetCommandLineArgsGet = ...

Podczas wprowadzania tych modyfikacji uzyskasz możliwość kontrolowania i testowania interakcji kodu ze zmiennymi środowiskowymi systemowymi, co jest podstawowym narzędziem do kompleksowego testowania jednostkowego.