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
Otwieranie programu Visual Studio i tworzenie
Class Library
projektuUstawianie nazwy projektu
HexFileReader
Ustaw nazwę
ShimsTutorial
rozwiązania .Ustaw platformę docelową projektu na .NET Framework 4.8
Usuwanie pliku domyślnego
Class1.cs
Dodaj nowy plik
HexFile.cs
i dodaj następującą definicję klasy:
Tworzenie projektu testowego
Kliknij rozwiązanie prawym przyciskiem myszy i dodaj nowy projekt
MSTest Test Project
Ustawianie nazwy projektu
TestProject
Ustaw platformę docelową projektu na .NET Framework 4.8
Dodawanie zestawu fakes
Dodawanie odwołania do projektu
HexFileReader
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.
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
Zmodyfikuj plik domyślny, aby dodać następujący kod
UnitTest1.cs
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); } }
Poniżej przedstawiono Eksplorator rozwiązań pokazujące wszystkie pliki
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 ShimsContext
obiektu . 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ą MyMethod
wystą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ą MyMethod
wystą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 ), environment
moż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.