TDD w systemach embedded - wszystkie wpisy

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.

TDD w systemach embedded - Nawigacja