Testowanie metod asynchronicznych w Javie

Jeśli w swoim życiu napisaliście już setki klas i tysiące metod, a nie stworzyliście jeszcze żadnego testu, to marni z Was developerzy. Testy jednostkowe są naprawdę pożytecznym narzędziem, bo choć wydłużają proces produkcji, jednocześnie powodują, że wartość końcowego produktu jest wielokrotnie większa, a Wy jesteście pewni, że Wasz produkt działa. Pisanie testów metod pobierających argumenty i zwracających wyniki, które można na podstawie tych argumentów zweryfikować, to kaszka z mleczkiem. Znacznie trudniej testować metody, które uruchamiają zadania w tle, a wyniki zwracają przez tzw. callbacki. Pokaże Wam, jak uporałem się z tym problemem.

Z pewnością zetknęliście się kiedyś z modułem, który wystawiał dwa publiczne interfejsy. Jeden z nich służył do zarządzania komponentem, natomiast drugi implementowaliście w celu uzyskania informacji zwrotnych z tego modułu. Takie API można znaleźć w  kodzie C++ znacznie częściej, aniżeli w Javie czy C#. Metoda interfejsu zarządzającego często nic nie zwraca, a jeżeli już, to jedynie kod informacyjny np. ‚operacja zaplanowana do wykonania’, albo ‚nie można wykonać’. Po pewnym czasie przychodzi odpowiedź w formie wywołania metody z interfejsu notyfikacji, który uprzednio zaimplementowaliśmy i przekazaliśmy do modułu. Test takiej metody powinien sprawdzać zwracany kod, czekać (ale nie nieskończenie długo) na przyjście odpowiedzi oraz zweryfikować przysłane w notyfikacji dane.  Przykładowy projekt takich interfejsów możecie znaleźć w moim repozytorium w pakiecie pl.fones.blog.asynctest.

Klasa Engine to interfejs zarządzający, EngineNotifcations to, rzecz jasna, interfejs powiadomień, EngineTest to testy naszego komponentu, dziedziczą po AsyncBaseTestCase, a te z kolei po JUnitowych test case’ach. Będziemy testować metodę start(), która zwraca swój wynik w notyfikacji engineStarted().

Cała logika testowania asynchronicznych metod zawarta jest w klasie AsyncBaseTestCase. Metody notifyCallback() oraz waitForCallback() powinny być wołane w naszym teście, jedna w zaimplementowanym na rzecz testu interfejsie notyfikacji, a druga – w samej metodzie testującej. Ich działanie opiera się na obiekcie CountDownLunch.  Metoda waitForCallback() czeka, aż wartość licznika tego obiektu będzie równa zero, a metoda notifyCallback() zmniejsza jego wartość domyślnie ustawioną na 1. Dodatkowe struktury służą do przekazania parametrów notyfikacji i informacji, czy dany callback w ogóle nastąpił. W ten sposób możemy ustawić, jaki maksymalny czas dajemy komponentowi na odpowiedź. Poprzez tablicę flag sprawdzamy, czy dana notyfikacja nastąpiła, a przez kolekcję rezultatów dowiadujemy się o przekazanych parametrach. Wszystko w bezpieczny, wielowątkowy sposób, z wykorzystaniem dostępnych metod synchronizacji.

Jeśli znacie inny sposób, który usprawniłby moje rozwiązanie, chętnie go poznam.