Czy należy na stałe kodować swoje dane we wszystkich testach jednostkowych?

33

Większość dostępnych tu samouczków / przykładów testowania jednostek zazwyczaj obejmuje zdefiniowanie danych do przetestowania dla każdego testu. Myślę, że jest to część teorii „wszystko powinno być testowane w izolacji”.

Jednak odkryłem, że w przypadku aplikacji wielowarstwowych z dużą ilością DI kod wymagany do skonfigurowania każdego testu jest bardzo długi. Zamiast tego zbudowałem wiele klas baz testowych, które mogę teraz odziedziczyć, i które mają wiele gotowych rusztowań testowych.

W ramach tego buduję również fałszywe zestawy danych, które reprezentują bazę danych działającej aplikacji, chociaż zwykle z jednym lub dwoma wierszami w każdej „tabeli”.

Czy przyjętą praktyką jest predefiniowanie, jeśli nie wszystkie, większości danych testowych we wszystkich testach jednostkowych?

Aktualizacja

Z poniższych komentarzy wynika, że ​​robię więcej integracji niż testowania jednostkowego.

Mój obecny projekt to ASP.NET MVC, w którym najpierw używam Jednostki pracy nad Entity Framework Code i Moq do testowania. Wyśmiewałem UoW i repozytoria, ale używam prawdziwych klas logiki biznesowej i testuję działania kontrolera. Testy często sprawdzają, czy UoW zostało popełnione, np .:

[TestClass]
public class SetupControllerTests : SetupControllerTestBase {
  [TestMethod]
  public void UserInvite_ExistingUser_DoesntInsertNewUser() {
    // Arrange
    var model = new Mandy.App.Models.Setup.UserInvite() {
      Email = userData.First().Email
    };

    // Act
    setupController.UserInvite(model);

    // Assert
    mockUserSet.Verify(m => m.Add(It.IsAny<UserProfile>()), Times.Never);
    mockUnitOfWork.Verify(m => m.Commit(), Times.Once);
  }
}

SetupControllerTestBasebuduje fałszywy UoW i tworzy instancję userLogic.

Wiele testów wymaga posiadania istniejącego użytkownika lub produktu w bazie danych, więc wstępnie wypełniłem to, co zwraca fałszywy UoW, w tym przykładzie userData, który zawiera tylko IList<User>jeden rekord użytkownika.

mattdwen
źródło
4
Problem z samouczkami / przykładami polega na tym, że muszą być one proste, ale nie można pokazać rozwiązania złożonego problemu na prostym przykładzie. Powinny im towarzyszyć „studia przypadków” opisujące, w jaki sposób narzędzie jest wykorzystywane w rzeczywistych projektach o rozsądnej wielkości, ale rzadko tak jest.
Jan Hudec
Może mógłbyś dodać kilka małych przykładów kodu, z którego nie jesteś zadowolony.
Luc Franken
Jeśli potrzebujesz dużo kodu instalacyjnego do uruchomienia testu, ryzykujesz uruchomienie testu funkcjonalnego. Jeśli test się nie powiedzie po zmianie kodu, ale nie ma nic złego w kodzie. To zdecydowanie test funkcjonalny.
Reactgular,
Książka „Wzorce testowe xUnit” stanowi mocne uzasadnienie dla urządzeń wielokrotnego użytku i pomocników. Kod testowy powinien być tak samo łatwy do utrzymania, jak każdy inny kod.
Chuck Krutsinger
Ten artykuł może być pomocny: yegor256.com/2015/05/25/unit-test-scaffolding.html
yegor256

Odpowiedzi:

25

Ostatecznie chcesz napisać jak najmniej kodu, aby uzyskać jak najwięcej wyników. Posiadanie wielu tego samego kodu w wielu testach a) powoduje kodowanie i kopiowanie i wklejanie, a b) oznacza, że ​​jeśli zmieni się podpis metody, możesz w końcu naprawić wiele zepsutych testów.

Używam podejścia polegającego na posiadaniu standardowych klas TestHelper, które zapewniają mi wiele typów danych, z których rutynowo korzystam, dzięki czemu mogę tworzyć zestawy standardowych klas encji lub klas DTO , aby moje testy mogły odpytywać i dokładnie wiedzieć, co otrzymam za każdym razem. Więc mogę zadzwonić, TestHelper.GetFooRange( 0, 100 )aby uzyskać zakres 100 obiektów Foo ze wszystkimi ustawionymi ich zależnymi klasami / polami.

Zwłaszcza tam, gdzie w systemie typu ORM skonfigurowane są złożone relacje, które muszą być obecne, aby wszystko działało poprawnie, ale niekoniecznie są istotne dla tego testu, który może zaoszczędzić dużo czasu.

W sytuacjach, w których testuję blisko poziomu danych, czasami tworzę testową wersję mojej klasy repozytorium, do której można uzyskiwać zapytania w podobny sposób (ponownie jest to środowisko typu ORM i nie byłoby to istotne w przypadku prawdziwa baza danych), ponieważ wykrywanie dokładnych odpowiedzi na zapytania jest bardzo pracochłonne i często zapewnia jedynie niewielkie korzyści.

Należy jednak zachować ostrożność podczas testów jednostkowych:

  • Upewnij się, że twoje kpiny kpiną . Klasy, które wykonują operacje wokół testowanej klasy, muszą być obiektami próbnymi, jeśli wykonujesz testy jednostkowe. Twoje klasy typu DTO / encja mogą być prawdziwe, ale jeśli klasy wykonują operacje, musisz je wyśmiewać - w przeciwnym razie, gdy zmieni się kod pomocniczy i testy zaczną się nie udać, musisz dużo dłużej szukać, aby dowiedzieć się, która zmiana faktycznie spowodował problem.
  • Upewnij się, że testujesz swoje zajęcia . Czasami, gdy przeglądamy pakiet testów jednostkowych, staje się oczywiste, że połowa testów faktycznie testuje frameworki bardziej niż rzeczywisty kod, który powinny być testowane.
  • Nie używaj ponownie fałszywych / pomocniczych obiektów To wielka sprawa - kiedy ktoś zaczyna próbować być sprytnym dzięki testom jednostkowym obsługującym kod, naprawdę łatwo jest przypadkowo stworzyć obiekty, które utrzymują się między testami, co może mieć nieprzewidywalne skutki. Na przykład wczoraj miałem test, który zdał, gdy został uruchomiony samodzielnie, zdany, gdy wszystkie testy w klasie zostały uruchomione, ale nie powiódł się, gdy uruchomiono cały zestaw testów. Okazało się, że w pomocniku testowym znajdował się podstępny obiekt statyczny, który, gdy go stworzyłem, na pewno nigdy nie spowodowałby problemu. Pamiętaj tylko: na początku testu wszystko jest tworzone, pod koniec testu wszystko jest niszczone.
glenatron
źródło
10

Cokolwiek czyni intencję twojego testu bardziej czytelnym.

Zgodnie z ogólną zasadą:

Jeśli dane są częścią testu (np. Nie powinny wypisywać wierszy ze stanem 7), to zakoduj je w teście, aby było jasne, co autor zamierzał zrobić.

Jeśli dane są tylko wypełniacz, aby upewnić się, że ma coś do pracy (np. Nie powinno znaku rejestrze jako kompletne, jeżeli usługa przetwarzania rzuca wyjątek), a następnie za wszelką cenę mieć metodę BuildDummyData lub klasę testową, która utrzymuje nieistotnych danych z testu .

Ale zauważ, że staram się wymyślić dobry przykład tego drugiego. Jeśli masz ich wiele w urządzeniu do testów jednostkowych, prawdopodobnie masz inny problem do rozwiązania ... być może testowana metoda jest zbyt skomplikowana.

pdr
źródło
+1 Zgadzam się. Pachnie to tym, co testuje jest ściśle powiązany z testowaniem jednostkowym.
Reactgular,
5

Różne metody testowania

Najpierw określ, co robisz: testy jednostkowe lub testy integracyjne . Liczba warstw nie ma znaczenia dla testów jednostkowych, ponieważ najprawdopodobniej testowana jest tylko jedna klasa. Reszta wyśmiewasz. W przypadku testów integracji nieuniknione jest testowanie wielu warstw. Jeśli masz dobre testy jednostkowe, sztuczka polega na tym, aby testy integracyjne nie były zbyt skomplikowane.

Jeśli twoje testy jednostkowe są dobre, nie musisz powtarzać testowania wszystkich szczegółów podczas testowania integracji.

Warunki, których używamy, są nieco zależne od platformy, ale można je znaleźć na prawie wszystkich platformach testowych / programistycznych:

Przykładowa aplikacja

W zależności od technologii, której używasz, nazwy mogą się różnić, ale wykorzystam to jako przykład:

Jeśli masz prostą aplikację CRUD z modelem Product, ProductsController i widokiem indeksu, który generuje tabelę HTML z produktami:

Efektem końcowym aplikacji jest wyświetlenie tabeli HTML z listą wszystkich aktywnych produktów.

Testów jednostkowych

Model

Model, który możesz dość łatwo przetestować. Istnieją na to różne metody; używamy urządzeń. Myślę, że to właśnie nazywacie „fałszywymi zestawami danych”. Dlatego przed każdym uruchomieniem testu tworzymy tabelę i wstawiamy oryginalne dane. Większość platform ma na to metody. Na przykład w klasie testowej metoda setUp () uruchamiana przed każdym testem.

Następnie uruchamiamy nasz test, na przykład: produkty testGetAllActive .

Testujemy więc bezpośrednio do testowej bazy danych. Nie kpimy z źródła danych; sprawiamy, że zawsze jest tak samo. Dzięki temu możemy na przykład przetestować nową wersję bazy danych i pojawią się wszelkie problemy z zapytaniami.

W prawdziwym świecie nie zawsze możesz podążać w 100% za pojedynczą odpowiedzialnością . Jeśli chcesz to zrobić jeszcze lepiej, możesz użyć źródła danych, które wyśmiewasz. Dla nas (używamy ORM), który wydaje się testowaniem już istniejącej technologii. Również testy stają się znacznie bardziej złożone i tak naprawdę nie testują zapytań. Trzymamy to w ten sposób.

Zaszyfrowane dane są osobno przechowywane w urządzeniach. To urządzenie jest jak plik SQL z instrukcją tworzenia tabeli i wstawkami do rekordów, których używamy. Utrzymujemy je małe, chyba że istnieje rzeczywista potrzeba testowania z dużą ilością zapisów.

class ProductModel {
  public function getAllActive() {
    return $this->find('all', array('conditions' => array('active' => 1)));
  }
}

Kontroler

Kontroler wymaga więcej pracy, ponieważ nie chcemy z nim testować modelu. Więc to, co robimy, to kpina z modelu. Oznacza to: Testujemy metodę: index (), która powinna zwrócić listę rekordów.

Wyśmiewamy więc metodę modelową getAllActive () i dodajemy do niej ustalone dane (na przykład dwa rekordy). Teraz testujemy dane, które kontroler wysyła do widoku i porównujemy, czy naprawdę odzyskamy te dwa rekordy.

function testProductIndexLoggedIn() {
  $this->setLoggedIn();
  $this->ProductsController->mock('ProductModel', 'index', function(return array(your records) ));
  $result=$this->ProductsController->index();
  $this->assertEquals(2, count($result['products']));
}

Wystarczy. Staramy się dodawać jak najmniej funkcji do kontrolera, ponieważ utrudnia to testowanie. Ale oczywiście zawsze jest w nim trochę kodu. Na przykład testujemy wymagania takie jak: Pokaż te dwa rekordy tylko wtedy, gdy jesteś zalogowany.

Tak więc kontroler potrzebuje normalnie jednej próbki i niewielkiej ilości zakodowanych danych. Dla systemu logowania może być inny. W naszym teście mamy do tego metodę pomocniczą: setLoggedIn (). Ułatwia to testowanie przy użyciu logowania lub bez logowania.

class ProductsController {
  public function index() {
    if($this->loggedIn()) {
      $this->set('products', $this->ProductModel->getAllActive());
    }
  }
}

Widoki

Testowanie widoków jest trudne. Najpierw wyodrębniamy logikę, która się powtarza. Umieszczamy to w Pomocnikach i testujemy te klasy w ścisły sposób. Oczekujemy zawsze takiej samej wydajności. Na przykład: generujHtmlTableFromArray ().

Następnie mamy pewne widoki specyficzne dla projektu. Nie testujemy ich. Testowanie jednostkowe nie jest tak naprawdę pożądane. Trzymamy je do testów integracyjnych. Ponieważ wyodrębniliśmy dużą część kodu do widoków, mamy tutaj mniejsze ryzyko.

Jeśli zaczniesz testować te, prawdopodobnie będziesz musiał zmieniać testy za każdym razem, gdy zmieniasz fragment HTML, który nie jest przydatny w większości projektów.

echo $this->tableHelper->generateHtmlTableFromArray($products);

Testy integracyjne

W zależności od platformy możesz tutaj pracować z historiami użytkowników itp. Może to być strona internetowa Selenium lub inne porównywalne rozwiązania.

Ogólnie po prostu ładujemy bazę danych z urządzeniami i stwierdzamy, które dane powinny być dostępne. Do pełnego testowania integracji używamy ogólnie bardzo globalnych wymagań. Tak: Ustaw produkt jako aktywny, a następnie sprawdź, czy produkt stanie się dostępny.

Nie testujemy wszystkiego ponownie, na przykład, czy dostępne są odpowiednie pola. Tutaj testujemy większe wymagania. Ponieważ nie chcemy powielać naszych testów ze sterownika lub widoku. Jeśli coś jest naprawdę kluczową / podstawową częścią aplikacji lub ze względów bezpieczeństwa (sprawdź, czy hasło NIE jest dostępne), dodajemy je, aby upewnić się, że jest poprawne.

Zaszyfrowane dane są przechowywane w urządzeniach.

function testIntegrationProductIndexLoggedIn() {
  $this->setLoggedIn();
  $result=$this->request('products/index');

  $expected='<table';
  $this->assertContains($expected, $result);

  // Some content from the fixture record
  $expected='<td>Product 1 name</td>';
  $this->assertContains($expected, $result);
}
Luc Franken
źródło
To świetna odpowiedź na zupełnie inne pytanie.
pdr
Dzięki za opinie. Być może masz rację, że nie wspomniałem o tym zbyt konkretnie. Powodem pełnej odpowiedzi jest to, że widzę jedną z najtrudniejszych rzeczy podczas testowania w zadanym pytaniu. Przegląd tego, jak testowanie w izolacji pasuje do różnych rodzajów testów. Dlatego dodałem w każdej części, w jaki sposób dane są przetwarzane (lub rozdzielane). Zobaczę, czy mogę to wyjaśnić.
Luc Franken
Odpowiedź została zaktualizowana o kilka przykładów kodu, aby wyjaśnić, jak testować bez wywoływania wszelkiego rodzaju innych klas.
Luc Franken
4

Jeśli piszesz testy, które wymagają dużej ilości DI i okablowania, aż do korzystania z „prawdziwych” źródeł danych, prawdopodobnie opuściłeś obszar zwykłych testów jednostkowych i wszedłeś w domenę testów integracyjnych.

Myślę, że w przypadku testów integracyjnych wspólna logika konfiguracji danych nie jest złym pomysłem. Głównym celem takich testów jest udowodnienie, że wszystko jest poprawnie skonfigurowane. Jest to raczej niezależne od konkretnych danych przesyłanych przez twój system.

Z drugiej strony, w przypadku testów jednostkowych, zalecałbym, aby cel klasy testowej stanowił jedną „prawdziwą” klasę i wyśmiewał wszystko inne. Następnie powinieneś naprawdę zakodować dane testowe, aby upewnić się, że zakryłeś jak najwięcej ścieżek błędów specjalnych / poprzednich błędów.

Aby dodać do testów półokreślony / losowy element, lubię wprowadzać fabryki modeli losowych. W teście z wykorzystaniem instancji mojego modelu używam następnie tych fabryk, aby utworzyć prawidłowy, ale całkowicie losowy obiekt modelu, a następnie na stałe zakodować tylko te właściwości, które są interesujące dla danego testu. W ten sposób określasz wszystkie istotne dane bezpośrednio w teście, oszczędzając jednocześnie potrzeby określania wszystkich nieistotnych danych i (w pewnym stopniu) testowania, czy nie ma niezamierzonych zależności od innych pól modelu.

Sven Amann
źródło
-1

Wydaje mi się, że kodowanie większości danych do testów jest dość powszechne.

Rozważ prostą sytuację, w której określony zestaw danych powoduje wystąpienie błędu. Możesz specjalnie utworzyć test jednostkowy dla tych danych, aby wykonać poprawkę i upewnić się, że błąd nie wróci. Z czasem twoje testy będą miały zestaw danych, które obejmują wiele przypadków testowych.

Wstępnie zdefiniowane dane testowe pozwalają również na zbudowanie zestawu danych obejmującego szeroki i znany zakres sytuacji.

To powiedziawszy, myślę, że warto mieć trochę przypadkowych danych w swoich testach.

Sasbury
źródło
Czy naprawdę przeczytałeś pytanie, a nie tylko tytuł?
Jakob
wartość polegająca na tym, że w twoich testach są jakieś losowe dane - Tak, ponieważ nie ma nic lepszego niż ustalenie, co wydarzyło się w teście, gdy raz w tygodniu kończy się niepowodzeniem.
pdr
Przydaje się mieć w swoich testach losowe dane do testów hazing / fuzzing / input. Ale nie w testach jednostkowych byłby to koszmar.
glenatron