Czy powinienem testować odziedziczone metody?

30

Załóżmy, że mam menedżera klas wywodzącego się z pracownika klasy podstawowej i że pracownik ma metodę getEmail (), która jest dziedziczona przez menedżera . Czy powinienem sprawdzić, czy zachowanie metody getEmail () menedżera jest w rzeczywistości takie samo jak zachowanie pracownika?

W momencie pisania tych testów zachowanie będzie takie samo, ale oczywiście w pewnym momencie w przyszłości ktoś może zastąpić tę metodę, zmienić jej zachowanie, a zatem zepsuć moją aplikację. Jednak wydaje się nieco dziwne, aby zasadniczo przetestować pod kątem braku wtrącania się kodu.

(Należy pamiętać, że testowanie metody Manager :: getEmail () nie poprawia pokrycia kodu (ani nawet żadnych innych wskaźników jakości kodu (?)) Do momentu utworzenia / zastąpienia Manager :: getEmail () .)

(Jeśli odpowiedź brzmi „Tak”, przydatne byłyby pewne informacje na temat zarządzania testami dzielonymi między klasami podstawowymi i pochodnymi).

Równoważne sformułowanie pytania:

Jeśli klasa pochodna dziedziczy metodę z klasy podstawowej, jak wyrazić (przetestować), czy oczekuje się, że odziedziczona metoda:

  1. Zachowuj się dokładnie tak samo jak obecnie baza (jeśli zachowanie bazy zmienia się, zachowanie metody pochodnej nie zmienia się);
  2. Zachowuj się dokładnie tak samo jak baza przez cały czas (jeśli zmieni się zachowanie klasy podstawowej, zmieni się również zachowanie klasy pochodnej); lub
  3. Zachowuj się jednak, jak chce (nie obchodzi cię zachowanie tej metody, ponieważ nigdy jej nie nazywasz).
mjs
źródło
1
IMO wywodzące się Managerz klasy Employeebyło pierwszym poważnym błędem.
CodesInChaos
4
@CodesInChaos Tak, to może być zły przykład. Ten sam problem dotyczy jednak dziedziczenia.
mjs
1
Nie musisz nawet zastępować metody. Metoda klasy bazowej może wywoływać inne metody instancji, które są przesłonięte, a wykonanie tego samego kodu źródłowego dla metody wciąż powoduje inne zachowanie.
gnasher729

Odpowiedzi:

23

Przyjąłbym tutaj pragmatyczne podejście: jeśli ktoś w przyszłości zastąpi Manager :: getMail, to na deweloperze spoczywa obowiązek dostarczenia kodu testowego dla nowej metody.

Oczywiście jest to poprawne tylko wtedy, gdy Manager::getEmailnaprawdę ma taką samą ścieżkę kodu jak Employee::getEmail! Nawet jeśli metoda nie zostanie zastąpiona, może zachowywać się inaczej:

  • Employee::getEmailmogłyby wywołać niektóre chronione wirtualne getInternalEmail, które jest zastępowane w Manager.
  • Employee::getEmailmoże uzyskać dostęp do jakiegoś stanu wewnętrznego (np. niektóre pola _email), które mogą różnić się w dwóch implementacjach: Na przykład domyślna implementacja Employeemoże zapewnić, że _emailjest zawsze [email protected], ale Managerjest bardziej elastyczna w przydzielaniu adresów e-mail.

W takich przypadkach możliwe jest, że błąd przejawia się tylko w Manager::getEmail, nawet jeśli implementacja samej metody jest taka sama. W takim przypadku Manager::getEmailosobne testowanie może mieć sens.

Heinzi
źródło
14

Ja bym.

Jeśli myślisz „cóż, to tak naprawdę wywołujesz tylko Employee :: getEmail (), ponieważ go nie zastępuję, więc nie muszę testować Managera :: getEmail ()”, to tak naprawdę nie testujesz zachowania of Manager :: getEmail ().

Zastanowiłbym się, co powinien zrobić tylko Manager :: getEmail (), a nie to, czy jest to dziedziczone czy zastąpione. Jeśli zachowanie Managera :: getEmail () powinno polegać na zwracaniu tego, co zwraca Employee :: getMail (), to jest to test. Jeśli zachowanie ma zwrócić „[email protected]”, to jest to test. Nie ma znaczenia, czy zostanie zaimplementowane przez dziedziczenie, czy zastąpione.

Liczy się to, że jeśli zmieni się w przyszłości, twój test go złapie i wiesz, że coś się zepsuło lub trzeba to przemyśleć.

Niektóre osoby mogą nie zgadzać się z pozorną nadmiarowością czeku, ale moim przeciwnikiem byłoby testowanie zachowania Employee :: getMail () i Manager :: getMail () jako odrębnych metod, niezależnie od tego, czy są dziedziczone, czy nie. zastąpione. Jeśli przyszły programista musi zmienić zachowanie Managera :: getMail (), musi także zaktualizować testy.

Opinie mogą się różnić, ale myślę, że Digger i Heinzi podali uzasadnione powody przeciwne.

seggy
źródło
1
Podoba mi się argument, że testy behawioralne są ortogonalne w stosunku do sposobu budowania klasy. Jednak całkowicie ignorowanie dziedziczenia wydaje się trochę zabawne. (A jak zarządzać wspólnymi testami, jeśli masz dużo dziedziczenia?)
mjs
1
Dobrze wyłożone. Tylko dlatego, że Menedżer używa odziedziczonej metody, w żaden sposób nie oznacza, że ​​nie należy jej testować. Mój wielki przywódca powiedział kiedyś: „Jeśli nie zostanie przetestowany, zostanie zepsuty”. W tym celu, jeśli wykonasz testy pracownika i kierownika wymagane przy odprawie kodu, będziesz mieć pewność, że programista sprawdzający nowy kod menedżera, który może zmienić zachowanie odziedziczonej metody, naprawi test odzwierciedlający nowe zachowanie . Albo zapukaj do twoich drzwi.
hurricMitch
2
Naprawdę chcesz przetestować klasę Manager. Fakt, że jest to podklasa pracownika, a getMail () jest implementowany w oparciu o dziedziczenie, jest tylko szczegółem implementacji, który należy zignorować podczas tworzenia testów jednostkowych. W następnym miesiącu dowiadujesz się, że dziedziczenie menedżera od pracownika było złym pomysłem i zastępujesz całą strukturę dziedziczenia i ponownie wdrażasz wszystkie metody. Testy jednostkowe powinny kontynuować testowanie kodu bez problemów.
gnasher729
5

Nie testuj tego jednostkowo. Wykonaj test funkcjonalny / akceptacyjny.

Testy jednostkowe powinny testować każdą implementację, jeśli nie zapewniasz nowej implementacji, trzymaj się zasady DRY. Jeśli chcesz włożyć tutaj trochę wysiłku, możesz ulepszyć oryginalny test jednostkowy. Tylko jeśli zastąpisz metodę, powinieneś napisać test jednostkowy.

Jednocześnie testy funkcjonalne / akceptacyjne powinny upewnić się, że pod koniec dnia cały kod działa zgodnie z jego przeznaczeniem i, mam nadzieję, wyłapie wszelkie dziwactwa wynikające z dziedziczenia.

MrFox
źródło
4

Reguły Roberta Martina dotyczące TDD to:

  1. Nie wolno pisać żadnego kodu produkcyjnego, chyba że ma to negatywny wynik pozytywnego testu jednostkowego.
  2. Nie wolno pisać więcej testów jednostkowych niż jest to wystarczające do zaliczenia; awarie kompilacji to awarie.
  3. Nie wolno pisać więcej kodu produkcyjnego, niż jest to wystarczające, aby przejść jeden niepowodzenie testu jednostkowego.

Jeśli będziesz przestrzegać tych zasad, nie będziesz w stanie zadać tego pytania. Jeśli getEmailmetoda wymaga przetestowania, zostałaby przetestowana.

Daniel Kaplan
źródło
3

Tak, powinieneś przetestować odziedziczone metody, ponieważ w przyszłości mogą zostać zastąpione. Ponadto odziedziczone metody mogą wywoływać metody wirtualne, które są zastępowane , co zmieniłoby zachowanie nieprzepisanej odziedziczonej metody.

Sposób, w jaki to testuję, polega na utworzeniu klasy abstrakcyjnej w celu przetestowania (ewentualnie abstrakcyjnej) klasy bazowej lub interfejsu, takich jak ten (w języku C # przy użyciu NUnit):

public abstract class EmployeeTests
{
    protected abstract Employee CreateInstance(string name, int age);

    [Test]
    public void GetEmail_ReturnsValidEmailAddress()
    {
        // Given
        var sut = CreateInstance("John Doe", 20);

        // When
        string email = sut.GetEmail();

        // Then
        Assert.IsTrue(Helper.IsValidEmail(email));
    }
}

Następnie mam klasę z testami specyficznymi dla Manageri integruję testy pracownika w ten sposób:

[TestFixture]
public class ManagerTests
{
    // Other tests.

    [TestFixture]
    public class ManagerEmployeeTests : EmployeeTests
    {
        protected override Employee CreateInstance(string name, int age);
        {
            return new Manager(name, age);
        }
    }
}

Powodem, dla którego mogę to zrobić, jest zasada podstawienia Liskowa: testy Employeepowinny przejść pomyślnie, gdy miną Managerobiekt, ponieważ pochodzi Employee. Muszę więc napisać moje testy tylko raz i mogę sprawdzić, czy działają one dla wszystkich możliwych implementacji interfejsu lub klasy bazowej.

Daniel AA Pelsmaeker
źródło
Dobrze, jeśli chodzi o zasadę podstawienia Liskowa, masz rację, że jeśli tak się stanie, klasa pochodna przejdzie wszystkie testy klasy podstawowej. Jednak LSP jest często łamane, w tym przez samą setUp()metodę xUnit ! I prawie każda platforma MVC sieci, która obejmuje przesłonięcie metody „indeksu”, również łamie LSP, czyli w zasadzie wszystkie.
mjs
To jest absolutnie poprawna odpowiedź - słyszałem, że nazywa się to „Abstract Test Pattern”. Z ciekawości, po co używać klasy zagnieżdżonej, a nie tylko mieszać testy za pośrednictwem class ManagerTests : ExployeeTests? (tj. odzwierciedlenie dziedziczenia testowanych klas). Czy jest to głównie estetyka, która pomaga w przeglądaniu wyników?
Luke Usherwood
2

Czy powinienem sprawdzić, czy zachowanie metody getEmail () menedżera jest w rzeczywistości takie samo jak zachowanie pracownika?

Powiedziałbym „nie”, ponieważ moim zdaniem byłby to powtarzany test. Raz przetestowałbym testy pracowników i to by było na tyle.

W momencie pisania tych testów zachowanie będzie takie samo, ale oczywiście w przyszłości ktoś może zastąpić tę metodę, zmienić jej zachowanie

Jeśli metoda zostanie zastąpiona, potrzebne będą nowe testy, aby sprawdzić zastąpione zachowanie. Takie jest zadanie osoby wdrażającej getEmail()metodę nadpisywania .


źródło
1

Nie, nie musisz testować odziedziczonych metod. Klasy i ich przypadki testowe, które opierają się na tej metodzie, i tak ulegną awarii, jeśli zachowanie się zmieni Manager.

Pomyśl o następującym scenariuszu: Adres e-mail jest składany jako Imię[email protected]:

class Employee{
    String firstname, lastname;
    String getEmail() { 
        return firstname + "." + lastname + "@example.com";
    }
}

Jednostka przetestowała to i działa dobrze dla twojego Employee. Utworzyłeś także klasę Manager:

class Manager extends Employee { /* nothing different in email generation */ }

Teraz masz klasę, ManagerSortktóra sortuje menedżerów na liście na podstawie ich adresów e-mail. Zakładasz, że generowanie wiadomości e-mail jest takie samo jak w Employee:

class ManagerSort {
    void sortManagers(Manager[] managerArrayToBeSorted)
        // sort based on email address omitted
    }
}

Piszesz test dla ManagerSort:

void testManagerSort() {
    Manager[] managers = ... // build random manager list
    ManagerSort.sortManagers(managers);

    Manager[] expected = ... // expected result
    assertEquals(expected, managers); // check the result
}

Wszystko dziala. Teraz ktoś przychodzi i zastępuje getEmail()metodę:

class Manager extends Employee {
    String getEmail(){
        // managers should have their lastname and firstname order changed
        return lastname + "." + firstname + "@example.com";
    }
}

Co się teraz dzieje? Twój testManagerSort()zawiedzie, ponieważ getEmail()od Managerzostało nadpisane. Zbadasz ten problem i znajdziesz przyczynę. A wszystko to bez pisania osobnego testu dla odziedziczonej metody.

Dlatego nie trzeba testować odziedziczonych metod.

W przeciwnym razie, na przykład w Javie, trzeba by przetestować wszystkie metody odziedziczone Objectjak toString(), equals()itd. W każdej klasie.

Uooo
źródło
Nie powinieneś być mądry w testach jednostkowych. Mówisz „Nie muszę testować X, ponieważ gdy X nie powiedzie się, Y nie powiedzie się”. Ale testy jednostkowe oparte są na założeniach błędów w kodzie. Po błędach w kodzie, dlaczego uważasz, że złożone relacje między testami działają tak, jak oczekujesz?
gnasher729
@ gnasher729 Mówię: „Nie muszę testować X w podklasie, ponieważ jest ona już testowana w testach jednostkowych dla nadklasy ”. Oczywiście, jeśli zmienisz implementację X, musisz napisać odpowiednie testy.
Uooo