Testy jednostkowe w Django

12

Naprawdę trudno mi napisać skuteczne testy jednostkowe dla dużego projektu Django. Mam dość dobry zasięg testów, ale zdałem sobie sprawę, że testy, które piszę, są zdecydowanie testami integracji / akceptacji, a nie testami jednostkowymi, i mam krytyczne części mojej aplikacji, które nie są testowane skutecznie. Chcę to naprawić jak najszybciej.

Oto mój problem. Mój schemat jest głęboko relacyjny i mocno zorientowany na czas, co daje mojemu obiektowi modelowemu wysokie sprzężenie wewnętrzne i dużo stanu. Wiele moich metod modelowych korzysta z zapytań opartych na przedziałach czasowych i mam dużo do czynienia z auto_now_addpolami oznaczonymi czasem . Więc weź metodę, która wygląda następująco:

def summary(self, startTime=None, endTime=None):
    # ... logic to assign a proper start and end time 
    # if none was provided, probably using datetime.now()

    objects = self.related_model_set.manager_method.filter(...)

    return sum(object.key_method(startTime, endTime) for object in objects)

Jak podejść do testowania czegoś takiego?

Oto gdzie jestem do tej pory. Przyszło mi do głowy, że celowi testowania jednostkowego należy nadać pewne kpiny by key_methodz jego argumentów, czy summarypoprawnie filtruje / agreguje, aby uzyskać poprawny wynik?

Wyśmiewanie datetime.now () jest dość proste, ale jak mogę wyśmiewać resztę zachowania?

  • Mógłbym używać urządzeń, ale słyszałem plusy i minusy używania urządzeń do budowania moich danych (słaba konserwacja to dla mnie oszustwo, które uderza mnie w domu).
  • Mógłbym również skonfigurować moje dane za pośrednictwem ORM, ale to może być ograniczenie, ponieważ wtedy muszę również tworzyć powiązane obiekty. A ORM nie pozwala auto_now_addręcznie zadzierać z polami.
  • Wyśmiewanie ORM to kolejna opcja, ale nie tylko trudne jest wyśmiewanie głęboko zagnieżdżonych metod ORM, ale logika w kodzie ORM zostaje wyśmiewana z testu, a wyśmiewanie wydaje się naprawdę uzależniać test od wewnętrznych elementów i zależności testowana funkcja.

Najtrudniejsze do zgryzienia wydają się takie funkcje, które są oparte na kilku warstwach modeli i funkcjach niższego poziomu i są bardzo zależne od czasu, nawet jeśli funkcje te nie są zbyt skomplikowane. Moim ogólnym problemem jest to, że bez względu na to, jak wydaje mi się to kroić, moje testy wyglądają na znacznie bardziej złożone niż funkcje, które testują.

acjay
źródło
Najpierw powinieneś napisać testy jednostkowe, które pomogą wykryć problemy z testowalnością w projekcie przed napisaniem rzeczywistego kodu produkcyjnego.
Chedy2149,
2
Jest to pomocne, ale tak naprawdę nie rozwiązuje problemu najlepszego testowania z natury stanowych aplikacji obciążonych ORM.
acjay
Musisz wyodrębnić warstwę uporczywości
Chedy2149,
1
Brzmi świetnie hipotetycznie, ale jeśli chodzi o utrzymanie projektu, myślę, że wstawienie niestandardowej warstwy trwałości między logiką biznesową i wyjątkowo dobrze udokumentowaną Django ORM jest nietrudne. Nagle klasy zapełniają się mnóstwem drobnych metod pośrednich, które z czasem trzeba ponownie opracować. Być może jest to jednak uzasadnione w miejscach, w których testowalność ma kluczowe znaczenie.
acjay
Sprawdź

Odpowiedzi:

6

Zamierzam napisać odpowiedź na to, co do tej pory wymyśliłem.

Moja hipoteza jest taka, że ​​dla funkcji z głębokim sprzężeniem i stanem rzeczywistość jest taka, że ​​po prostu potrzeba wielu linii do kontrolowania jej zewnętrznego kontekstu.

Oto z grubsza mój przypadek testowy, w oparciu o standardową bibliotekę próbną:

  1. Używam standardowego ORM do ustawiania sekwencji zdarzeń
  2. Tworzę własny początek datetimei podważam auto_now_addczasy, aby dopasować je do ustalonej osi czasu mojego projektu. Myślałem, że ORM na to nie pozwala, ale działa dobrze.
  3. Upewniam się, że testowana funkcja używa, from datetime import datetimeaby można było wstawić datetime.now()tylko tę funkcję (jeśli wyśmiewam całą datetimeklasę, ORM sprawdza dopasowanie).
  4. Tworzę własny zamiennik dla object.key_method(), z prostą, ale dobrze zdefiniowaną funkcjonalnością, która zależy od argumentów. Chcę, aby zależało to od argumentów, ponieważ w przeciwnym razie mógłbym nie wiedzieć, czy logika testowanej funkcji działa. W moim przypadku po prostu zwraca liczbę sekund między startTimea endTime. I łatam to, owijając ją w lambda i łatając bezpośrednio do object.key_method()używania new_callablekwarg z patch.
  5. Na koniec uruchamiam serię twierdzeń dla różnych wywołań summaryz różnymi argumentami, aby sprawdzić równość z oczekiwanymi ręcznie obliczonymi wynikami uwzględniającymi dane zachowanie próbnegokey_method

Nie trzeba dodawać, że jest to znacznie dłuższe i bardziej skomplikowane niż sama funkcja. To zależy od DB i tak naprawdę nie wydaje się testem jednostkowym. Ale jest również dość oddzielony od wewnętrznych funkcji - tylko jej sygnatury i zależności. Myślę więc, że to może być test jednostkowy.

W mojej aplikacji funkcja jest bardzo istotna i podlega refaktoryzacji w celu zoptymalizowania jej wydajności. Myślę więc, że kłopoty są tego warte, pomimo złożoności. Ale wciąż jestem otwarty na lepsze pomysły, jak do tego podejść. Cała część mojej długiej podróży w kierunku bardziej opartego na testach stylu rozwoju ...

acjay
źródło