Jak radzić sobie ze statycznymi klasami użyteczności przy projektowaniu pod kątem testowalności


62

Staramy się zaprojektować nasz system w taki sposób, aby był testowalny i w większości został opracowany przy użyciu TDD. Obecnie próbujemy rozwiązać następujący problem:

W różnych miejscach konieczne jest stosowanie metod statycznego pomocnika, takich jak ImageIO i URLEncoder (oba standardowe API Java) oraz różnych innych bibliotek, które składają się głównie z metod statycznych (takich jak biblioteki Apache Commons). Jednak niezwykle trudno jest przetestować metody wykorzystujące takie statyczne klasy pomocnicze.

Mam kilka pomysłów na rozwiązanie tego problemu:

  1. Użyj fałszywego frameworka, który potrafi drwić z klas statycznych (np. PowerMock). To może być najprostsze rozwiązanie, ale w jakiś sposób wydaje się, że trzeba się poddać.
  2. Utwórz natychmiastowe klasy opakowań wokół wszystkich tych narzędzi statycznych, aby można je było wstrzykiwać do klas, które ich używają. To brzmi jak stosunkowo czyste rozwiązanie, ale obawiam się, że skończymy z tworzeniem okropnej liczby tych klas opakowań.
  3. Wyodrębnij każde wywołanie tych statycznych klas pomocniczych do funkcji, którą można zastąpić, i przetestuj podklasę klasy, którą faktycznie chcę przetestować.

Ale ciągle myślę, że to musi być problem, z którym wiele osób musi się zmierzyć podczas TDD - więc muszą już być rozwiązania tego problemu.

Jaka jest najlepsza strategia umożliwiająca testowanie klas korzystających z tych statycznych pomocników?


Nie jestem pewien, co rozumiesz przez „wiarygodne i / lub oficjalne źródła”, ale zgadzam się z tym, co napisał @berecursive w swojej odpowiedzi. PowerMock istnieje z jakiegoś powodu i nie powinno się wydawać, że się poddaje, zwłaszcza jeśli nie chcesz sam pisać klas opakowań. Metody końcowe i statyczne są uciążliwe, jeśli chodzi o testy jednostkowe (i TDD). Osobiście? Używam opisanej przez ciebie metody 2.
Deco

„wiarygodne i / lub oficjalne źródła” to tylko jedna z opcji, którą możesz wybrać, rozpoczynając nagrodę za pytanie. Co właściwie mam na myśli: doświadczenia lub odniesienia do artykułów napisanych przez ekspertów TDD. Lub jakiekolwiek doświadczenie kogoś, kto napotkał ten sam problem ...

Odpowiedzi:


34

(Obawiam się, że nie ma tu żadnych „oficjalnych” źródeł - to nie jest specyfikacja, jak dobrze testować. Tylko moje opinie, które, mam nadzieję, będą przydatne).

Gdy te metody statyczne reprezentują prawdziwe zależności, utwórz opakowania. Więc dla rzeczy takich jak:

  • ImageIO
  • Klienci HTTP (lub cokolwiek innego związanego z siecią)
  • System plików
  • Uzyskiwanie aktualnego czasu (mój ulubiony przykład, w którym pomaga wstrzyknięcie zależności)

... sensownie jest stworzyć interfejs.

Ale wielu metod w Apache Commons prawdopodobnie nie należy wyśmiewać / sfałszować. Na przykład weź metodę, aby połączyć listę ciągów, dodając przecinek między nimi. Wyśmiewanie ich nie ma sensu - po prostu pozwól statycznemu wywołaniu wykonać swoją normalną pracę. Nie chcesz ani nie musisz zastępować normalnego zachowania; nie masz do czynienia z zasobem zewnętrznym lub czymś, z czym trudno jest pracować, to tylko dane. Rezultat jest przewidywalny i i tak nigdy nie chciałbyś, aby był to coś innego niż to, co ci da.

Podejrzewam, że po usunięciu wszystkich wywołań statycznych, które tak naprawdę są wygodnymi metodami z przewidywalnymi, „czystymi” wynikami (takimi jak kodowanie base64 lub URL), a nie punktami wejścia w cały wielki bałagan zależności logicznych (jak HTTP), przekonasz się, że jest to całkowicie praktyczne robić właściwe rzeczy z prawdziwymi zależnościami.


20

Jest to zdecydowanie uparte zdanie / pytanie, ale za ile warto pomyślałem, że wrzucę moje dwa centy. Pod względem stylu TDD metoda 2 jest zdecydowanie podejściem, które podąża za nią do litery. Argumentem dla metody 2 jest to, że jeśli kiedykolwiek chciałbyś zastąpić implementację jednej z tych klas - powiedzmy ImageIOrównoważnej biblioteki - możesz to zrobić, zachowując zaufanie do klas, które wykorzystują ten kod.

Jednak, jak wspomniałeś, jeśli użyjesz wielu metod statycznych, to w końcu napiszesz dużo kodu opakowania. Na dłuższą metę może to nie być złe. Jeśli chodzi o łatwość konserwacji, istnieją na to argumenty. Osobiście wolałbym takie podejście.

Powiedziawszy to, PowerMock istnieje z jakiegoś powodu. Jest to dość dobrze znany problem polegający na tym, że testowanie przy użyciu metod statycznych jest bardzo bolesne, stąd powstanie PowerMock. Myślę, że musisz rozważyć swoje opcje pod kątem tego, ile pracy będzie wymagało obejście wszystkich klas pomocników w porównaniu z użyciem PowerMock. Nie sądzę, że korzystanie z PowerMock „rezygnuje” - po prostu czuję, że owijanie klas zapewnia większą elastyczność w dużym projekcie. Im więcej zamówień publicznych (interfejsów), tym czystsze może być oddzielenie zamiaru od realizacji.


1
Dodatkowy problem, o którym nie jestem pewny: czy wdrażając opakowania, czy zaimplementowałbyś wszystkie metody klasy, która jest opakowana, czy tylko te, które są obecnie potrzebne?

3
Podążając za zwinnymi pomysłami, powinieneś zrobić najprostszą rzecz, która działa, i unikać pracy, której nie potrzebujesz. Dlatego powinieneś ujawniać tylko te metody, których naprawdę potrzebujesz.
Assaf Stone

@AssafStone zgodził się

Bądź ostrożny z PowerMock, wszystkie manipulacje klasami, które muszą zrobić, aby wyśmiewać metody, wiążą się z dużym nakładem pracy. Twoje testy będą znacznie wolniejsze, jeśli będziesz go intensywnie używać.
bcarlso

Czy naprawdę musisz dużo pisać, jeśli połączysz swoje testy / migrację z przyjęciem biblioteki DI / IoC?

4

Jako odniesienie dla wszystkich, którzy również mają do czynienia z tym problemem i natkną się na to pytanie, opiszę, w jaki sposób postanowiliśmy rozwiązać problem:

Zasadniczo podążamy ścieżką opisaną jako # 2 (klasy otoki dla narzędzi statycznych). Ale używamy ich tylko wtedy, gdy jest to zbyt skomplikowane, aby zapewnić narzędziu wymagane dane do uzyskania pożądanego wyniku (tj. Gdy absolutnie musimy kpić z metody).

Oznacza to, że nie musimy pisać opakowania dla prostego narzędzia, takiego jak Apache Commons StringEscapeUtils(ponieważ ciągi, których potrzebują, można łatwo podać) i nie używamy próbnych metod dla metod statycznych (jeśli uważamy, że może być czas, aby napisać klasa opakowania, a następnie wyśmiewaj się z instancji opakowania).



1

Pracuję dla dużej firmy ubezpieczeniowej, a nasz kod źródłowy powiększa się do 400 MB czystych plików Java. Opracowujemy całą aplikację bez myślenia o TDD. Od stycznia tego roku rozpoczęliśmy testy junit dla każdego elementu.

Najlepszym rozwiązaniem w naszym dziale było użycie fałszywych obiektów w niektórych metodach JNI, które były niezawodne w systemie (napisane w C) i jako takie nie można dokładnie oszacować wyników za każdym razem w każdym systemie operacyjnym. Nie mieliśmy innej opcji niż użycie wyśmiewanych klas i konkretnych implementacji metod JNI specjalnie w celu przetestowania każdego modułu aplikacji dla każdego obsługiwanego systemu operacyjnego.

Ale to było naprawdę szybkie i do tej pory działało całkiem dobrze. Polecam - http://www.easymock.org/


1

Obiekty współdziałają ze sobą, aby osiągnąć cel, gdy obiekt jest trudny do przetestowania ze względu na środowisko (punkt końcowy usługi WWW, warstwa dao uzyskująca dostęp do bazy danych, kontrolery obsługujące parametry żądania HTTP) lub jeśli chcesz przetestować obiekt w izolacji, a następnie wyśmiewasz te obiekty.

konieczność wyśmiewania metod statycznych jest nieprzyjemnym zapachem, musisz zaprojektować swoją aplikację bardziej zorientowaną obiektowo, a metody statyczne użytkowe do testowania jednostkowego nie dodają dużej wartości do projektu, klasa opakowania jest dobrym podejściem w zależności od sytuacji, ale spróbuj aby przetestować te obiekty, które używają metod statycznych.


1

Czasami używam opcji 4

  1. Użyj wzorca strategii. Utwórz klasę narzędziową za pomocą metod statycznych, które delegują implementację do instancji interfejsu wtykowego. Kod statycznego inicjatora, który podłącza konkretną implementację. Podłącz próbną implementację do testowania.

Coś takiego.

public class DateUtil {
    public interface ITimestampGenerator {
        long getUtcNow();
    }

    class ConcreteTimestampGenerator implements ITimestampGenerator {
        public long getUtcNow() { return System.currentTimeMillis(); }
    }

    private static ITimestampGenerator timestampGenerator;

    static {
        timestampGenerator = new ConcreteTimeStampGenerator;
    }

    public static DateTime utcNow() {
        return new DateTime(timestampGenerator.getUtcNow(), DateTimeZone.UTC);
    }

    public static void setTimestampGenerator(ITimestampGenerator t) {...}

    // plus other util routines, which may or may not use the timestamp generator 
}

To, co podoba mi się w tym podejściu, polega na tym, że utrzymuje statyczne metody narzędzi, co wydaje mi się właściwe, gdy próbuję użyć klasy w całym kodzie.

Math.sum(17, 29, 42);
// vs
new Math().sum(17, 29, 42);
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.