Najlepszy sposób na jednostkowe metody testowe, które wywołują inne metody z tej samej klasy


35

Niedawno dyskutowałem z przyjaciółmi, która z poniższych 2 metod najlepiej jest usunąć wyniki lub wywołania metod z tej samej klasy z metod z tej samej klasy.

To bardzo uproszczony przykład. W rzeczywistości funkcje są znacznie bardziej złożone.

Przykład:

public class MyClass
{
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionB()
     {
         return new Random().Next();
     }
}

Aby to przetestować, mamy 2 metody.

Metoda 1: Użyj funkcji i akcji, aby zastąpić funkcjonalność metod. Przykład:

public class MyClass
{
     public Func<int> FunctionB { get; set; }

     public MyClass()
     {
         FunctionB = FunctionBImpl;
     }

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionBImpl()
     {
         return new Random().Next();
     }
}

[TestClass]
public class MyClassTests
{
    private MyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new MyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionB = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Metoda 2: Uczyń członków wirtualnymi, wyprowadź klasę i skorzystaj z klasy pochodnej Funkcje i akcje zastępujące funkcjonalność Przykład:

public class MyClass
{     
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected virtual int FunctionB()
     {
         return new Random().Next();
     }
}

public class TestableMyClass
{
     public Func<int> FunctionBFunc { get; set; }

     public MyClass()
     {
         FunctionBFunc = base.FunctionB;
     }

     protected override int FunctionB()
     {
         return FunctionBFunc();
     }
}

[TestClass]
public class MyClassTests
{
    private TestableMyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new TestableMyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionBFunc = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Chcę wiedzieć, który jest lepszy, a także DLACZEGO?

Aktualizacja: UWAGA: Funkcja B może być również publiczna


Twój przykład jest prosty, ale nie do końca poprawny. FunctionAzwraca bool, ale ustawia tylko zmienną lokalną xi niczego nie zwraca.
Eric P.

1
W tym konkretnym przykładzie FunctionB może należeć public staticdo innej klasy.

W przypadku recenzji kodu oczekuje się opublikowania rzeczywistego kodu, a nie jego uproszczonej wersji. Zobacz FAQ. W tej chwili zadajesz konkretne pytanie, nie szukając recenzji kodu.
Winston Ewert

1
FunctionBjest zepsuty przez projekt. new Random().Next()prawie zawsze się myli. Powinieneś wstrzyknąć instancję Random. ( Randomjest także źle zaprojektowaną klasą, która może powodować kilka dodatkowych problemów)
CodesInChaos

bardziej ogólnie, DI za pośrednictwem delegatów jest absolutnie w porządku imho
jk.

Odpowiedzi:


32

Edytowano po oryginalnej aktualizacji plakatu.

Uwaga: nie jest programistą C # (głównie Java lub Ruby). Moja odpowiedź brzmiałaby: w ogóle bym tego nie testował i nie sądzę, że powinieneś.

Dłuższa wersja to: prywatne / chronione metody nie są częściami API, są to po prostu opcje implementacji, które możesz zdecydować o przejrzeniu, aktualizacji lub całkowitym wyrzuceniu bez żadnego wpływu na zewnątrz.

Podejrzewam, że masz test na FunctionA (), która jest częścią klasy widoczną ze świata zewnętrznego. Powinien być jedynym, który ma umowę na wdrożenie (i którą można przetestować). Twoja prywatna / chroniona metoda nie ma umowy na spełnienie i / lub przetestowanie.

Zobacz pokrewną dyskusję tam: https://stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones

Po komentarzu , jeśli funkcja B jest publiczna, po prostu przetestuję oba za pomocą testu jednostkowego. Możesz myśleć, że test funkcji A nie jest całkowicie „jednostkowy” (jak wywołuje funkcję B), ale nie martwiłbym się tym: jeśli test funkcji B działa, ale nie test funkcji A, oznacza to wyraźnie, że problem nie leży w subdomena FunctionB, która jest wystarczająco dobra dla mnie jako dyskryminatora.

Jeśli naprawdę chcesz całkowicie rozdzielić dwa testy, użyłbym jakiejś techniki kpiny, aby wyśmiewać FunctionB podczas testowania FunctionA (zazwyczaj zwraca ustaloną znaną poprawną wartość). Brakuje mi wiedzy na temat ekosystemu C #, aby doradzić konkretnej kpiącej bibliotece, ale możesz spojrzeć na to pytanie .


2
Całkowicie zgadzam się z odpowiedzią @Martin. Kiedy piszesz testy jednostkowe dla klasy, nie powinieneś testować metod . To, co testujesz, to zachowanie klasy , że umowa (deklaracja, co klasa ma zrobić) jest spełniona. Dlatego testy jednostkowe powinny obejmować wszystkie wymagania wystawione dla tej klasy (przy użyciu publicznych metod / właściwości), w tym wyjątkowe przypadki

Witam, dziękuję za odpowiedź, ale nie odpowiedziała na moje pytanie. Nie ma znaczenia, czy funkcja B była prywatna / chroniona. Może być również publiczny i nadal może być wywoływany z FunctionA.

Najczęstszym sposobem radzenia sobie z tym problemem, bez przeprojektowywania klasy bazowej, jest podklasa MyClassi przesłonięcie metody za pomocą funkcji, którą chcesz zlikwidować. Dobrym pomysłem może być również zaktualizowanie pytania w celu uwzględnienia FunctionBgo jako publicznego.
Eric P.

1
protectedmetody są częścią publicznej powierzchni klasy, chyba że upewnisz się, że nie można zaimplementować swojej klasy w różnych zestawach.
CodesInChaos

2
Fakt, że FunctionA wywołuje FunctionB, jest nieistotnym szczegółem z perspektywy testów jednostkowych. Jeśli testy dla Funkcji A są napisane poprawnie, jest to szczegół implementacji, który można później przefaktoryzować bez przerywania testów (o ile ogólne zachowanie Funkcji A pozostanie niezmienione). Prawdziwy problem polega na tym, że pobieranie losowej liczby przez funkcję B musi być wykonane za pomocą wstrzykniętego obiektu, aby można było użyć próbnego podczas testowania, aby upewnić się, że dobrze znana liczba zostanie zwrócona. Pozwala to przetestować dobrze znane wejścia / wyjścia.
Dan Lyons

11

Zgadzam się z teorią, że jeśli funkcja jest ważna do przetestowania lub jest ważna do zastąpienia, jest wystarczająco ważna, aby nie być prywatnym szczegółem implementacji testowanej klasy, ale być publicznym szczegółem implementacji innej klasy.

Więc jeśli jestem w scenariuszu, w którym mam

class A 
{
     public B C()
     {
         D();
     }

     private E D();
     {
         // i actually want to control what this produces when I test C()
         // or this is important enough to test on its own
         // and, typically, both of the above
     }
}

Potem idę do refaktoryzacji.

class A 
{
     ICollaborator collaborator;

     public A(ICollaborator collaborator)
     {
         this.collaborator = collaborator;
     }

     public B C()
     {
         collaborator.D();
     }
}

Teraz mam scenariusz, w którym D () jest niezależnie testowalny i w pełni wymienny.

Jako środek organizacji mój współpracownik może nie żyć na tym samym poziomie przestrzeni nazw. Na przykład, jeśli Ajest w FooCorp.BLL, mój współpracownik może znajdować się na innej głębokiej warstwie, jak w FooCorp.BLL.Collaborators (lub jakakolwiek inna nazwa jest odpowiednia). Mój współpracownik może być ponadto widoczny tylko w zestawie za pomocą internalmodyfikatora dostępu, który następnie ujawnię również projektom testującym moje jednostki za pomocą InternalsVisibleToatrybutu zestawu. Zaletą jest to, że nadal możesz utrzymywać interfejs API w czystości, jeśli chodzi o osoby dzwoniące, jednocześnie produkując weryfikowalny kod.


Tak, jeśli ICollaborator potrzebuje kilku metod. Jeśli masz obiekt, którego jedynym zadaniem jest owinięcie jednej metody, wolałbym, aby zastąpić ją delegatem.
jk.

Musisz zdecydować, czy wyznaczony delegat ma sens, czy interfejs ma sens, a ja nie zdecyduję za ciebie. Osobiście nie jestem przeciwny pojedynczym (publicznym) klasom metod. Im mniejsze, tym lepiej, ponieważ stają się coraz łatwiejsze do zrozumienia.
Anthony Pegram

0

Dodając do tego, co zauważa Martin,

Jeśli twoja metoda jest prywatna / chroniona - nie testuj jej. Jest to wewnętrzne dla klasy i nie powinno być dostępne poza klasą.

W obu wymienionych przeze mnie podejściach mam obawy -

Metoda 1 - To faktycznie zmienia zachowanie badanej klasy w teście.

Metoda 2 - W rzeczywistości nie testuje kodu produkcyjnego, zamiast tego testuje inną implementację.

W podanym problemie widzę, że jedyną logiką A jest sprawdzenie, czy wynik funkcji B jest parzysty. Mimo że jest to przykładowe, funkcja B podaje wartość losową, którą trudno przetestować.

Oczekuję realistycznego scenariusza, w którym możemy skonfigurować MyClass w taki sposób, abyśmy wiedzieli, jaka funkcja B zwróci. Wtedy nasz oczekiwany wynik jest znany, możemy wywołać FunctionA i potwierdzić rzeczywisty wynik.


3
protectedjest prawie taki sam jak public. Tylko privatei internalsą szczegóły implementacji.
CodesInChaos

@codeinchaos - Jestem tu ciekawy. W przypadku testu metody chronione są „prywatne”, chyba że zmodyfikujesz atrybuty zestawu. Tylko typy pochodne mają dostęp do chronionych członków. Z wyjątkiem wirtualnego, nie rozumiem, dlaczego chroniony powinien być traktowany podobnie do publicznego z testu. czy mógłbyś opracować proszę?
Srikanth Venugopalan

Ponieważ te pochodne klasy mogą znajdować się w różnych zestawach, są one narażone na kod strony trzeciej, a zatem stanowią część powierzchni publicznej twojej klasy. Aby je przetestować, możesz je wykonać internal protected, użyć prywatnego pomocnika refleksji lub utworzyć klasę pochodną w projekcie testowym.
CodesInChaos

@CodesInChaos, zgodził się, że klasa pochodna może znajdować się w różnych zestawach, ale zakres jest nadal ograniczony do typów bazowych i pochodnych. Modyfikowanie modyfikatora dostępu tylko w celu umożliwienia jego przetestowania jest czymś, co mnie trochę denerwuje. Zrobiłem to, ale wydaje mi się to antypatternem.
Srikanth Venugopalan

0

Ja osobiście używam Method1, tj. Przekształcam wszystkie metody w Action lub Funcs, ponieważ to znacznie poprawiło dla mnie testowalność kodu. Podobnie jak w przypadku każdego rozwiązania, z tym podejściem wiążą się zalety i wady:

Plusy

  1. Pozwala na prostą strukturę kodu, przy czym używanie wzorców kodów tylko do testów jednostkowych może dodatkowo zwiększyć złożoność.
  2. Umożliwia zapieczętowanie klas i eliminację metod wirtualnych wymaganych przez popularne frameworki takie jak Moq. Uszczelnianie klas i eliminowanie metod wirtualnych czyni je kandydatami do inlinizacji i innych optymalizacji kompilatora. ( https://msdn.microsoft.com/en-us/library/ff647802.aspx )
  3. Upraszcza testowanie, ponieważ zastąpienie implementacji Func / Action w teście jednostkowym jest tak proste, jak tylko przypisanie nowej wartości do Func / Action
  4. Pozwala również na sprawdzenie, czy statyczny Func został wywołany z innej metody, ponieważ metod statycznych nie można wyśmiewać.
  5. Łatwe refaktoryzacja istniejących metod do Funcs / Actions, ponieważ składnia wywołania metody w witrynie wywołującej pozostaje taka sama. (W przypadkach, gdy nie można zmienić metody na Func / Action, zobacz minusy)

Cons

  1. Funkcji / akcji nie można użyć, jeśli można wyprowadzić klasę, ponieważ funkcje / akcje nie mają ścieżek dziedziczenia, takich jak metody
  2. Nie można użyć parametrów domyślnych. Utworzenie Func z domyślnymi parametrami pociąga za sobą utworzenie nowego delegata, co może spowodować, że kod będzie mylący w zależności od przypadku użycia
  3. Nie można użyć nazwanej składni parametru do wywoływania metod takich jak coś (firstName: „S”, lastName: „K”)
  4. Największą wadą jest to, że nie masz dostępu do odwołania „this” w Funcs and Actions, więc wszelkie zależności od klasy muszą zostać jawnie przekazane jako parametry. Jest dobrze, ponieważ znasz wszystkie zależności, ale źle, jeśli masz wiele właściwości, od których będzie zależeć Twój Func. Twój przebieg będzie się różnić w zależności od przypadku użycia.

Podsumowując, korzystanie z testu Funcs and Actions for Unit jest świetne, jeśli wiesz, że twoje klasy nigdy nie zostaną zastąpione.

Ponadto zwykle nie tworzę właściwości Funcs, ale wstawiam je bezpośrednio jako takie

public class MyClass
{
     public Func<int> FunctionB = () => new Random().Next();

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }
}

Mam nadzieję że to pomoże!


-1

Korzystanie z Mock jest możliwe. Nuget: https://www.nuget.org/packages/moq/

I wierz mi, że jest to dość proste i ma sens.

public class SomeClass
{
    public SomeClass(int a) { }

    public void A()
    {
        B();
    }

    public virtual void B()
    {

    }
}

[TestFixture]
public class Test
{
    [Test]
    public void Test_A_Calls_B()
    {
        var mockedObject = new Mock<SomeClass>(5); // You can also specify constructor arguments.
        //You can also setup what a function can return.
        var obj = mockedObject.Object;
        obj.A();

        Mock.Get(obj).Verify(x=>x.B(),Times.AtLeastOnce);//This test passes
    }
}

Mock potrzebuje metody wirtualnej do zastąpienia.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.