Jak przeprowadzić test jednostkowy obiektu za pomocą zapytań do bazy danych

153

Słyszałem, że testowanie jednostkowe jest „niesamowite”, „naprawdę fajne” i „wszelkiego rodzaju dobre rzeczy”, ale 70% lub więcej moich plików obejmuje dostęp do bazy danych (niektóre czyta, inne zapisują) i nie wiem jak napisać test jednostkowy dla tych plików.

Używam PHP i Python, ale myślę, że to pytanie dotyczy większości / wszystkich języków, które używają dostępu do bazy danych.

Teifion
źródło

Odpowiedzi:

82

Sugerowałbym wyśmiewanie twoich połączeń z bazą danych. Mocks to w zasadzie obiekty, które wyglądają jak obiekt, do którego próbujesz wywołać metodę, w tym sensie, że mają te same właściwości, metody itp. Dostępne dla wywołującego. Ale zamiast wykonywać dowolną czynność, do której zostały zaprogramowane, gdy wywoływana jest określona metoda, całkowicie ją pomija i po prostu zwraca wynik. Wynik ten jest zwykle definiowany przez Ciebie z wyprzedzeniem.

Aby skonfigurować obiekty do mockowania, prawdopodobnie będziesz musiał użyć jakiegoś rodzaju odwrócenia wzorca iniekcji kontroli / zależności, jak w poniższym pseudokodzie:

class Bar
{
    private FooDataProvider _dataProvider;

    public instantiate(FooDataProvider dataProvider) {
        _dataProvider = dataProvider;
    }

    public getAllFoos() {
        // instead of calling Foo.GetAll() here, we are introducing an extra layer of abstraction
        return _dataProvider.GetAllFoos();
    }
}

class FooDataProvider
{
    public Foo[] GetAllFoos() {
        return Foo.GetAll();
    }
}

Teraz w teście jednostkowym tworzysz makietę FooDataProvider, która umożliwia wywołanie metody GetAllFoos bez konieczności trafiania w bazę danych.

class BarTests
{
    public TestGetAllFoos() {
        // here we set up our mock FooDataProvider
        mockRepository = MockingFramework.new()
        mockFooDataProvider = mockRepository.CreateMockOfType(FooDataProvider);

        // create a new array of Foo objects
        testFooArray = new Foo[] {Foo.new(), Foo.new(), Foo.new()}

        // the next statement will cause testFooArray to be returned every time we call FooDAtaProvider.GetAllFoos,
        // instead of calling to the database and returning whatever is in there
        // ExpectCallTo and Returns are methods provided by our imaginary mocking framework
        ExpectCallTo(mockFooDataProvider.GetAllFoos).Returns(testFooArray)

        // now begins our actual unit test
        testBar = new Bar(mockFooDataProvider)
        baz = testBar.GetAllFoos()

        // baz should now equal the testFooArray object we created earlier
        Assert.AreEqual(3, baz.length)
    }
}

Powszechny scenariusz kpiny, w skrócie. Oczywiście nadal będziesz prawdopodobnie chciał przetestować jednostkowo również rzeczywiste wywołania bazy danych, dla których będziesz musiał trafić do bazy danych.

Doug R.
źródło
Wiem, że to jest stare, ale co z utworzeniem zduplikowanej tabeli do tej, która jest już w bazie danych. W ten sposób możesz potwierdzić, że połączenia DB działają?
bretterer
1
Używam PDO PHP jako mojego najniższego poziomu dostępu do bazy danych, z którego wyodrębniłem interfejs. Następnie zbudowałem na tej podstawie warstwę bazy danych obsługującą aplikacje. Jest to warstwa zawierająca wszystkie nieprzetworzone zapytania SQL i inne informacje. Reszta aplikacji współdziała z tą bazą danych wyższego poziomu. Zauważyłem, że działa to całkiem dobrze w przypadku testów jednostkowych; Testuję moje strony aplikacji pod kątem interakcji z bazą danych aplikacji. Testuję moją bazę danych aplikacji pod kątem interakcji z PDO. Zakładam, że PDO działa bez błędów. Kod źródłowy: manx.codeplex.com
zalegalizuj
1
@bretterer - Utworzenie zduplikowanej tabeli jest dobre do testowania integracji. Do testów jednostkowych zwykle używałbyś pozorowanego obiektu, który pozwoli ci przetestować jednostkę kodu niezależnie od bazy danych.
BornToCode
2
Jaka jest wartość wyśmiewania wywołań bazy danych w testach jednostkowych? Nie wydaje się to przydatne, ponieważ można zmienić implementację, aby zwracała inny wynik, ale test jednostkowy byłby (nieprawidłowo) pozytywny.
bmay2
2
@ bmay2 Nie mylisz się. Moja pierwotna odpowiedź została napisana dawno temu (9 lat!), Kiedy wiele osób nie pisało swojego kodu w testowalny sposób, a narzędzi testujących brakowało. Nie polecałbym już tego podejścia. Dzisiaj po prostu założyłbym testową bazę danych i zapełniłbym ją danymi potrzebnymi do testu i / lub zaprojektowałbym kod tak, żebym mógł przetestować jak najwięcej logiki bez bazy danych.
Doug R
25

Idealnie byłoby, gdyby twoje obiekty były wytrwałymi ignorantami. Na przykład, powinieneś mieć „warstwę dostępu do danych”, do której będziesz wysyłać żądania, które zwracałyby obiekty. W ten sposób możesz pozostawić tę część poza testami jednostkowymi lub przetestować je w izolacji.

Jeśli obiekty są ściśle powiązane z warstwą danych, trudno jest przeprowadzić odpowiednie testy jednostkowe. pierwsza część testu jednostkowego to „jednostka”. Wszystkie jednostki powinny mieć możliwość testowania w izolacji.

W moich projektach C # używam NHibernate z całkowicie oddzielną warstwą danych. Moje obiekty znajdują się w podstawowym modelu domeny i są dostępne z mojej warstwy aplikacji. Warstwa aplikacji komunikuje się zarówno z warstwą danych, jak iz warstwą modelu domeny.

Warstwa aplikacji jest czasami nazywana „warstwą biznesową”.

Jeśli używasz PHP, utwórz określony zestaw klas TYLKO dla dostępu do danych. Upewnij się, że obiekty nie mają pojęcia, w jaki sposób są utrwalane, i połącz je w klasach aplikacji.

Inną opcją byłoby użycie mocking / stubs.

Sean Chambers
źródło
Zawsze się z tym zgadzałem, ale w praktyce ze względu na terminy i „ok, teraz dodaj jeszcze jedną funkcję, do 14.00 dzisiaj” jest to jedna z najtrudniejszych rzeczy do osiągnięcia. Takie rzeczy są jednak głównym celem refaktoryzacji, jeśli mój szef kiedykolwiek zdecyduje, że nie pomyślał o 50 nowych pojawiających się problemach, które wymagają zupełnie nowej logiki biznesowej i tabel.
Darren Ringer
3
Jeśli obiekty są ściśle powiązane z warstwą danych, trudno jest przeprowadzić odpowiednie testy jednostkowe. pierwsza część testu jednostkowego to „jednostka”. Wszystkie jednostki powinny mieć możliwość testowania w izolacji. ładne wyjaśnienie
Amitābha,
11

Najłatwiejszym sposobem testowania jednostkowego obiektu z dostępem do bazy danych jest użycie zakresów transakcji.

Na przykład:

    [Test]
    [ExpectedException(typeof(NotFoundException))]
    public void DeleteAttendee() {

        using(TransactionScope scope = new TransactionScope()) {
            Attendee anAttendee = Attendee.Get(3);
            anAttendee.Delete();
            anAttendee.Save();

            //Try reloading. Instance should have been deleted.
            Attendee deletedAttendee = Attendee.Get(3);
        }
    }

Spowoduje to przywrócenie stanu bazy danych, w zasadzie jak wycofanie transakcji, dzięki czemu możesz uruchomić test tyle razy, ile chcesz, bez żadnych skutków ubocznych. Z powodzeniem stosowaliśmy to podejście w dużych projektach. Nasza kompilacja zajmuje trochę czasu (15 minut), ale posiadanie 1800 testów jednostkowych nie jest straszne. Ponadto, jeśli czas kompilacji jest problemem, możesz zmienić proces kompilacji, aby mieć wiele kompilacji, jedną do budowania src, drugą, która uruchamia się później, która obsługuje testy jednostkowe, analizę kodu, pakowanie itp.

BZ.
źródło
1
+1 Oszczędza dużo czasu podczas testów jednostkowych warstw dostępu do danych. Zwróć uwagę, że TS często potrzebuje usługi MSDTC, co może nie być pożądane (w zależności od tego, czy Twoja aplikacja będzie potrzebować MSDTC)
StuartLC
Pierwotne pytanie dotyczyło PHP, ten przykład wydaje się być w C #. Środowiska są bardzo różne.
zalegalizować
2
Autor pytania stwierdził, że jest to ogólne pytanie odnoszące się do wszystkich języków, które mają coś wspólnego z bazą danych.
Vedran,
9
a to drodzy przyjaciele to testy integracyjne
AA.
10

Być może mogę wam posmakować naszego doświadczenia, kiedy zaczęliśmy przyglądać się testom jednostkowym naszego procesu średniego poziomu, który obejmował mnóstwo operacji SQL „logiki biznesowej”.

Najpierw stworzyliśmy warstwę abstrakcji, która pozwoliła nam „wstawić” dowolne rozsądne połączenie z bazą danych (w naszym przypadku po prostu obsługiwaliśmy pojedyncze połączenie typu ODBC).

Gdy to było na miejscu, mogliśmy zrobić coś takiego w naszym kodzie (pracujemy w C ++, ale jestem pewien, że masz pomysł):

GetDatabase (). ExecuteSQL ("INSERT INTO foo (bla, bla)")

W normalnym czasie wykonywania GetDatabase () zwróciłoby obiekt, który zasilał cały nasz plik sql (w tym zapytania), przez ODBC bezpośrednio do bazy danych.

Następnie zaczęliśmy przyglądać się bazom danych w pamięci - zdecydowanie najlepszym wydaje się być SQLite. ( http://www.sqlite.org/index.html ). Jest niezwykle prosty w konfiguracji i obsłudze, a także pozwolił nam podklasę i przesłonięcie GetDatabase () w celu przekazania sql do bazy danych w pamięci, która została utworzona i zniszczona dla każdego wykonywanego testu.

Wciąż jesteśmy na wczesnym etapie, ale jak na razie wygląda to dobrze, jednak musimy upewnić się, że tworzymy wszystkie wymagane tabele i wypełniamy je danymi testowymi - jednak nieco zmniejszyliśmy obciążenie tutaj, tworząc ogólny zestaw funkcji pomocniczych, które mogą wiele zdziałać za nas.

Ogólnie rzecz biorąc, bardzo pomogło to w naszym procesie TDD, ponieważ wprowadzenie czegoś, co wydaje się dość nieszkodliwymi zmianami w celu naprawienia niektórych błędów, może mieć dość dziwny wpływ na inne (trudne do wykrycia) obszary twojego systemu - ze względu na samą naturę sql / baz danych.

Oczywiście nasze doświadczenia skupiały się wokół środowiska programistycznego C ++, jednak jestem pewien, że być może możesz uzyskać coś podobnego działającego w PHP / Python.

Mam nadzieję że to pomoże.

Alan
źródło
9

Powinieneś mockować dostęp do bazy danych, jeśli chcesz przetestować swoje klasy jednostkowo. W końcu nie chcesz testować bazy danych w teście jednostkowym. To byłby test integracji.

Odebrać wywołania, a następnie wstawić makietę, która po prostu zwraca oczekiwane dane. Jeśli twoje klasy nie robią więcej niż wykonywanie zapytań, może nawet nie warto ich testować, chociaż ...

Martin Klinke
źródło
6

W książce xUnit Test Patterns opisano niektóre sposoby obsługi kodu testów jednostkowych, który trafia do bazy danych. Zgadzam się z innymi ludźmi, którzy mówią, że nie chcesz tego robić, ponieważ jest to powolne, ale musisz to kiedyś zrobić, IMO. Wyszydzenie połączenia db w celu przetestowania rzeczy wyższego poziomu jest dobrym pomysłem, ale przejrzyj tę książkę, aby uzyskać sugestie dotyczące interakcji z rzeczywistą bazą danych.

Chris Farmer
źródło
4

Dostępne opcje:

  • Napisz skrypt, który wyczyści bazę danych przed rozpoczęciem testów jednostkowych, a następnie zapełni bazę danych predefiniowanym zestawem danych i uruchom testy. Możesz to również zrobić przed każdym testem - będzie to powolne, ale mniej podatne na błędy.
  • Wstrzyknij bazę danych. (Przykład w pseudo-Java, ale dotyczy wszystkich języków OO)

    class Database {
     publiczne zapytanie wynikowe (zapytanie łańcuchowe) {... tutaj prawdziwa baza danych ...}
    }

    class MockDatabase rozszerza bazę danych { public Result query (String query) { return "próbny wynik"; } }

    class ObjectThatUsesDB { public ObjectThatUsesDB (baza danych) { this.database = db; } }

    teraz w środowisku produkcyjnym używasz normalnej bazy danych i dla wszystkich testów po prostu wstrzykujesz fałszywą bazę danych, którą możesz utworzyć ad hoc.

  • Nie używaj bazy danych w ogóle przez większość kodu (i tak jest to zła praktyka). Utwórz obiekt "bazy danych", który zamiast zwracać z wynikami, zwróci normalne obiekty (tj. Zwróci Userzamiast krotki {name: "marcin", password: "blah"}) napisz wszystkie testy z rzeczywistymi obiektami skonstruowanymi ad hoc i napisz jeden duży test, który zależy od bazy danych, która zapewnia taką konwersję działa OK.

Oczywiście te podejścia nie wykluczają się wzajemnie i można je dowolnie łączyć i dopasowywać.

Marcin
źródło
3

Testowanie jednostkowe dostępu do bazy danych jest dość łatwe, jeśli projekt ma wysoką spójność i luźne powiązania. W ten sposób możesz przetestować tylko to, co robi każda klasa, bez konieczności testowania wszystkiego naraz.

Na przykład, jeśli wykonujesz testy jednostkowe klasy interfejsu użytkownika, napisane przez Ciebie testy powinny tylko próbować zweryfikować, czy logika wewnątrz interfejsu użytkownika działa zgodnie z oczekiwaniami, a nie logika biznesowa lub akcja bazy danych za tą funkcją.

Jeśli chcesz przetestować jednostkowo rzeczywisty dostęp do bazy danych, skończysz bardziej testem integracji, ponieważ będziesz zależny od stosu sieciowego i serwera bazy danych, ale możesz sprawdzić, czy twój kod SQL robi to, o co go prosiłeś robić.

Dla mnie osobiście ukryta moc testów jednostkowych polega na tym, że zmusza mnie to do projektowania aplikacji w znacznie lepszy sposób niż bez nich. To dlatego, że naprawdę pomogło mi to oderwać się od mentalności „ta funkcja powinna zrobić wszystko”.

Przepraszam, nie mam żadnych konkretnych przykładów kodu dla PHP / Python, ale jeśli chcesz zobaczyć przykład .NET, mam post opisujący technikę, której użyłem do wykonania tych samych testów.

Toran Billups
źródło
2

Zwykle próbuję zrywać testy między testowaniem obiektów (i ORM, jeśli istnieje) i testowaniem bazy danych. Testuję obiektową stronę rzeczy, mockując wywołania dostępu do danych, podczas gdy testuję stronę db, testując interakcje obiektu z bazą danych, która, z mojego doświadczenia, jest zwykle dość ograniczona.

Kiedyś frustrowałem się pisaniem testów jednostkowych, dopóki nie zacząłem kpić z części dostępu do danych, więc nie musiałem tworzyć testowej bazy danych ani generować danych testowych w locie. Mockując dane, możesz wygenerować je wszystkie w czasie wykonywania i mieć pewność, że obiekty działają poprawnie ze znanymi danymi wejściowymi.

akmad
źródło
2

Nigdy nie robiłem tego w PHP i nigdy nie używałem Pythona, ale to, co chcesz zrobić, to wyszydzenie wywołań do bazy danych. Aby to zrobić, możesz zaimplementować IoC niezależnie od tego, czy jest to narzędzie innej firmy, czy zarządzasz nim samodzielnie, a następnie możesz zaimplementować próbną wersję programu wywołującego bazę danych, dzięki której będziesz kontrolować wynik fałszywego połączenia.

Prosta forma IoC może być wykonana po prostu przez kodowanie do interfejsów. Wymaga to pewnej orientacji obiektowej w kodzie, więc może nie mieć zastosowania do tego, co robisz (mówię to, ponieważ wszystko, co muszę zrobić, to twoja wzmianka o PHP i Pythonie)

Mam nadzieję, że to pomocne, jeśli nie masz teraz żadnych terminów do wyszukania.

codeLes
źródło
2

Zgadzam się z pierwszym po - dostęp do bazy danych powinien zostać usunięty do warstwy DAO, która implementuje interfejs. Następnie możesz przetestować swoją logikę pod kątem implementacji kodu pośredniczącego warstwy DAO.

Chris Marasti-Georg
źródło
2

Możesz użyć mocking frameworków, aby wyodrębnić silnik bazy danych. Nie wiem, czy PHP / Python ma jakieś, ale dla języków typowanych (C #, Java itp.) Jest wiele opcji

Zależy to również od tego, jak zaprojektowałeś kod dostępu do bazy danych, ponieważ niektóre projekty są łatwiejsze do testowania jednostkowego niż inne, o których wspominano we wcześniejszych postach.

chakrit
źródło
2

Konfigurowanie danych testowych dla testów jednostkowych może być wyzwaniem.

Jeśli chodzi o Javę, jeśli używasz Spring API do testów jednostkowych, możesz kontrolować transakcje na poziomie jednostki. Innymi słowy, można wykonywać testy jednostkowe, które obejmują aktualizacje / wstawienia / usunięcia bazy danych oraz wycofywanie zmian. Pod koniec wykonywania pozostawiasz wszystko w bazie danych tak, jak było przed rozpoczęciem wykonywania. Dla mnie jest tak dobry, jak to tylko możliwe.

Bino Manjasseril
źródło