Nieodłącznym elementem TDD i unit testów jest mockowanie zależności. Powstało w tym celu sporo bibliotek dla różnych języków i różnych frameworków testowych. Mogłoby się więc wydawać, że wystarczy wybrać swoją ulubioną bibliotekę do mocków i po prostu jej używać. Okazuje się jednak, że pisanie prostych mocków samemu czasem może okazać się lepszym wyborem. Pokażę dzisiaj jak łatwo można napisać własne mocki do frameworków Unity i CppUTest.
Zalety własnych mocków
Największymi zaletami własnych mocków jest ich prostota prostota i brak zewnętrznych zależności. Dla porównania na przykład CMock – dedykowane mocki do Unity – wykorzystuje Ruby do parsowania headerów produkcyjnych i generowania na ich podstawie kodu mocków, który następnie wykorzystujemy w naszych testach. Rodzi to od razu kilka problemów. Po pierwsze każde środowisko, na którym budujemy i uruchamiamy testy musi mieć zainstalowane Ruby. Po drugie mocki są generowane automatyczne podczas builda, są więc za każdym razem tworzone od nowa. Z kolei przed zbudowaniem projektu w ogóle nie ma tych plików, więc nie możemy choćby podejrzeć ich implementacji.
Skoro jesteśmy przy implementacji, mocki z zewnętrznej biblioteki często są dla nas czarną skrzynką. Wywołujemy je w kodzie testów, ale nie mamy pojęcia, co dzieje się pod spodem. Zwykle nas to po prostu nie obchodzi – do momentu, kiedy coś przestaje działać. Poza tym składnia mocków bywa skomplikowana i nieczytelna.
Pisanie własnych mocków, szczególnie jeśli zaczynamy z TDD, ma również wartość edukacyjną. Pisząc je musimy rozwiązać kilka problemów programistycznych takich jak zaprojektowanie API, sposób przechowywania argumentów funkcji i zwracanych wartości. Poznajemy również ograniczenia mocków. Wiemy jakie funkcjonalności są trudne do osiągnięcia.
Mocki w C
Wystarczy tej teorii, pora na trochę kodu. Najpierw zastanowimy się, jak mogą wyglądać mocki napisane w C do wykorzystania na przykład w Unity.
Utworzymy mocka dla funkcji wypełniającej bufor danymi. Jako argumenty przyjmuje ona bufor do wypełnienia oraz jego rozmiar. Zwraca natomiast ilość wpisanych bajtów. Sygnatura takiej funkcji wygląda tak:
int32_t fill_buffer(uint8_t *buf, int32_t len);
Aby powyższą funkcję zamockować, potrzebujemy interfejsu do wykorzystania przez unit testy. Musimy być w stanie skonfigurować wartość zwracaną przez funkcję oraz mieć dostęp do przekazanych argumentów. Przydatna jest również informacja o ilości wywołań funkcji. Poniżej deklaracje funkcji naszego mocka:
void mock_fill_buffer_init(int32_t ret); int32_t mock_fill_buffer_cnt_get(void); uint8_t * mock_fill_buffer_arg_buf_get(void); int32_t mock_fill_buffer_arg_len_get(void);
Implementacja mocka jest dosyć prosta. Musimy mieć zmienną przechowującą parametry mocka – ilość wywołań, argumenty i zwracaną wartość. Funkcja init ustawi je na wartości początkowe, funkcje get zwrócą poszczególne parametry, natomiast mock funkcji produkcyjnej zapisze przekazane argumenty i zwróci wartość ret.
struct mock_fill_buffer_params { int32_t cnt; uint8_t *arg_buf; int32_t arg_len; int32_t ret; }; static struct mock_fill_buffer_params mock_fill_buffer; void mock_fill_buffer_init(int32_t ret) { mock_fill_buffer.cnt = 0; mock_fill_buffer.arg_buf = NULL; mock_fill_buffer.arg_len = -1; mock_fill_buffer.ret = ret; } int32_t mock_fill_buffer_cnt_get(void) { return mock_fill_buffer.cnt; } uint8_t * mock_fill_buffer_arg_buf_get(void) { return mock_fill_buffer.arg_buf; } int32_t mock_fill_buffer_arg_len_get(void) { return mock_fill_buffer.arg_len; } int32_t fill_buffer(uint8_t *buf, int32_t len) { mock_fill_buffer.cnt++; mock_fill_buffer.arg_buf = buf; mock_fill_buffer.arg_len = len; return mock_fill_buffer.ret; }
Jak widać utworzenie własnego mocka w C jest proste, ale wymaga całkiem sporo pisania. Z pomocą może nam przyjść IDE. W Eclipse na przykład możemy zdefiniować szablon kodu, w którym musimy podmienić jedynie typy zmiennych i nazwy własne elementów. Oczywiście kodu mocków w dalszym ciągu będzie sporo, co doda nam pracy na przykład podczas refactoringu. Jednak dzięki temu rozwiązaniu mamy kontrolę nad kodem i nie musimy korzystać z automatycznego generowania kodu jak w przypadku CMock. Innym sposobem na zmniejszenie duplikacji jest użycie makr. Istnieje nawet projekt na GitHubie z makrami do C – Fake Function Framework.
Mocki w C++
Jeżeli korzystamy z frameworka testowego w C++, możemy wykorzystać dobrodziejstwa tego języka również do pisania mocków. Z pomocą przychodzą nam klasy oraz templaty.
Klasa mocka funkcji fill_buffer może wyglądać następująco:
class Mock_fill_buffer { private: int32_t cnt = 0; uint8_t *arg_buf = NULL; int32_t arg_len = -1; int32_t ret = 0; public: Mock_fill_buffer() = default; Mock_fill_buffer(int32_t ret_val) : ret(ret_val) {} uint8_t * get_arg_buf() {return arg_buf;} int32_t get_arg_len() {return arg_len;} int32_t call(uint8_t *buf, int32_t len) { cnt_++; arg_buf = buf; arg_len = len; return ret; } };
Domyślne wartości zmiennych wewnętrznych są ustawiane w domyślnym konstruktorze. Mamy również drugi konstruktor inicjalizujący ret na przekazaną wartość.
Potrzebujemy jeszcze wywołać metodę call mocka w testowej wersji funkcji fill_buffer:
static Mock_fill_buffer *mock_fill_buffer = nullptr; void mock_fill_buffer_set(Mock_fill_buffer *mock) { mock_fill_buffer = mock; } int32_t fill_buffer(uint8_t *buf, int32_t len) { return (nullptr == mock_fill_buffer) ? 0 : mock_fill_buffer->call(buf, len); }
Na początku testu musi być zadeklarowana zmienna klasy Mock_fill_buffer i przekazana do funkcji mock_fill_buffer_set, aby mock został użyty. Po zakończeniu testu w funkcji teardown należy wywołać funkcję mock_fill_buffer_set z nullptr jako parametrem.
Przedstawione rozwiązanie w dalszym ciągu wymaga od nas wiele pisania. Dla mockowanych funkcji wykorzystujących inne typy i ilości argumentów potrzebujemy odrębne klasy. Tym razem jednak zamiast szablonów w IDE możemy użyć szablonów C++.
Wykorzystanie templatów C++
Wystarczy stworzyć template dla najczęściej używanych ilości argumentów w wariantach, gdy funkcja zwraca jakąś wartość lub nie. Każdy mock wykorzystuje zmienną cnt liczącą ilość wywołań. Możemy więc wykorzystać również dziedziczenie. Przykładowa implementacja templatów dla mocków od 0 do 2 argumentów może wyglądać następująco:
class Base_mock { protected: int32_t cnt_ = 0; public: int32_t get_count() {return cnt_;} }; class Mock_no_ret_no_arg : public Base_mock { public: void call() {cnt_++;} }; template <typename TArg1> class Mock_no_ret_1_arg : public Base_mock { protected: TArg1 arg1_ = TArg1(); public: TArg1 get_arg1() {return arg1_;} void call(TArg1 arg1) {cnt_++; arg1_ = arg1;} }; template <typename TArg1, typename TArg2> class Mock_no_ret_2_arg : public Base_mock { protected: TArg1 arg1_ = TArg1(); TArg2 arg2_ = TArg2(); public: TArg1 get_arg1() {return arg1_;} TArg2 get_arg2() {return arg2_;} void call(TArg1 arg1, TArg2 arg2) {cnt_++; arg1_ = arg1; arg2_ = arg2;} }; template <typename TRet> class Mock_ret_no_arg : public Base_mock { protected: TRet ret_; public: Mock_ret_no_arg() = default; Mock_ret_no_arg(TRet ret_val) : ret_(ret_val) {} TRet call() {cnt_++; return ret_;} }; template <typename TRet, typename TArg1> class Mock_ret_1_arg : public Base_mock { protected: TArg1 arg1_ = TArg1(); TRet ret_; public: Ret1ArgMock() = default; Ret1ArgMock(TRet ret_val) : ret_(ret_val) {} TArg1 getArg1() {return arg1_;} TRet call(TArg1 arg1) {cnt_++; arg1_ = arg1; return ret_;} }; template <typename TRet, typename TArg1, typename TArg2> class Mock_ret_2_arg : public Base_mock { protected: TArg1 arg1_ = TArg1(); TArg2 arg2_ = TArg2(); TRet ret_; public: Mock_ret_2_arg () = default; Mock_ret_2_arg (TRet ret_val) : ret_(ret_val) {} TArg1 get_arg1() {return arg1_;} TArg2 get_arg2() {return arg2_;} TRet call(TArg1 arg1, TArg2 arg2) {cnt_++; arg1_ = arg1; arg2_ = arg2; return ret_;} };
Mając takie templaty mock do funkcji fill_buffer możemy zdefiniować jako:
using Mock_fill_buffer = Mock_ret_1_arg<int32_t, uint8_t *, int32_t>;
Obsługa funkcji mock_fill_buffer_set pozostaje taka sama.
Dzięki użyciu templatów możemy łatwo tworzyć mocki kolejnych funkcji nie dopisując zbyt dużo kodu.
Podsumowanie
Jak widać, jeżeli potrzebujemy podstawowych mocków zadających zwracaną wartość funkcji i zapamiętujących jej argumenty, nie musimy specjalnie wykorzystywać frameworków do mockowania. Jesteśmy w stanie bez problemu wykorzystać własne mocki. Dzięki temu unikamy dodatkowych zewnętrznych zależności oraz dziwnej składni często występującej w mockach. Pisał na ten temat nawet Uncle Bob.
Jednak kiedy testujemy bardziej skomplikowane zależności takie jak sekwencje wywołań, wielokrotne wywołanie jednej mockowanej funkcji w pojedynczym teście z różnymi argumentami i zwracanymi wartościami – wtedy lepiej użyć gotowej biblioteki, która posiada te mechanizmy.
Dodaj komentarz