Stosowanie wycinków kodu do izolowania od siebie poszczególnych części aplikacji w celu przeprowadzania testów jednostkowych

Typy wycinków są jedną z dwóch technologii dostępnych w środowisku Microsoft Fakes, które ułatwiają izolowanie testowanego składnika od innych wywoływanych przez niego składników.Odcinek jest niewielkim fragmentem kodu, który zajmuje miejsce innego składnika podczas testu.Korzyścią wynikającą z zastosowania wycinka są spójne wyniki, co ułatwia tworzenie testów.Testy można będzie uruchomić, nawet jeśli inne składniki jeszcze nie działają.

Aby uzyskać omówienie i szybki start do środowiska Fakes, zobacz sekcję Izolowanie testowanego kodu za pomocą struktury Microsoft Fakes.

Aby użyć wycinków, trzeba napisać składnik w taki sposób, aby korzystał tylko z interfejsów, a nie z klas, i odwoływał się do innych części aplikacji.To dobra praktyka tworzenia projektów, ponieważ zmiany są wprowadzane tylko w jednej części i jest mniej prawdopodobne, że inne również będą wymagać zmian.Do celów testowych pozwala zastąpić wycinkiem rzeczywisty składnik.

Na diagramie składnikiem StockAnalyzer jest ten, który chcemy przetestować.Zwykle używa on innego składnika RealStockFeed.RealStockFeed zwraca jednak różne wyniki przy każdym wywołaniu jego metod, co utrudnia test StockAnalyzer.Podczas testowania można zastąpić go inną klasą StubStockFeed.

Rzeczywistym i klasy skrótową są zgodne z jednego interfejsu.

Wycinki opierają się w ten sposób na swoich możliwościach bycia strukturą kodu, dlatego zwykle są one używane w celu wyizolowania jednej strony aplikacji z innej.Aby odłączyć je od innych zestawów niebędących pod kontrolą, takich jak System.dll, normalnie zostałyby użyte podkładki.Zobacz Stosowanie podkładek do izolowania aplikacji od innych zestawów w celu przeprowadzania testów jednostkowych.

Wymagania

  • Visual Studio Ultimate

W tym temacie:

Jak używać wycinków

Zaprojektowane do wstrzykiwania zależności

Aby korzystać z wycinków, aplikacja musi być tak zaprojektowana, aby różne składniki nie były zależne od siebie, ale tylko od definicji interfejsu.Zamiast być połączone w czasie kompilacji, składniki są połączone w czasie wykonywania.Ten wzór pomaga stworzyć oprogramowanie, które będzie niezawodne i łatwe do zaktualizowania, ponieważ zmiany zwykle nie są propagowane przez granice składnika.Zalecamy następujące działanie, nawet jeśli użytkownik nie używa wycinków.Podczas pisania nowy kod jest czytelna iniekcji zależności wzorzec.Jeśli piszesz testy dla istniejącego oprogramowania, możliwe, że trzeba będzie je refraktoryzować.Jeżeli byłoby to niepraktyczne, można rozważyć użycie zamiast niego podkładki.

Zacznijmy tę dyskusję od motywującego przykładu, takiego jak ten na diagramie.Klasa, którą odczytuje StockAnalyzer, udostępnia ceny i generuje interesujące wyniki.Obejmuje ona niektóre metody publiczne, które chcemy sprawdzić.Aby zachować ich prostotę, po prostu przyjrzyjmy się jednej z tych metod — bardzo prostej — która zgłasza aktualną cenę udziału.Chcemy napisać test jednostkowy tej metody.Oto pierwszy projekt testu:

        [TestMethod]
        public void TestMethod1()
        {
            // Arrange:
            var analyzer = new StockAnalyzer();
            // Act:
            var result = analyzer.GetContosoPrice();
            // Assert:
            Assert.AreEqual(123, result); // Why 123?
        }
    <TestMethod()> Public Sub TestMethod1()
        ' Arrange:
        Dim analyzer = New StockAnalyzer()
        ' Act:
        Dim result = analyzer.GetContosoPrice()
        ' Assert:
        Assert.AreEqual(123, result) ' Why 123?
    End Sub

Jeden z problemów z tym testem staje się natychmiast oczywisty: ceny udziału różnią się, więc potwierdzenie zwykle zakończy się niepowodzeniem.

Innym problemem może być to, że składnik StockFeed, który jest używany przez StockAnalyzer, jest wciąż w fazie opracowywania.Oto pierwszy projekt kodu testowanej metody:

        public int GetContosoPrice()
        {
            var stockFeed = new StockFeed(); // NOT RECOMMENDED
            return stockFeed.GetSharePrice("COOO");
        }
    Public Function GetContosoPrice()
        Dim stockFeed = New StockFeed() ' NOT RECOMMENDED
        Return stockFeed.GetSharePrice("COOO")
    End Function

W obecnym stanie metoda ta nie może kompilować lub może zgłosić wyjątek, ponieważ praca w klasie StockFeed nie została jeszcze zakończona.

Wstrzyknięcie interfejsu rozwiązuje oba te problemy.

Wstrzyknięcie interfejsu wykorzystuje następującą regułę:

  • Kod jakiegokolwiek składnika aplikacji nigdy nie powinien jawnie odnosić się do klasy w innym składniku, deklaracji lub instrukcji new.Zamiast tego zmienne i parametry powinny być zadeklarowane razem z interfejsami.Wystąpienia składnika powinny być tworzone tylko przez kontener składnika.

    Przez „składnik” w tym przypadku rozumie się klasę lub grupę klas, które można dopracowywać i aktualizować łącznie.Składnikiem jest zazwyczaj kod w jednym projekcie programu Visual Studio.Rozdzielenie klas w obrębie jednego składnika nie jest zbyt ważne, ponieważ są one aktualizowane w tym samym czasie.

    Oddzielenie składników od klas stosunkowo stabilnej platformy, takiej jak System.dll, również nie jest zbyt istotne.Pisanie interfejsów dla wszystkich tych klas spowodowałoby zaśmiecenie kodu.

Kod StockAnalyzer można zatem poprawić przez oddzielenie go od StockFeed przy użyciu interfejsu, takiego jak:

    public interface IStockFeed
    {
        int GetSharePrice(string company);
    }

    public class StockAnalyzer
    {
        private IStockFeed stockFeed;
        public Analyzer(IStockFeed feed)
        {
            stockFeed = feed;
        }
        public int GetContosoPrice()
        {
            return stockFeed.GetSharePrice("COOO");
        }
    }
Public Interface IStockFeed
    Function GetSharePrice(company As String) As Integer
End Interface

Public Class StockAnalyzer
    ' StockAnalyzer can be connected to any IStockFeed:
    Private stockFeed As IStockFeed
    Public Sub New(feed As IStockFeed)
        stockFeed = feed
    End Sub  
    Public Function GetContosoPrice()
        Return stockFeed.GetSharePrice("COOO")
    End Function
End Class

W tym przykładzie StockAnalyzer przekazuje implementację IStockFeed podczas konstruowania.W ukończonej aplikacji kod inicjowania może wykonać połączenie:

analyzer = new StockAnalyzer(new StockFeed())

Istnieją bardziej elastyczne sposoby wykonywania tego połączenia.Na przykład StockAnalyzer może zaakceptować obiekt fabryki, który może utworzyć wystąpienie różnych implementacji IStockFeed w różnych warunkach.

Generowanie wycinków

Klasa, którą chcesz przetestować, została odłączona od innych składników, z których korzysta.Oddzielenie powoduje, że aplikacja staje się bardziej solidna i elastyczna, a ponadto pozwala połączyć składnik testu z implementacją wycinka w ramach testowania interfejsów.

Można po prostu zwyczajnie napisać wycinki jako klasy.Jednak środowisko Microsoft Fakes zapewnia bardziej dynamiczny sposób tworzenia najodpowiedniejszych wycinków dla każdego testu.

Aby użyć wycinków, należy najpierw wygenerować typy wycinków z definicji interfejsu.

Dodawanie podrobionych zestawów

  1. W oknie Eksploratora rozwiązań rozwiń listę Odwołania dla projektu testowego.

    • Pracując w języku Visual Basic, należy na pasku narzędzi Eksploratora rozwiązań wybrać opcję Pokaż wszystkie pliki, aby zobaczyć listę odwołań.
  2. Wybierz zestaw zawierający definicje interfejsu, dla których chcesz utworzyć wycinki.

  3. W menu skrótów wybierz polecenie Dodaj podrobiony zestaw.

Napisz test z wycinkami

[TestClass]
class TestStockAnalyzer
{
    [TestMethod]
    public void TestContosoStockPrice()
    {
      // Arrange:

        // Create the fake stockFeed:
        IStockFeed stockFeed = 
             new StockAnalysis.Fakes.StubIStockFeed() // Generated by Fakes.
                 {
                     // Define each method:
                     // Name is original name + parameter types:
                     GetSharePriceString = (company) => { return 1234; }
                 };

        // In the completed application, stockFeed would be a real one:
        var componentUnderTest = new StockAnalyzer(stockFeed);

      // Act:
        int actualValue = componentUnderTest.GetContosoPrice();

      // Assert:
        Assert.AreEqual(1234, actualValue);
    }
    ...
}
<TestClass()> _
Class TestStockAnalyzer

    <TestMethod()> _
    Public Sub TestContosoStockPrice()
        ' Arrange:
        ' Create the fake stockFeed:
        Dim stockFeed As New StockAnalysis.Fakes.StubIStockFeed
        With stockFeed
            .GetSharePriceString = Function(company)
                                       Return 1234
                                   End Function
        End With
        ' In the completed application, stockFeed would be a real one:
        Dim componentUnderTest As New StockAnalyzer(stockFeed)
        ' Act:
        Dim actualValue As Integer = componentUnderTest.GetContosoPrice
        ' Assert:
        Assert.AreEqual(1234, actualValue)
    End Sub
End Class

Specjalną funkcję pe‎łni tutaj klasa StubIStockFeed.Dla każdego typu publicznego w zestawie, do którego istnieje odwołanie, mechanizm Microsoft Fakes generuje klasę wycinków.Nazwa klasy wycinka jest tworzona od nazwy interfejsu, z „Fakes.Stub” jako prefiksem i dołączonymi nazwami typu parametru.

Wycinki kodu są generowane także dla metod pobierających i ustawiających właściwości, dla zdarzeń i metod ogólnych.

Weryfikowanie wartości parametrów

Można zweryfikować, że jeżeli składnik wywołuje inny składnik, przekazuje poprawne wartości.Teraz można umieścić potwierdzenie w wycinku lub przechowywać wartość i weryfikować ją w głównej części testu.Na przykład:

[TestClass]
class TestMyComponent
{
       
    [TestMethod]
    public void TestVariableContosoPrice()
    {
     // Arrange:
        int priceToReturn;
        string companyCodeUsed;
        var componentUnderTest = new StockAnalyzer(new StubIStockFeed()
            {
               GetSharePriceString = (company) => 
                  { 
                     // Store the parameter value:
                     companyCodeUsed = company;
                     // Return the value prescribed by this test:
                     return priceToReturn;
                  };
            };
        // Set the value that will be returned by the stub:
        priceToReturn = 345;

     // Act:
        int actualResult = componentUnderTest.GetContosoPrice();

     // Assert:
        // Verify the correct result in the usual way:
        Assert.AreEqual(priceToReturn, actualResult);

        // Verify that the component made the correct call:
        Assert.AreEqual("COOO", companyCodeUsed);
    }
...}
<TestClass()> _
Class TestMyComponent
    <TestMethod()> _
    Public Sub TestVariableContosoPrice()
        ' Arrange:
        Dim priceToReturn As Integer
        Dim companyCodeUsed As String = ""
        Dim stockFeed As New StockAnalysis.Fakes.StubIStockFeed()
        With stockFeed
            ' Implement the interface's method:
            .GetSharePriceString = _
                Function(company)
                    ' Store the parameter value:
                    companyCodeUsed = company
                    ' Return a fixed result:
                    Return priceToReturn
                End Function
        End With
        ' Create an object to test:
        Dim componentUnderTest As New StockAnalyzer(stockFeed)
        ' Set the value that will be returned by the stub:
        priceToReturn = 345

        ' Act:
        Dim actualResult As Integer = componentUnderTest.GetContosoPrice()

        ' Assert:
        ' Verify the correct result in the usual way:
        Assert.AreEqual(priceToReturn, actualResult)
        ' Verify that the component made the correct call:
        Assert.AreEqual("COOO", companyCodeUsed)
    End Sub
...
End Class

Wycinki dla różnych rodzajów elementów członkowskich typu

Metody

Jak opisano w przykładzie, metody można dzielić na wycinki, dołączając delegata do instancji klasy wycinka.Nazwa typu wycinka pochodzi od nazwy metody i parametrów.Na przykład, biorąc pod uwagę następujący interfejs IMyInterface i metodę MyMethod:

// application under test
interface IMyInterface 
{
    int MyMethod(string value);
}

Dołączamy odcinek do metody MyMethod, która zawsze zwraca 1:

// unit test code
  var stub = new StubIMyInterface ();
  stub.MyMethodString = (value) => 1;

Jeśli nie podano wycinka dla funkcji, środowisko Fakes wygeneruje funkcję zwracającą wartość domyślną typu zwracanego.Dla liczb wartością domyślną jest 0, a dla typów klasy jest null (C#) lub Nothing (Visual Basic).

Właściwości

Metody pobierające i ustawiające są widoczne jako oddzielne delegaty i mogą tworzyć poszczególne wycinki.Na przykład rozważmy właściwość Value z IMyInterface:

// code under test
interface IMyInterface 
{
    int Value { get; set; }
}

Aby symulować auto-właściwości, można dołączyć delegaty do metod pobierających i ustawiających Value:

// unit test code
int i = 5;
var stub = new StubIMyInterface();
stub.ValueGet = () => i;
stub.ValueSet = (value) => i = value;

Jeśli nie podano metody zastępczej ani dla metod ustawiających, ani pobierających właściwości, środowisko Fakes wygeneruje odcinek, który przechowuje wartości, tak aby właściwość zastępcza działała jak prosta zmienna.

Zdarzenia

Zdarzenia są uwidocznione jako pola delegatów.W rezultacie wszystkie zdarzenia przekształcone na wycinki mogą być łatwo wywoływane przez wywołanie zdarzenia pola pomocniczego.Rozważmy następujący interfejs do przekształcenia na wycinki:

// code under test
interface IWithEvents 
{
    event EventHandler Changed;
}

Aby wywołać zdarzenie Changed, można po prostu wywołać pomocniczego delegata:

// unit test code
  var withEvents = new StubIWithEvents();
  // raising Changed
  withEvents.ChangedEvent(withEvents, EventArgs.Empty);

Metody ogólne

Istnieje możliwość tworzenia wycinków dla metod ogólnych poprzez dostarczenie delegata dla każdego żądanego wystąpienia metody.Na przykład, biorąc pod uwagę następujący interfejs, zawierający metodę ogólną:

// code under test
interface IGenericMethod 
{
    T GetValue<T>();
}

można napisać test, który tworzy wycinki wystąpienia GetValue<int>:

// unit test code
[TestMethod]
public void TestGetValue() 
{
    var stub = new StubIGenericMethod();
    stub.GetValueOf1<int>(() => 5);

    IGenericMethod target = stub;
    Assert.AreEqual(5, target.GetValue<int>());
}

Jeśli w kodzie nastąpi wywołanie GetValue<T> z jakimkolwiek innym wystąpieniem, odcinek po prostu wywoła dane zachowanie.

Wycinki wirtualnych klas

W poprzednich przykładach wycinki zostały wygenerowane z interfejsów.Można również wygenerować wycinki z klasy, która ma członków virtual lub abstract.Na przykład:

// Base class in application under test
    public abstract class MyClass
    {
        public abstract void DoAbstract(string x);
        public virtual int DoVirtual(int n)
        { return n + 42; }
        public int DoConcrete()
        { return 1; }
    }

W wycinku wygenerowanym z tej klasy można ustawić metody delegowane dla DoAbstract() i DoVirtual(), ale nie DoConcrete().

// unit test
  var stub = new Fakes.MyClass();
  stub.DoAbstractString = (x) => { Assert.IsTrue(x>0); };
  stub.DoVirtualInt32 = (n) => 10 ;
  

Jeśli nie podasz delegata dla metody wirtualnej, środowisko Fakes może zapewnić zachowanie domyślne albo wywoływać metodę w klasie podstawowej.Aby korzystać z podstawowej metody nazwanej, należy ustawić właściwość CallBase:

// unit test code
var stub = new Fakes.MyClass();
stub.CallBase = false;
// No delegate set – default delegate:
Assert.AreEqual(0, stub.DoVirtual(1));

stub.CallBase = true;
//No delegate set - calls the base:
Assert.AreEqual(43,stub.DoVirtual(1));

Debugowanie wycinków

Typy wycinków zostały tak zaprojektowane, aby zapewniać płynność debugowania.Domyślnie debuger pomija kod generowany, powinien więc wejść bezpośrednio do niestandardowych implementacji elementu członkowskiego, które zostały dołączone do wycinka.

Ograniczenia dotyczące wycinka

  1. Podpisy metod ze wskaźnikami nie są obsługiwane.

  2. Zapieczętowane klasy lub metody statyczne nie mogą zostać przekształcone na wycinki, ponieważ typy wycinka opierają się na wysyłaniu wirtualnej metody.Dla tych klas należy używać typów podkładek opisanych w sekcji Stosowanie podkładek do izolowania aplikacji od innych zestawów w celu przeprowadzania testów jednostkowych

Zmiana domyślnego zachowania wycinków

Każdy wygenerowany typ wycinka posiada wystąpienie interfejsu IStubBehavior (poprzez właściwość IStub.InstanceBehavior).Zachowanie jest wywoływane za każdym razem, gdy klient wywołuje element członkowski, który nie ma dołączonego niestandardowego delegata.Jeśli nie ustawiono zachowania, zostanie użyte wystąpienie zwrócone przez właściwość StubsBehaviors.Current.Domyślnie właściwość ta zwraca zachowanie, które zgłasza wyjątek NotImplementedException.

Zachowanie to można zmienić w dowolnym momencie przez ustawienie właściwości InstanceBehavior na dowolnym wystąpieniu wycinka.Na przykład poniższa wstawka kodu zmienia zachowanie, które nie wykonuje działania lub zwraca domyślną wartość typu zwracanego: default(T):

// unit test code
var stub = new StubIFileSystem();
// return default(T) or do nothing
stub.InstanceBehavior = StubsBehaviors.DefaultValue;

Zachowanie to można także zmienić globalnie, dla wszystkich obiektów z tym wycinkiem, których zachowanie nie zostało ustawione, poprzez ustawienie właściwości StubsBehaviors.Current:

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

Zasoby zewnętrzne

Wskazówki

Testowanie w przypadku dostarczania ciągłego z programu Visual Studio 2012 w rozdziale 2: testowania jednostek: testowanie wewnątrz

Zobacz też

Koncepcje

Izolowanie testowanego kodu za pomocą struktury Microsoft Fakes