Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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
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
Dodaj zestaw Fakes
Dodaj odwołanie do projektu
HexFileReader
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.
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); } }
Oto Eksplorator rozwiązań przedstawiający wszystkie pliki
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 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
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.