Witajcie,
Dzisiaj mam do was kilka pytań dotyczących testów jednostkowych baz danych.
W jaki sposób wyposażyć bazę danych o testowe rekordy przed przystąpieniem do testu? Wiem że można to zrobić za pomocą metody z adnotacją @Before, lecz adnotacja ta wykonuje się przed każdym testem. Ja potrzebuję tych samych danych do każdego testu, dlatego nie ma sensu (moim zdaniem) ich za każdym razem zapisywać i usuwać z bazy.
Co testować związanego z bazą?
Operacje na encjach, ich walidacje?
a nie raczej testy integracyjne?
DBunit?
- Zapoznaj się na czym polega test jednostkowy: że jest to testowanie w izolacji w odłączeniu od środowiska i innych klas (a więc o bazie danych nie ma mowy).
- Poczytaj o testach integracyjnych, aby zrozumieć ideę (nie zaszkodzi również zapoznać się z takimi aplikacjami jak Jenkins, gdyż testy integracyjne długo się wykonują i często są wykonywane w specjalnych środowiskach testowych).
- Skupiłbym się na nauce testowania jednostkowego i logiki biznesowej (mocki). W tym celu polecam zestaw:
- TestNG (umożliwia grupowanie testów na szybkie jednostkowe i wolne integracyjne, co może być przydatne jak w przyszłości zechcesz testować integracyjnie), wspiera testy parametryczne out-of-box (w przeciwieństwie do JUnit, gdzie dla testów parametrycznych trzeba instalować dodatek JUnitParams, który potrafi się gryźć z innymi rozszerzeniami)
- Mockito: umożliwia mockowanie i testowanie interakcji między obiektami (testy jednostkowe): to dobra inwestycja
Osobiście odradzam zaczynanie od testów integracyjnych. To już jest testowanie wyższego rzędu. Aby to robić musisz dobrze rozumieć, co chcesz osiągnąć i ile Cię to kosztuje: testy integracyjne są drogie!
- Jeśli koniecznie chcesz mieć testy integracyjne poczytaj o:
- Arquillian w środowisku JEE (ja tak testowałem integracyjnie EJB korzystając z embeeded serwera aplikacyjnego)
- Spring Testing w środowisku Spring
- embeeded EJB container (warto wiedzieć, że jest, ale w praktyce to jednak wygodniej mieć Arquilliana)
- Zakładam, że Twój use-case to załadowanie kilku rekordów do bazy. Następnie sprawdzanie czy zapytania zwracają to co chcesz. Po zakończeniu testu, chcesz aby rekordów nie było. Ja takie rzeczy robiłem z Arquillianem. Po każdym teście jest robiony rollback. To samo uzyskałem również ze Spring Testing (który był szybszy niż Arquillian).
http://antoniogoncalves.org/2012/01/16/wytiwyr-what-you-test-is-what-you-run/
Piszę aktualnie proste forum ucząc się springa i ogólnie pisania aplikacji klasy enterprise.
Testy jednostkowe wykorzystywałem do tej pory jedynie w sprawdzeniu czy coś działa, tak żeby nie czekać za każdym razem na deploy aplikacji. Dlatego też moje doświadczenie z nimi jest znikome. Zdaję sobie również sprawę że nie do końca odpowiednio je wykorzystywałem. O mockowaniu też troche czytałem i mockowałem kilka obiektów do użytku w tych testach.
Rozumiem że testy integracyjne to takie, które mają zależności z innymi obiektami? Podczas testów jednostkowych testujemy zachowanie tylko jednego bez relacji z innymi?
Zaprogramowany napisał(a):
Rozumiem że testy integracyjne to takie, które mają zależności z innymi obiektami?
Tak. Co więcej prawidłowy test integracyjny może być powiązany ze środowiskiem (i bardzo często jest). Test integracyjny rozumie np. dependency injection tzn. potrafi wstrzyknąć prawdziwą zależność, a nie tylko mocka wprowadzonego przez testera. Test integracyjny ma też możliwość operwania na prawdziwych zasobach kontekstowych jak z połączeniami z bazą danych czy Web Services. Oczywistą rzeczą jest, że test integracyjny będzie się dłużej wykonywał, a także jest bardziej wrażliwy (bo poza kodem testuje też działanie konfiguracji). Test integracyjny wymaga, aby był wykonywany w środowisku w którym istnieje dostęp do bazy danych (i odpowiednich danych, jeśli dotyczy bazy). Oczywiście test integracyjny daje znacznie szerszy obraz niż jednostkowy, ale więcej kosztuje. Jest też rzadziej uruchamiany.
Zaprogramowany napisał(a):
Podczas testów jednostkowych testujemy zachowanie tylko jednego bez relacji z innymi?
Tak się robi w prawidłowych testach jednostkowych (w zasadzie do tego służy dependency injection, aby można było testować w izolacji, a zależności zastąpić mockami). Po co? Aby błędy w jednej klasie nie propagowały się do innej klasy. Gdyby błędy się propagowały z jednej klasy do drugiej (jak w testach integracyjnych) nagle mogłoby się okazać, że drobny błąd spowoduje, że większość kodu testów w projekcie wykona się nieprawidłowo i np. nie można zrobić refactoringu. Dzięki izolacji można łatwiej wykryć, gdzie doszło do uszkodzenia i zlokalizować usterkę.
Kolejna rzecz to regularność i czas uruchamiania. W zasadzie testy jednostkowe odpalamy za każdym budowaniem projektu, niezależnie od tego czy mamy działające środowisko uruchomieniowe czy nie (odłączenie od kontekstu). Dlatego muszą być szybkie, nie ma mowy o regularnym łączeniu się z bazą (mvn test).
Przeczytałem artykuł, który mi podesłałeś.
Autor próbuje przekonać że testy jednostkowe są bezużyteczne, ponieważ musimy mockować wiele obiektów. Załóżmy że tak jak w przykładzie podanym przez niego, potrzebuję przetestować walidacje encji podczas dodawania jej do bazy. Chcąc sprawdzić taką rzecz, muszę mockować mnóstwo obiektów (takich jak entity manager).
W takiej sytuacji powinienem wykonać test jednostkowy i mockować te obiekty, czy integracyjny?
IMO autor po prostu chciał powiedzieć, że nie wszystko wytestujesz jednostkowo i pokazał klasyczny przykład: DAO (ja do takich rzeczy używałem Arquilliana). Mimo to polecam skupić się na unit testach i mockito: nauczą odpowiedniego myślenia. Ważne, aby zrozumieć też kiedy nie należy ich pisać (a EntityManager to dobry przykład).
Tam gdzie masz logikę testy integracyjne to często marnotrastwo zasobów i dochodzą problemy, o których wspomniałem (propagacje błędów na inne obiekty w testach).
Do testowania walidacji encji (bean validation) nadaje się test integracyjny. Do logiki (która nie zależy od kontenera) nadaje się test jednostkowy.
http://www.adam-bien.com/roller/abien/entry/unit_integration_testing_the_bean
Dodatkowo taki Arquillian za każdym razem tworzy paczkę i deploy'uje ją do środowiska, w którym może wykonać test. To mocno zniechęca.
Mógłbyś podesłać jeszcze jakieś dobre artykuły do przeczytania i jakiś dobrze otestowany kod na githubie?
Na szybko mogę polecić książkę Kaczanowskiego z przykładami "Good Tests, Bad Tests" z realnych projektów (można pobrać za free):
http://practicalunittesting.com/btgt.php
To jest całkiem fajna prezentacja:
Dzięki za porady, odezwę się jak oczytam się i osłucham się w tych źródłach i będę miał jeszcze jakieś pytania, wątpliwości :)
Oglądnąłem wykład, przeczytałem książke i wydaje mi się że powoli zaczynam rozumieć o co w tym wszystkim chodzi. Napisałem test jednostkowy convertera obiektu InetAddress do Integer. Wygląda on tak:
public class InetAddressToIntegerConverterTest {
private static final byte[] IP_TEST_BYTES = new byte[]{127, 0, 0, 1};
private static final Integer IP_TEST_INT = 2130706433;
@Test
public void shouldConvertIntegerToInetAddress() {
InetAddressToIntegerConverter converter = new InetAddressToIntegerConverter();
InetAddress inetAddress = converter.convertToEntityAttribute(IP_TEST_INT);
Assert.assertEquals(inetAddress.getAddress(), IP_TEST_BYTES);
}
@Test
public void shouldConvertInetAddressToInteger() {
InetAddress address = Mockito.mock(InetAddress.class);
Mockito.when(address.getAddress()).thenReturn(IP_TEST_BYTES);
InetAddressToIntegerConverter converter = new InetAddressToIntegerConverter();
Integer intIP = converter.convertToDatabaseColumn(address);
Assert.assertEquals(intIP, IP_TEST_INT);
}
}
Jakieś uwagi, sugestie?
- Test nie mówi o tym co się stanie, jeśli zostanie wprowadzene nieprawidłowe wejście. Czy nie powinniśmy się spodziewać np. IllegalArgumentException (Runtime)? Sugeruje wprowadzić test-case dla nieprawidłowego wejścia-wyjścia np.
shoultThrowIllegalArgumentExceptionForInvalidInput i sprawdzać czy rzuca odpowiedni wyjątek @Test(expected = Exception.class). - Dla skrócenia i poprawy czytelności kodu warto stosować statyczne importy np. assertEquals zamiast Assert.assertEquals oraz mock zamiast Mockito.mock.
- W zasadzie w obu testach testujesz ten sam obiekt. Warto stosować nazwę SYSTEM UNDER TEST, bo osoba z zewnątrz od razu widzi o co chodzi. Wystarczy też utworzyć jeden obiekt testowany, bo każdy @Test to oddzielny program, który wykonuje się niezależnie (nie ma przechowywania stanu między testami, przynajmniej w typowych unit-testach): każdy test widzi klase testującą w tym samym stanie początkowym.
- Zawsze można poprawić nazewnictwo spodziewaych danych np. EXPECTED_VALID_IP_ADDRESS itp.
- Mockito niekoniecznie jest tu wymagane. Zdaje się, że można utworzyć obiekt normalnie:
http://stackoverflow.com/questions/5719449/creating-inetaddress-object-in-java
Owszem, czasem przydaje się mockować parametr wejściowy, w szczególności, gdy jest interfejsem i nie można sobie stworzyć takiego wejścia.
public class InetAddressToIntegerConverterTest {
private static final byte[] IP_TEST_BYTES = new byte[]{127, 0, 0, 1};
private static final Integer IP_TEST_INT = 2130706433;
// system under test
InetAddressToIntegerConverter sut = new InetAddressToIntegerConverter();
@Test
public void shouldConvertIntegerToInetAddress() {
InetAddress inetAddress = sut.convertToEntityAttribute(IP_TEST_INT);
assertEquals(inetAddress.getAddress(), IP_TEST_BYTES);
}
@Test
public void shouldConvertInetAddressToInteger() {
InetAddress address = mock(InetAddress.class);
when(address.getAddress()).thenReturn(IP_TEST_BYTES);
Integer intIP = sut.convertToDatabaseColumn(address);
assertEquals(intIP, IP_TEST_INT);
}
}
Finalna wersja wygląda tak:
public class InetAddressToIntegerConverterTest {
private static InetAddress INET_ADDRESS;
private static final byte[] BYTES_INET_ADDRESS = new byte[]{127, 0, 0, 1};
private static final Integer INTEGER_INET_ADDRESS = 2130706433;
private static final InetAddressToIntegerConverter CONVERTER = new InetAddressToIntegerConverter();
@BeforeClass
public static void setup() throws UnknownHostException {
INET_ADDRESS = InetAddress.getByAddress(BYTES_INET_ADDRESS);
}
@Test
public void shouldConvertIntegerToInetAddress() {
InetAddress inetAddress = CONVERTER.convertToEntityAttribute(INTEGER_INET_ADDRESS);
assertEquals(inetAddress.getAddress(), INET_ADDRESS.getAddress());
}
@Test
public void shouldConvertInetAddressToInteger() {
Integer intIP = CONVERTER.convertToDatabaseColumn(INET_ADDRESS);
assertEquals(intIP, INTEGER_INET_ADDRESS);
}
@Test(expectedExceptions = NullPointerException.class)
public void shouldThrowNullPointerExceptionForInvalidInputIntegerToConvert() {
CONVERTER.convertToDatabaseColumn(null);
}
@Test(expectedExceptions = NullPointerException.class)
public void shouldThrowNullPointerExceptionForInvalidInputInetAddressToConvert() {
CONVERTER.convertToDatabaseColumn(null);
}
}
Jeszcze coś poprawić? Czy wszystko ok?
- Trochę się nie zrozumieliśmy. To sprawdzanie dla NullPointerException kompletnie nie ma sensu. Miałem na myśli zrobienie walidacji wejścia:
http://stackoverflow.com/questions/15208544/when-should-an-illegalargumentexception-be-thrown
Jeśli już to dla IllegalArgumentException, który sam świadomie w zaprojektowanej przez siebie klasie (sut) rzucasz. Aby sprawdzić czy na pewno został rzucony kiedy chciałeś. - Pisałem, aby sut nie był statyczny. A Ty zrobiłeś odwrotnie.
- Kieruj się może zasadą, aby lepiej tych staticow nie używać (mniejsze ryzyko, że nieświadomie wprowadzisz trudne do wykrycia błędy). Kiedyś zrozumiesz: chpdozi o to, aby przypadkiem nie zepsuć izolacji między testami.
public class InetAddressToIntegerConverterTest {
private InetAddress INET_ADDRESS;
private byte[] BYTES_INET_ADDRESS = new byte[]{127, 0, 0, 1};
private Integer INTEGER_INET_ADDRESS = 2130706433;
private InetAddressToIntegerConverter sut = new InetAddressToIntegerConverter();
@BeforeMethod(alwaysRun = true)
public void init() throws UnknownHostException {
INET_ADDRESS = InetAddress.getByAddress(BYTES_INET_ADDRESS);
}
@Test
public void shouldConvertIntegerToInetAddress() {
InetAddress inetAddress = sut.convertToEntityAttribute(INTEGER_INET_ADDRESS);
assertEquals(inetAddress.getAddress(), INET_ADDRESS.getAddress());
}
@Test
public void shouldConvertInetAddressToInteger() {
Integer intIP = sut.convertToDatabaseColumn(INET_ADDRESS);
assertEquals(intIP, INTEGER_INET_ADDRESS);
}
}
Zabrałem się za pisanie kolejnego testu - tym razem Controllera. Z wykładu, który został podlinkowany wcześniej wynika, że powinienem w jednym teście testować jedną funkcjonalność. Co w sytuacji kiedy potrzebuję mieć testowe obiekty do wykonania testu, ale z tych obiektów korzysta tylko jeden test? Initializować je bezpośrednio w teście? Wydzielić klasę dla tego jednego testu? Bo rozumiem że nie ma sensu robić tego w metodzie oznaczonej @BeforeTest skoro tylko 1/100 testów będzie korzystać z tych pól.
Zaprogramowany napisał(a):
Zabrałem się za pisanie kolejnego testu - tym razem Controllera.
Można testować kontrolery, jeśli są tam jakieś ifki. Np. warunkowa nawigacja w zależności od warunku. Pamiętaj jednak, że typowym miejscem, gdzie testy sie szczególnie przydatne są services z logiką biznesową. Kontrolery z reguły są proste i zwykle nie ma tam zbyt dużo logiki do testowania (bo nie ma if'ków). Testy w kontrolerach na pewno rzadziej się przydadza jak w typowo biznesowych services.
Zaprogramowany napisał(a):
Z wykładu, który został podlinkowany wcześniej wynika, że powinienem w jednym teście testować jedną funkcjonalność. Co w sytuacji kiedy potrzebuję mieć testowe obiekty do wykonania testu, ale z tych obiektów korzysta tylko jeden test? Initializować je bezpośrednio w teście?
Tak. Ogólnie możesz sobie zastosować taki schemat pseudokodem
@Test
metoda:
// warunki poczatkowe: tworzenie obiektow i konfiguracja mockow na potrzeby testu
// wywolanie metody na SUT
// sprawdzenie asercji i / lub interakcji z mockami
Zaprogramowany napisał(a):
Wydzielić klasę dla tego jednego testu? Bo rozumiem że nie ma sensu robić tego w metodzie oznaczonej @BeforeTest skoro tylko 1/100 testów będzie korzystać z tych pól.
Wydzielenie oddzielnej klasy na jeden test nie ma sensu. Poza tym stracisz mozliwosc szybkiej nawigacji w IDE miedzy klasa testujaca, a testowana. Uwzlegnij warunki poczatkowe wedlug schematu w pseudokodzie.
W controllerze chcę testować czy odpowiednie elementy zostały dodane do widoku i czy mają one odpowiednie wartości zgodnie przeliczone z danymi wejściowymi. Tak żeby potem w razie refactoringu gdy o czymś zapomnę posypał się odpowiedni test i nie było problemów podczas renderowania strony
Napisałem klasę do testowania jednej z klas controllerów. Wygląda ona tak:
public class ForumControllerTest {
private ForumController forumController;
private CategorySectionService categorySectionService;
private CategoryService categoryService;
@BeforeTest
public void setup() {
categorySectionService = mock(CategorySectionService.class);
categoryService = mock(CategoryService.class);
forumController = new ForumController(this.categorySectionService, this.categoryService);
}
@Test
public void shouldRenderCategoriesSections() {
List<CategorySection> categories = new ArrayList<>();
categories.add(new CategorySection("foo"));
when(this.categorySectionService.findAllForHomePage()).thenReturn(categories);
ModelMap modelMap = new ModelMap();
this.forumController.index(modelMap);
assertEquals((Object) categories, modelMap.get("category_section"));
}
@Test
public void shouldRenderTopicsForCategory() {
Integer page = 1;
PageRequest request = new PageRequest(page, 20);
Category category = new Category("foo", "foo", "foo");
when(this.categoryService.findOneForCategoryPage(eq(category.getName()), eq(request))).thenReturn(category);
ModelMap modelMap = new ModelMap();
this.forumController.category(modelMap, category.getName(), page);
assertEquals((Object) category, modelMap.get("category_topics"));
}
}
Mam wrażenie że wreszcie zrozumiałem o co w tym chodzi, dlatego proszę o ostatnią ocenę i kończe temat :)
Raczej po mału zmierzasz w dobrym kierunku. DI przez konstuktor jest ok.
Uwagi:
- this obniża czytelność.
- Można zrobić od razu (mock), nie musi byc w @BeforeTest:
private CategorySectionService categorySectionService = mock(CategorySectionService.class);
- W pierwszym teście testujesz, czy do modelMap (które inicjalnie jest puste) trafia lista kategorii zwrócona przez mocka (ale nazwa testu raczej niewiele mówi). W drugim robisz coś podobnego (czy pojawiła się kategoria). Nazwy są słabe. Pamiętaj, że w unit-teście nic się nie renderuje. Możesz raczej pisać nazwy testów, sugerujące, że mapa powinna coś zawierać, a nie że coś wyrenderuje (bo to nie jest prawda): unit test jest głupi i odłączony od kontenera.
- Trzeba się sporo domyślać. Byłoby łatwiej jakbyś na przyszłość pokazał klasę ForumController (zwłaszcza, że rzadko coś robię w MVC).
- Jestem dość sceptycznie nastawiony do testowania takich rzeczy: ale może dlatego, że na codzień działam z JSF i mam inne potrzeby. Może nazwy testu w stylu viewMapShouldContainX będą lepsze? Nie wiem.
- Dla trywialnych testów zapoznaj się z konstrukcjami anyString() any(Object.class). Przyspieszają robotę przy mockowaniu.
- zamiast nazwy 'forumController' sugeruje sut.
- założyłem, że to liniowy kod (kontroler), w którym nie ma żadnych warunków (wiele osób uważa, że to zbyt trywialne, aby to testować podobnie jak gettery i settery). Chyba, że uważasz że to może się zepsuć (wtedy to coś innego).
Pamiętaj, że najważniejsze są if'ki i interakcje. Jeśli chciałeś sprawdzić, czy po wykonaniu metody kontrolera w inicjalnie pustym ViewMap jest odpowiednia wartość to raczej załapałeś ideę mockowania.
Tak, o to mi chodziło. Dzięki w takim razie za tak obszerną pomoc :) Myślę że jeszcze kilka lub kilkadziesiąt testów i dojdę do perfekcji ;)
Pozdrawiam i jeszcze raz dzięki :)