Unit testy, jak mockowac plik.h

0

Hejka :)

Zaczynam zagłębiać technikę pisania testów do C/C++. Jednak nie rozumiem jednej rzeczy. Przykładowo jeżeli używam np. CppUtest to logiczne jest dla mnie podmienianie argumentów funkcji, albo samych funkcji za pomocą wskaźników. Natomiast nie rozumiem jak podmienia się nagłówki plików? Aby móc ominąć inne zależność prowadzące do jeszcze większej liczby plików.

Np. załóżmy, że mam taka sytuację:

src/
------ fun.cpp fun.h { include f1, include f2 }
mocks/
------ fake_fun.h { include fake_f1, include fake_2 }
test/ test.cpp

W pliku 'fun.cpp' mam dużo funkcji, które korzystają z #definów zawartych w includach <f1> <f2> zgrupowanych w pliku 'fun.h'. Można by powiedzieć, że 'fun.h' składa się z samych #definów, które prowadzą do innych plików, wywołań itd. Chciałbym teraz podmienić plik z kodem funkcji 'fun.h' na plik nagłówkowy 'fake_fun.h' z pustymi #definami zawartymi w <fake_1> <fake_2>. Umożliwiło by mi to testowanie samych funkcji. Rozdzielenie zależności. Jak to zrobić bez ingerowania w strukturę pliku 'fun.cpp'? Da się jakoś podmieć/wstrzyknąć plik 'fake_fun.h' zamiast 'fun.h'? Jak to się robi? Na poziomie linkowania? Czy w samym unit teście. Nie rozumiem tego.

W skrócie zamiast fun.h/fun.cpp chciałym uruchomić coś w stylu fake_fun.h pracujące z funkcjami zawartymi w fun.cpp. Kod znajdujący się w folderze /src byłby nienaruszony.

Pozdrawiam

0

Jeżeli w nagłówku masz tylko deklaracje a całą implementację w pliku fun.cpp to nie musisz podmieniać inkluda tylko jednostkę translacyjną. Czyli nie linkujesz w projekcie z testami pliku fun.cpp tylko fake_fun.cpp i nie potrzebujesz zmieniać niczego innego. Z zastrzeżeniem, że w pliku fake_fun.cpp musisz mieć definicje wszystkiego co jest używane, nie będziesz miał fallbacku do implementacji z fun.cpp.

Taka technika "mockowania" jest bardzo niepraktyczna, w praktyce nie widziałem by było to wykorzystywane na szerszą skalę. W obecnym projekcie czasem z tego korzystamy, ale nie do faktycznego mockowania, ale do wycięcia ciężkich, nie testowalnych funkcji odpowiedzialnych np. za UI, które są w testach nie używane, ale linker wymaga definicji. Robimy tak, bo mamy bajzel z zależnościami a nie dlatego, że to dobra praktyka.

0

Hm,. jeżeli stworze plik fake_fun.cpp to wtedy muszę przekopiować ręcznie, niektóre funkcje z fun.cpp. Tworzy się nadmiarowy kod (dwa razy te same funkcje w fun.cpp i fake_fun.cpp ?). Boje się, że może mi się to z czasem rozjechać gdy zapomnę, że zmieniałem coś w pliku oryginalnym. Nie da się tego jakoś zautomatyzować lub obejść ?

Wiem, wiem, jeżeli coś jest już na wstępnie źle napisane to potem ciężko się to "łata". Myślałem, że istnieje jakiś magiczny sposób. Chyba, że źle kolegę zrozumiałem? Może kolega rozjaśnić pojęcie podmiany jednostki translacyjnej ?

1

Możesz sobie makrami "wyłączyć" funkcje, ale wtedy robi się z tego niezła kaszanka ;)

fun.cpp

void regularFunction() {};

#ifndef TEST_MOCKS_ON
void functionToBeMockedInTests() {}
#endif

Wtedy gdzieś w swoim teście robisz #define TEST_MOCKS_ON i wtedy linker wybierze definicje z fake_fun.cpp. Ale zamiast tak kombinować, lepiej nauczyć się jak robić normalne dependency injection wtedy po prostu jawnie w testach wstrzykujesz swój mock do kodu, który ma być testowany i nie zastanawiasz się, która implementacja obecnie jest używana przez testy ;)

2

Takie testowanie nie ma sensu. Jeżeli testowanie na danym poziomie jest trudne to spróbuj poziom wyżej tzn. jeżeli chcesz przetestować klasę A, która jest używana tylko przez klasę B to lepiej testować klasę B. W testowaniu nie chodzi o to, żeby wszystko testować w separacji, tylko, żeby mieć pewność, że kod działa i da się go refaktorować. Im testy prostsze tym lepiej, kombinowanie z mockami oraz skomplikowane pisanie testów zazwyczaj wynika ze słabego designu. Jeżeli jednak chcesz stworzyć separację to musisz to zrobić od strony kodu przykładowo przekazując std::function albo zawierając logikę, która jest zmienna w klasie, która ma wirtualne metody. Takie przekazywanie logiki z zewnątrz do klasy/funkcji nazywa się dependency injection

2

Daj kod do przetestowania i przykład testu jaki chcesz napisać to ci to poprawimy.
I na litość boską nie pisz o "jak mockowac plik.h".
Coś musiałeś źle zrozumieć, bo to nie ma najmniejszego sensu. To jest jak proszenie o radę: "jak napompować bagażnik".
Mockuje się zależności testowanego kodu. Np twój kod wykonuje jakąś logikę i na jej podstawie wywołuje funkcje systemowe. Właściwie tworząc kod, zastępuje się wywołania funkcji systemowych, funkcjami kontrolowanymi przez test.

0

Fatalnie tłumacze to, co chce zrobić. Mam model samochodu RC. Ma bardzo dużo komend, którym nim sterują. Logika jest banalna z Waszego punktu widzenia. Zawsze testowałem na żywym modelu nowe zmiany. Jednak odkryłem, że przecież można testować niektóre funkcje bez uruchamianiu całego modelu. Odkrycie roku z mojej strony ;p Sąsiadka będzie mogła teraz spać spokojnie ... Mam problemy z #definami zawartymi w plikach nagłówkowych oraz z bibliotekami, które są znane tylko mikrokomputerowi <JB/xxxx>. #Define są przygotowane pod mikrokomputer, który mam zainstalowany w modelu. Testowanie chciałbym wykonywać na zwykłym PCcie. (Dwa różne kompilatory). Dlatego chciałbym mieć dwa pliki nagłówkowe: jeden dla mikrokomputera, drugi dla Pcta. Natomiast plik "lib_example.cpp" były dla nich wspólny. To akurat zrobić potrafię. Jednak mam problem z dowiązanie plików <JB/xxxx>.

Plik lib_example.h mam zależności do innych bibliotek, które nie są mi potrzebne mi do testowania komend, a powodują, że kod nie kompiluje się po stronie PCta. Dlatego mam stworzyć po stronie pcta, puste pliki <JB/io.h>, <JB/eeprom.h>, <JB/interrupt.h>, <JB/delay.h> z pustymi funkcjami aby oszukać kompilator? Tylko gdzie mam zainstalować te pliki aby kompilator je widział ? Nie chce ręcznie zmieniać <> na "". Ostre nawiasy oznaczają, że kompilator będzie szukał tych plików w jakieś domyślnej lokalizacji kompilatora, a nie w folderze gdzie mam napisany program ?

lib_motor_example.cpp


#include "lib_example.h"
#include <iostream>
#include <string>
using namespace std;

string StartMotor()
{
	string StartChar = "$#";
	string EndChar = "$#";

	string CmdMotor = MOTOR_FRONT_WHEELS + "/DIR:" + MOTOR_FW_DIR_ + "/GO";

	return 	StartChar + CmdMotor + EndChar;
}

//takich funkcji mam mnostwo
// jednak czasem korzystaja z funkcji, ktore dotycza mikrokomputera, przyklad ponizej:

void SendMsg(string MSG)
{
	SEND_COM = MSG;
       JB_data_send();   // !!!!!!!!!!!!!!!!!1 <--- funkcja z <JB/io.h> !!!!!!!!!!!!!!!!!!!!!!!!!!!
}

motor.h


#ifndef __lib_example_H
#define __lib_example_H

#include <JB/io.h>            !!!!!!!!!!!!!!!!!1 <--- nie wiem jak sobie z tymi poradzic  !!!!!!!!!!!!!!!!!1
#include <JB/eeprom.h>  !!!!!!!!!!!!!!!!!1 <--- nie wiem jak sobie z tymi poradzic  !!!!!!!!!!!!!!!!!1
#include <JB/interrupt.h>  !!!!!!!!!!!!!!!!!1 <--- nie wiem jak sobie z tymi poradzic  !!!!!!!!!!!!!!!!!1
#include <JB/delay.h>  !!!!!!!!!!!!!!!!!1 <--- nie wiem jak sobie z tymi poradzic  !!!!!!!!!!!!!!!!!1

#define PORT_NUM				7
#define PORT_NAME				COM
#define PORT_CNTR				PORT_NAME.BAUDCTRLB
#define SEND_COM				PORT_CNTR.PORT_NUM.DATA

// (...)

#define MOTOR_FRONT_WHEELS		M1
#define MOTOR_FW_DIR_			1
#define MOTOR_FW_DIR_BACK		0

//(...)

// Mnostwo definow, ktore sa zwiazane ze sprzetem. 
// Kompilator po stronie PC nie ma do nich dostepu do niektorych "definow", 
// Kod sluzy do sterowania modelem autka na zdalne sterowanie, mikrokomputer JaguarBoard

#endif

Jak się pozbyć <JB/xxxx>, aby móc symulować kod ?:

Nawet gdy stworze nowy plik:
lib_example_PC.h, oraz folder JB z plikami: io.h, eeprom.h, interrupt.h, delay.h to kompilator zgłasza error.

Rozumiem, że to powinno wygląda jakoś tak:
src/
lib_example.h lib_example.cpp
test/
lib_example_PC.h
test/JG
io.h eeprom.h interrupt.h delay.h

i jakos zmusic kompilator aby zadzialal z plikiem example.cpp z folderu /src a reszte wziął z folderu test ? Tylko jak ustawic kompilator w taki sposob ? Musze stworzyc wlasny makefile ?

Ps. Pomysł aby testować tylko klasę B jest sensowny. Nie pomyślałem o tym. Jak działa klasa B, to szkoda tracić czas na testowanie klasy A. Testować ją tylko wtedy gdy będzie problem z klasą B. Genialne w swojej prostocie :) Dzięki za porady. Nawet pisząc tego posta powoli rozumiem o co w tym chodzi.

1

WAT? Komunikacja z funkcją przez zmienną globalną?
Wyrazy współczucia, że musisz użyć takiego API.

To można zrobić tak:
definicja zależności:

class IJBData {
public:
     ~IJBData() {};

      virtual void send(const std::string&) const = 0;
};

Definicja zależności w wersji produkcyjnej:

// h
class JBDataLive : public IJBData {
     void send(const std::string&) const;
};

// cpp
#include <JB/io.h> 
#include <JB/eeprom.h>
#include <JB/interrupt.h>
#include <JB/delay.h>

// te makra to też niezłe WTF
#define PORT_NUM                7
#define PORT_NAME               COM
#define PORT_CNTR               PORT_NAME.BAUDCTRLB
#define SEND_COM                PORT_CNTR.PORT_NUM.DATA

void JBDataLive::send(const std::string& msg) const
{
     SEND_COM = msg;
     JB_data_send();
}

Kod produkcyjny, może wyglądać tak (niewiele togo pokazałeś i nie ma to za wiele sensu by stosować mock, więc mała przeróbka):

void StartMotor(const IJBData &api = JBDataLive{})
{
    string StartChar = "$#";
    string EndChar = "$#";

    string CmdMotor = MOTOR_FRONT_WHEELS + "/DIR:" + MOTOR_FW_DIR_ + "/GO";

    api.send(StartChar + CmdMotor + EndChar_);
}

W ten sposób cały bałagan z zewnętrznej biblioteki masz zamknięty za abstrakcją, której implementację łatwo podmienić polimorfizmem.
Dobry kompilator widząc w kodzie produkcyjnym jedną implementację interfejsu zamieni ją na bezpośrednie wywołania, więc nie ma co się martwić na zapas o narzuty wydajności.

0

Biblioteka jest dostarczona przez producenta silnika/sterownika. Nie będę w nią ingerował, bo często dostaje aktualizacje jak zmienia się hardware. Hm.. ciekawe. Po próbuje w ten sposób. Podoba mi się pomysł z api.send i wymuszeniem const = 0.

Dzięki

1 użytkowników online, w tym zalogowanych: 0, gości: 1