Czy testy integracyjne mają na celu powtórzenie wszystkich testów jednostkowych?

36

Powiedzmy, że mam funkcję (napisaną w Ruby, ale powinna być zrozumiała dla wszystkich):

def am_I_old_enough?(name = 'filip')
   person = Person::API.new(name)
   if person.male?
      return person.age > 21
   else
      return person.age > 18
   end
end

W testach jednostkowych stworzyłbym cztery testy, które obejmowałyby wszystkie scenariusze. Każdy będzie używał wyśmiewanego Person::APIobiektu metodami przerywanymi male?i age.

Teraz chodzi o pisanie testów integracyjnych. Zakładam, że Person :: API nie powinien już być wyśmiewany. Stworzyłbym więc dokładnie te same cztery przypadki testowe, ale bez kpienia z obiektu Person :: API. Czy to jest poprawne?

Jeśli tak, to po co w ogóle pisać testy jednostkowe, gdybym mógł po prostu napisać testy integracyjne, które dadzą mi więcej pewności siebie (ponieważ pracuję na prawdziwych obiektach, a nie skrótach i próbach)?

Filip Bartuzi
źródło
3
Cóż, jedną z rzeczy jest to, że kpiąco / testując je, możesz izolować wszelkie problemy w swoim kodzie. Jeśli test integracji zakończy się niepowodzeniem, nie wiesz, czyj kod jest uszkodzony, czy Twój, czy interfejs API.
Chris Wohlert
9
Tylko cztery testy? Masz sześć przedziałów wiekowych, które powinieneś testować: 17, 18, 19, 20, 21, 22 ...;)
David Arno
22
@FilipBartuzi, zakładam, że metoda sprawdza na przykład, czy mężczyzna ma powyżej 21 lat? Jak obecnie napisano, nie robi tego, jest to prawdą tylko wtedy, gdy są w wieku 22+. „Ponad 21” w języku angielskim oznacza „21+”. W twoim kodzie jest więc błąd. Takie błędy są rejestrowane przez testowanie wartości granicznych, tj. 20, 21, 22 dla mężczyzny, 17,18, 19 dla kobiety w tym przypadku. Potrzebnych jest co najmniej sześć testów.
David Arno,
6
Nie wspominając o przypadkach 0 i -1. Co to znaczy, że osoba ma -1 lat? Co powinien zrobić Twój kod, jeśli interfejs API zwróci coś bezsensownego?
RubberDuck,
9
Byłoby to o wiele łatwiejsze do przetestowania, gdybyś przekazał obiekt osoby jako parametr.
JeffO,

Odpowiedzi:

72

Nie, testy integracyjne nie powinny tylko powielać zakresu testów jednostkowych. Oni mogą powielać pewne zasięg, ale nie o to chodzi.

Celem testu jednostkowego jest upewnienie się, że określony mały fragment funkcji działa dokładnie i całkowicie zgodnie z przeznaczeniem. Test jednostkowy am_i_old_enoughpozwoliłby przetestować dane w różnym wieku, z pewnością te w pobliżu progu, być może wszystkie występujące w wieku ludzkim. Po napisaniu tego testu integralność am_i_old_enoughnigdy nie powinna już być kwestionowana.

Celem testu integracyjnego jest sprawdzenie, czy cały system lub kombinacja znacznej liczby komponentów działa prawidłowo, gdy są używane razem . Klient nie dba o określoną funkcję użyteczności, którą napisałeś, zależy mu na tym, aby jego aplikacja internetowa była odpowiednio zabezpieczona przed dostępem nieletnich, ponieważ w przeciwnym razie organy nadzoru będą miały swoje osły.

Sprawdzanie wiek użytkownika jest jedna niewielka część tej funkcji, ale test integracja nie sprawdza, czy funkcja użyteczności wykorzystuje prawidłową wartość progową. Sprawdza, czy osoba dzwoniąca podejmuje właściwą decyzję na podstawie tego progu, czy funkcja użyteczności jest w ogóle wywoływana, czy spełnione są inne warunki dostępu itp.

Powodem, dla którego potrzebujemy obu typów testów, jest zasadniczo kombinatoryczna eksplozja możliwych scenariuszy dla ścieżki przez bazę kodu, którą może wykonać wykonanie. Jeśli funkcja narzędziowa ma około 100 możliwych danych wejściowych, a istnieją setki funkcji narzędziowych, to sprawdzenie, czy właściwa rzecz dzieje się we wszystkich przypadkach, wymagałoby wielu, wielu milionów przypadków testowych. Po prostu sprawdzając wszystkie przypadki w bardzo małych zakresach, a następnie sprawdzając typowe, istotne lub prawdopodobne kombinacje tych zakresów, przy założeniu, że te małe zakresy są już poprawne, jak wykazano w testach jednostkowych , możemy uzyskać dość pewną ocenę, że system działa co powinno, bez utonięcia w alternatywnych scenariuszach do przetestowania.

Kilian Foth
źródło
6
„możemy uzyskać dość pewną ocenę, że system robi to, co powinien, bez zagłuszania alternatywnych scenariuszy do przetestowania”. Dziękuję Ci. Uwielbiam, gdy ktoś podchodzi do testów automatycznych z rozsądkiem.
jpmc26
1
JB Rainsberger ma przyjemną rozmowę na temat testów i kombinatorycznej eksplozji, o której piszesz w ostatnim akapicie, zatytułowanym „Zintegrowane testy to oszustwo” . Nie chodzi tu tak naprawdę o testy integracyjne, ale wciąż bardzo interesujące.
Bart van Nierop,
The customer doesn't care about a particular utility function you wrote, they care that their web app is properly secured against access by minors-> To bardzo sprytne nastawienie, dzięki! Problem polega na tym, że projektujesz dla siebie. Trudno jest rozdzielić sposób myślenia między byciem programistą a byciem menedżerem produktu w tym samym momencie
Filip Bartuzi,
14

Krótka odpowiedź brzmi „nie”. Bardziej interesujące jest to, dlaczego / jak taka sytuacja może się pojawić.

Myślę, że powstaje zamieszanie, ponieważ próbujesz przestrzegać ścisłych praktyk testowych (testy jednostkowe vs. testy integracyjne, kpiny itp.) W poszukiwaniu kodu, który nie wydaje się przestrzegać ścisłych praktyk.

Nie oznacza to, że kod jest „zły” lub że określone praktyki są lepsze niż inne. Po prostu, niektóre założenia przyjęte przez praktyki testowania mogą nie mieć zastosowania w tej sytuacji, i może pomóc zastosować podobny poziom „rygorystyczności” w praktykach kodowania i praktykach testowania; lub przynajmniej, aby uznać, że mogą być niezrównoważone, co spowoduje, że niektóre aspekty nie będą miały zastosowania lub będą zbędne.

Najbardziej oczywistym powodem jest to, że twoja funkcja wykonuje dwa różne zadania:

  • Wyszukiwanie Personwedług ich nazwy. Wymaga to testów integracyjnych, aby upewnić się, że może znaleźć Personobiekty, które są prawdopodobnie tworzone / przechowywane gdzie indziej.
  • Obliczanie, czy a Personjest wystarczająco stary, na podstawie ich płci. Wymaga to testów jednostkowych, aby upewnić się, że obliczenia przebiegają zgodnie z oczekiwaniami.

Dzięki pogrupowaniu tych zadań w jeden blok kodu nie można uruchomić jednego bez drugiego. Gdy chcesz przetestować obliczenia za pomocą jednostki, musisz sprawdzić Person(z prawdziwej bazy danych lub z kodu pośredniczącego / próbnego). Jeśli chcesz przetestować, czy wyszukiwanie integruje się z resztą systemu, musisz również wykonać obliczenia dotyczące wieku. Co powinniśmy zrobić z tymi obliczeniami? Czy powinniśmy to zignorować, czy sprawdzić? Wydaje się, że jest to dokładne położenie, które opisujesz w swoim pytaniu.

Jeśli wyobrażamy sobie alternatywę, możemy mieć obliczenia na własną rękę:

def is_old_enough?(person)
   if person.male?
      return person.age > 21
   else 
      return person.age > 18
   end
end

Ponieważ jest to czysta kalkulacja, nie musimy przeprowadzać na niej testów integracyjnych.

Możemy też pokusić się o osobne napisanie zadania wyszukiwania:

def person_from_name(name = 'filip')
   return Person::API.new(name)
end

Jednak w tym przypadku funkcjonalność jest tak bliska Person::API.new, że powinieneś użyć jej zamiast tego (jeśli domyślna nazwa jest konieczna, czy lepiej byłoby ją przechowywać gdzie indziej, na przykład atrybut klasy?).

Pisząc testy integracyjne dla Person::API.new(lub person_from_name) wszystko, o co musisz się martwić, to to, czy odzyskasz oczekiwane Person; wszystkie obliczenia oparte na wieku są przeprowadzane gdzie indziej, więc testy integracyjne mogą je zignorować.

Warbo
źródło
11

Kolejną kwestią, którą chciałbym dodać do odpowiedzi Killian, jest to, że testy jednostkowe przebiegają bardzo szybko, więc możemy mieć ich tysiące. Test integracji zwykle trwa dłużej, ponieważ wywołuje usługi sieciowe, bazy danych lub inne zależności zewnętrzne, więc nie możemy uruchomić tych samych testów (1000) dla scenariuszy integracji, ponieważ zajęłyby one zbyt dużo czasu.

Ponadto testy jednostkowe zwykle uruchamiane są w czasie kompilacji (na komputerze kompilacji), a testy integracyjne są uruchamiane po wdrożeniu na środowisku / maszynie.

Zazwyczaj uruchamia się nasze 1000 testów jednostkowych dla każdej kompilacji, a następnie 100 lub więcej testów integracyjnych o wysokiej wartości po każdym wdrożeniu. Nie możemy zabrać każdej kompilacji do wdrożenia, ale jest to w porządku, ponieważ kompilacja, którą zastosujemy do wdrożenia, zostaną uruchomione testy integracyjne. Zazwyczaj chcemy ograniczyć te testy do uruchomienia w ciągu 10 lub 15 minut, ponieważ nie chcemy zbyt długo wstrzymywać wdrożenia.

Ponadto w ramach tygodniowego harmonogramu możemy przeprowadzać regresyjny zestaw testów integracyjnych, które obejmują więcej scenariuszy w weekend lub inne przestoje. Może to potrwać dłużej niż 15 minut, ponieważ omówionych zostanie więcej scenariuszy, ale zazwyczaj nikt nie pracuje nad Sat / Sun, więc możemy poświęcić więcej czasu na testy.

Jon Raynor
źródło
nie dotyczy języków dynamicznych (tj. bez etapu kompilacji)
Filip Bartuzi