Uwaga: Pytanie ma niewiele wspólnego z samymi grami, aczkolwiek odpowiedź na nie może się przydać i w tej branży.
Krótki wstęp: Życie programisty C/C++ jest trudne :). Nie mam na myśli problemów ze znalezieniem pracy ale jej rodzaj. Bardzo często wiąże się ona z pracą z tak zwanym Legacy Code - miliony linii kodu bez jednego testu. Korzysta z tego kodu tysiące klientów - więc wiadomo nie można go "zepsuć". Kod ten tworzyło setki programistów, każdy miał inne doświadczenie i nawyki. Brak struktury projektu, jednolitego formatowania - jeden problem jest rozwiązywany na wiele sposobów, każdy z nich inny.
Trudno mi powiedzieć, czy to tylko moje doświadczenia, czy prawie każdy mógłby się pod tym podpisać.
Skupmy się jednak na problemie: Niejaki Michael Feathers napisał książkę (Working Effectively with Legacy Code) która próbuje jakoś odpowiedzieć na pytanie: Co z tym Legacy Code? Zaznajomiłem się z nią i staram się stosować przedstawione w niej metody pracy z kodem. Wszystko trzeba jednak osadzić w realiach zarządzania firmą - mój przełożony zgodził się, żebym sobie pracował z kodem tak jak lubię - ale żeby zmiany w plikach źródłowych kodu produkcyjnego były możliwie najmniejsze (najlepiej nic nie zmieniać jeśli nie trzeba). To jest ważne ograniczenie z mojej perspektywy.
Testowanie idzie nawet sprawnie. Dodawanie nowych funkcjonalności wspierane przez testy jest naprawdę produktywne i skutecznie. Większość moich zmian robi to co powinno praktycznie za pierwszym razem i bardzo rzadko przy tych modyfikacjach uszkodzeniu ulegają inne części systemu. Fajnie. Natomiast mam problem z wprowadzaniem testów do kodu który jest bardzo mocno napisany w C (są i takie fragmenty systemu).
Konkretnie z czym mam problem: Jak testować/refaktoryzować kod w C. Jest nawet książka która adresuje ten problem: "Test Driven Development for Embedded C" - przeglądnąłem ją tylko, ale jakoś nie znalazłem w niej rozwiązania mojego problemu. Oto jak wygląda przykładowy kod który zmieniam:
Plik: funkcjonalnoc.h
void funkcjaKtoraRobiCosWaznego(int a, strukturaJakas b);
Plik: funkcjonalnosc.cpp
static int global_1;
static int blobal_2;
<tutaj 100 innych globali>
void funkcjaKtoraRobiCosWaznego(int a, strukturaJakas b)
{
int zmiennaLokalna = 14;
<1000 linii kodu>
global_1 = 12;
if ( funkcjaLokalnaDlaTegoPliku(zmiennaLokalna ) )
{
//Tutaj dużo kodu
}
<2000 linii kodu>
funkcjaWInnymPliku(zmiennaLokalna);
}
static void funkcjaLokalnaDlaTegoPliku(zmiennaLokalna )
{
<tutaj znowu 1000 linii kodu>
}
Ok, tak wygląda mniej więcej kod. Teraz muszę zmienić implementację w funkcjaKtoraRobiCosWaznego. Zanim się tym zajmę, muszę trochę zrefaktoryzować tę funkcję - najpierw ją przetestować, żeby być pewnym że nic nie psuję, a potem zrefaktoryzować.
Nic prostszego:
Plik: Test.cpp
#include "potrzebnyNaglowek.h"
//Miejsce X
#include "funkcjonalnosc.cpp"
void funkcjaWInnymPliku(int zmiennaLokalna)
{
Expect(zmiennaLokalna, 150);
}
Test1()
{
strukturaJakas tmp;
tmp.pole = 15;
funkcjaKtoraRobiCosWaznego(100, tmp);
Expect(blobal_2, 12);
}
To tylko zarys - tak naprawdę to korzystam z google mock, ale to nie ma dużego znaczenia. Zwracam uwagę, że robię include na pliku cpp, dzięki temu mam dostęp do zmienny statycznych globalnych, a dodatkowo mogę robić sprytne definy jeżeli ich potrzebuję. Testowanie funkcjaKtoraRobiCosWaznego idzie jak złoto, powoli jestem gotowy do refaktoryzacji a tutaj nagle klops. Nie wiem jak "podmienić" zachowanie funkcjaLokalnaDlaTegoPliku. Jest zdefiniowana w tym samym pliku...
Jeśli w: "//Mijesce X" wstawię:
#define funkcjaLokalnaDlaTegoPliku funkcjaLokalnaDlaTegoPliku_STUB
nic mi to nie pomoże, bo zmieni się nazwa w miejscu wywołania i definicji.
Sporo czasu grzebałem w google, szukałem w książkach, ale żadne rozwiązanie mi się nie spodobało, np:
http://stackoverflow.com/questions/4882029/unit-test-stub-c-helper-methodhttp://stackoverflow.com/questions/8959132/function-mocking-for-testing-in-cWorking Effectively with Legacy Code - rozdział 19.
Generalnie wyszło mi, że tego problemu nie da się rozwiązać przez sprytne makro albo na poziomie linkera (dodam że korzystam z google test a projekt jest w Visual Studio). Jedyne wyjście to zmodyfikować plik: funkcjonalnosc.cpp (kod produkcyjny). Pierwsze pytanie: Czy to faktycznie prawda? Ktoś miał podobny problem i jakoś sprytnie sobie z tym poradził?
Ok załóżmy, że chcemy przerobić funkcjonalnosc.cpp, - jak to zrobić żeby móc "podmienić" implementację w funkcjaLokalnaDlaTegoPliku w teście. Można na przykład przekazać ją jako wskaźnik do funkcji, ale to nie jest dobre rozwiązanie. Takich funkcji do przekazania było by bardzo dużo, a do tego chcę żeby było ich jeszcze więcej (chcę powydzielać implementację do nowych funkcji).
Można dopisać do wywołania: funkcjaLokalnaDlaTegoPliku_ORG i w kodzie produkcyjnym:
#define _ORG
a w kodzie testowym:
#define _ORG _STUB
ale takie _ORG dziwnie będzie wyglądało w kodzie produkcyjnym. Mogę też wrzucić wszystkie te funkcje do klasy - już to sprawdziłem - działa niby fajnie, ale to dość duża modyfikacja w funkcjonalnosc.cpp. Taka zmiana jest dopuszczalna, bo cały kod jest kompilowany jako kod C++ (i testowy i produkcyjny) - ale czy to dobry pomysł ? Funkcje w tej klasie nadal korzystają z globalnych zmiennych. Można by te globalne zmienne wrzucić do klasy - ale to temat na innego posta.
Jeszcze inną opcją jest wyrzucenie funkcjaLokalnaDlaTegoPliku do innego pliku. Wtedy automatycznie stanie się zewnętrzną zależnością i będę mógł ją podmienić. Ale to nie jest dobre rozwiązanie, bo funkcjaLokalnaDlaTegoPliku jest mocno związana z funkcjaKtoraRobiCosWaznego (funkcja lokalna wykonuje pracę dla funkcji która robi coś ważnego - powinny być w jednym pliku bo realizują jedno zadanie i łatwiej zrozumieć co się dzieje gdy są blisko siebie )
Tutaj drugie pytanie: Jak wy to robicie? Jak przerobić funkcjonalnosc.cpp żeby móc wygodnie "podmienić" zachowanie funkcjaLokalnaDlaTegoPliku w teście. Przypominam, że chciałbym możliwie jak najmniej zmieniać plik funkcjonalnosc.cpp.
Jeżeli jest taka potrzeba, mogę dostarczyć bardziej "realne" fragmenty kodu - w sensie budujący się projekt który można sprawdzić. Zależy mi na konkretnych propozycjach rozwiązania - fragmenty kodu mile widziane.
Jeżeli ktoś ma ciekawy materiał na ten temat (ale nie generalnie o testowaniu tylko z rozwiązaniem tego konkretnego problemu) w internecie - proszę o linka.
Przepraszam za objętość tego wpisu.
Pozdrawiam,
Lerhes