Testowanie baz danych

0

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?

0

a nie raczej testy integracyjne?

DBunit?

2
  1. 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).
  2. 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).
  3. 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!

  1. 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)
  1. 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/

0

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?

1
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).

0

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?

1

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.

0

Mógłbyś podesłać jeszcze jakieś dobre artykuły do przeczytania i jakiś dobrze otestowany kod na githubie?

1

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:

0

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 :)

0

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?

1
  1. 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).
  2. Dla skrócenia i poprawy czytelności kodu warto stosować statyczne importy np. assertEquals zamiast Assert.assertEquals oraz mock zamiast Mockito.mock.
  3. 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.
  4. Zawsze można poprawić nazewnictwo spodziewaych danych np. EXPECTED_VALID_IP_ADDRESS itp.
  5. 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);
    }
 
}
0

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?

1
  1. 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ś.
  2. Pisałem, aby sut nie był statyczny. A Ty zrobiłeś odwrotnie.
  3. 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);
    }
 
}
0

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.

1
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.

0

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

0

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 :)

1

Raczej po mału zmierzasz w dobrym kierunku. DI przez konstuktor jest ok.

Uwagi:

  1. this obniża czytelność.
  2. Można zrobić od razu (mock), nie musi byc w @BeforeTest:
    private CategorySectionService categorySectionService = mock(CategorySectionService.class);
  1. 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.
  2. Trzeba się sporo domyślać. Byłoby łatwiej jakbyś na przyszłość pokazał klasę ForumController (zwłaszcza, że rzadko coś robię w MVC).
  3. 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.
  4. Dla trywialnych testów zapoznaj się z konstrukcjami anyString() any(Object.class). Przyspieszają robotę przy mockowaniu.
  5. zamiast nazwy 'forumController' sugeruje sut.
  6. 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.

0

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 :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1