Autor Wątek: Autotesty w programowaniu gier  (Przeczytany 2374 razy)

Offline Koshmaar

  • Użytkownik
    • Homepage

# Październik 19, 2006, 23:39:58
Hej

Interesuje mnie czy używacie w swoich projektach czegoś takiego jak automatyczne testy kodu? Jeśli tak, jakie są wasze doświadczenia z tym związane? Czy stworzenie i utrzymywanie bazy testów okazało się strzałem w dziesiątkę, pomogło wam w znalezieniu i usunięciu wielu dziwnych błędów, a może nie przyniosło oczekiwanych efektów i pisanie oraz utrzymywanie testów stało się mordęgą?

Jeśli nie stosowaliście testów automatycznych, w których piszecie kod testujący napisany kod, to czy używaliście na większą skalę testów manualnych, w których programista musi za każdym razem ręcznie sprawdzić czy wyniki są poprawne? Jakie są wasze przemyślenia z tym związane?

Jakiej architektury testów używacie? Jakieś podpowiedzi albo fajne sztuczki jak można sobie ułatwić testowanie? Stosujecie własne rozwiązanie, czy może któryś z zewnętrznych frameworków? Możecie podać nazwy godnych polecenia?


 ------------------


 Moje odpowiedzi są następujące:

 - w jednym projekcie używałem testów automatycznych wykorzystując Boost.Test. Moje wrażenia są mieszane, z jednej strony byłem bardziej pewny siebie podczas kodowania, nie bałem się wprowadzania większych zmian w kodzie gdyż wiedziałem że zawsze mogę uruchomić testy i szybko dowiedzieć się czy czegoś nie zepsułem. Z drugiej strony, czas związany z napisaniem i utrzymaniem testów był dość znaczny; najgorsze jest to że nie wielu rzeczy najzwyczajniej w świecie nie da się dobrze automatycznie przetestować (grafika, dźwięk, większe podsystemy), inne natomiast można, ale trzeba się nieźle nagłowić wymyślając jak to zrobić.

- testy manualne stosowałem przez długi czas, ogólnie jestem z nich zadowolony, napisanie zazwyczaj nie zajmuje wiele czasu i jest prostsze niż autotestów, napisanie zaś większego testu posiada przy tym dodatkową zaletę że podczas pisania właściwej gry można niekiedy stosować magiczną sztuczkę ctrl+c, ctrl+v. Wady są takie że sprawdzanie każdego testu po każdej rekompilacji jest bardzo mało efektywne, trzeba to robić raz na jakiś czas, zaś wtedy oczywiście mogą się niepostrzeżenie wkraść jakieś błędy.


- moja aktualna "architektura" testów: katalog tests w którym znajdują się podkatalogi z nazwami testowanych featursów, np. Logger, MemoryMgr, w których są projekty i źródła testów. Exeki są kierowane do jednego głównego katalogu bin\. Główny Solution silnika (VS) posiada katalog w którym znajdują się testy, tak więc rekompilując całość od razu rekompiluję testy, dodatkowo ułatwiona sprawa z debugowaniem kodu silnika przy pomocy testów, wszystko jest w jednym oknie i nie trzeba się przełączać.  Kładę nacisk na logowanie tego co się dzieje do plików (jak widać, testy manualne), ale zastanawiam się czy nie wprowadzić automatycznych, przynajmniej w niektórych dziedzinach...


 Mam nadzieję na wywiązanie się ciekawej dyskusji :-) aktualnie zaczynam pisać następną wersję swojego silnika od zera, i chciałbym stosować od początku rozwiązania które mają szansę się sprawdzić.




Offline Mr. Spam

  • Miłośnik przetworów mięsnych

bies

  • Gość
# Październik 20, 2006, 13:03:10
Używam testów jednostkowych w przypadku gdy piszę bibliotekę o dobrze wyspecyfikowanym interfejsie (staram się pisać dużo bibliotek w projekcie). Gdzie biblioteka to nie musi być nawet osobny plik do linkowania a wystarczy że będzie osobną przestrzenią nazw.

Korzystam z własnych testów (Boost.Test jest dla mnie zbyt rozbudowany, CppUnit jakoś mi nie pasuje a innych nie widziałem). Są to po prostu krótkie programiki piszące na stdout i zwracające odpowiedni kod błędu. A później wystarczy 'for test in tests/bin/*; do `$test`; done' (no trochę inna pętla, ale idea podobna ;) ).

Offline Koshmaar

  • Użytkownik
    • Homepage

# Październik 21, 2006, 18:38:04
Ok, dzięki za odpowiedzi (choć trochę ich mało :-> )

kaleef: UnitTest++ sprawdziłem, jak dla mnie jest ciutkę zbyt rozbudowany (podobnie jak Boost.TesT), ale jeszcze nie podjąłem decyzji więc może się zdecyduję właśnie na niego.

bies: szkoda że cmd.exe jest takie ograniczone... ale całe szczęście że istnieje MSYS :-)


Btw, http://www.gamedev.net/reference/programming/features/kjam/ wygląda na porządną alternatywę dla Make'a.

Offline orzech

  • Użytkownik
    • homepage

# Październik 21, 2006, 19:30:52
Swoja droga ciekawe - tylko trzy osoby na tym forum pisza zautomatyzowane testy? :P
To wbrew pozorom wcale nie jest powszechna umiejętność. :) Ja osobiście w ogóle tego nie próbowałem i szczerze przyznam, że póki co niewiele rozumiem. W firmie zastanawialiśmy się czy nie warto w ramach testu poprowadzić jednego projektu, który wykorzystywałby testy jednostkowe (z użyciem JUnit). Nie jestem jednak pewny, czy w przypadku takich projektów jak małe gry na telefony komórkowe warto zaprzęgać ten mega mechanizm, tym bardziej, że wiąże się to z dodatkowym nakładem pracy. ;)

Offline Koshmaar

  • Użytkownik
    • Homepage

# Październik 22, 2006, 00:47:47
Kaleef: ten przykład co podałeś jest faktycznie prosty, jednak w dokumentacji UnitTest++ wyglądało to trochę inaczej... trzeba będzie doczytać.

Widzę że dyskusja ciutkę się rozwinęła, dodam jeszcze własne przemyślenie. IMHO, największą przeszkodą w szerokim stosowaniu autotestów w programowaniu (ogólnie) i programowaniu gier (szczególnie) jest trudność w uzyskaniu od testowanego kodu informacji zwrotnych o powodzeniu / niepowodzeniu. Wspomniałem już o problemach z grafiką i dźwiękiem (do tej listy można jeszcze na szybko dodać sieć i AI), natomiast chciałem podać świeży przykład.

Piszę właśnie menadżera pamięci, który działa w ten sposób że (w uproszczeniu) informacje (dość szczegółowe, nazwa pliku, linia pliku, nazwa funkcji, typ, nazwa zmiennej itp.) o wszystkich alokacjach są zapisywane do wektora, podczas dealokacji są z niego usuwane, zaś przy zamykaniu programu to co pozostało w wektorze jest logowane.

Założyłem sobie, że MemMgr powinien być tak transparentny w użyciu jak tylko można, jedna funkcja do inicjalizacji/de-inicjalizacji, po makrze na alokację/dealokacja pamięci - to wszystko. MemMgr napisany, test działa, teraz człowiek chciałby go zautomatyzować, ale tutaj natyka się na problem - jak można to zrobić, nie otrzymując od MemMgr żadnych informacji? W końcu wszystko siedzi wewnątrz, jest ładnie zenkapsulowane i dostanie się do niego wymagało by tworzenia w interfejsie funkcji służących do zwracania stanu managera, a które byłyby używane tylko w tej jednej, jedynej, bardzo specyficznej sytuacji (testowanie) - nie muszę chyba mówić że takie rozwiązanie mnie nie pasuje. Hmmm, macie jakieś pomysły jak można rozwiązać ten problem? przy czym nie musi odnosić się konkretnie do podanego przykładu, tak naprawdę dość często występuje on również w wielu innych dziedzinach programowania...

Cytuj
No i testy jednostkowe mozna odpalac przy kazdej kompilacji - co raczej odpada przy recznym testowaniu kodu.

Prawda, tylko że takie uruchamianie kilku programów zajmuje trochę czasu - niech to będzie 1/2 sekundy, jednak kod w czasie produkcji jest dosyć często kompilowany, więc nawet taki minimalmy narzut często dodawany do czasu kompilacji może podować irytację (powód identyczny jak ten z wyłączaniem przez ludzi wodotrysków graficznych w Windowsach i Officie, typu płynnie pojawiające się okienka, paski itp.)

Osobiście nie będę podpinał testów automatycznych pod kompilator, tylko sam raz na jakiś czas (albo gdy dokonam większych zmian w kodzie) je ręcznie uruchamiał.

Offline Reg

  • Administrator
    • Adam Sawicki - Home Page

# Październik 22, 2006, 14:43:01
Ja nigdy nie miałem potrzeby stosowania jakiś bardzo sformalizowanych czy zautomatyzowanych testów. Kiedy piszę jakiś moduł, od tych najbardziej bazowych poprzez różne pośrednie aż do samej aplikacji, to zawsze jest przy tym jakiś plik Main.cpp, w nim funkcja main/WinMain i pewien kod, który mi pisany moduł testuje - to oczywiste. Czasami tylko, kiedy chcę się upewnić, że wszystko jest dobrze, piszę sobie testowanie czegoś w pętli czy staram się zaimplementować test możliwie wszystkich funkcji modułu na raz. Najczęściej jednak intuicyjne testowanie podczas pisania jest wystarczające.

Koshmaar: Coś mi się wydaje, że to nasze zainteresowanie inżynierią oprogramowania bardziej nam przeszkadza niż pomaga i trzeba pomyśleć o jakiejś zmianie podejścia do sprawy, bo inaczej do końca życia pozostaniemi pisaczami frameworków :) Ja właśnie w wakacje wziąłem się za szóste podejście (czy, żeby zabrzmiało bardziej jak z inżynierii - iterację :)) do mojego kodu, napisałem nowe moduły bazowe, wczoraj napisałem nowy framework, a od dzisiaj biorę się za manager zasobów.

bies

  • Gość
# Październik 22, 2006, 15:32:30
Założyłem sobie, że MemMgr powinien być tak transparentny w użyciu jak tylko można, jedna funkcja do inicjalizacji/de-inicjalizacji, po makrze na alokację/dealokacja pamięci - to wszystko. MemMgr napisany, test działa, teraz człowiek chciałby go zautomatyzować, ale tutaj natyka się na problem - jak można to zrobić, nie otrzymując od MemMgr żadnych informacji? W końcu wszystko siedzi wewnątrz, jest ładnie zenkapsulowane i dostanie się do niego wymagało by tworzenia w interfejsie funkcji służących do zwracania stanu managera, a które byłyby używane tylko w tej jednej, jedynej, bardzo specyficznej sytuacji (testowanie) - nie muszę chyba mówić że takie rozwiązanie mnie nie pasuje. Hmmm, macie jakieś pomysły jak można rozwiązać ten problem? przy czym nie musi odnosić się konkretnie do podanego przykładu, tak naprawdę dość często występuje on również w wielu innych dziedzinach programowania...
Dobrze kombinujesz, choć nie do końca. Zarządca pamięci (i ogólnie, wszelkie podsystemy ,,systemowe'') nie nadają się do testowania z zewnątrz. Dlatego sam zarządca będzie musiał udostępniać wersję (właśnie wersję, nie interfejs) zwracającą dane do analizy. Może to być np. bardzo rozbudowane logowanie opisujące obiekty, wskaźniki, obszary pamięci. Testowanie będzie polegało na napisaniu programu używającego zarządcy w możliwie różnorodny sposób i programu analizującego (może nawet graficznie) zachowanie zarządcy. Część tego można zautomatyzować (parowanie alokacji - dealokacji, zachodzenie na siebie obszarów pamięci, odwoływanie się do nieswojego obszaru). Zrobienie takiego programu będzie prawdopodobnie dość pracochłonne. Typowe narzędzia do autotestów też nie pomogą. Ale może się przydać.

Offline Steel_Eagle

  • Użytkownik

# Październik 22, 2006, 18:26:09
Przyznam, ze mnie zaciekawiliscie  ;) Czy moglibyscie sypnac troche wiecej informacji, bo w necie ciezko znalezc, a link podany wyzej wydaje sie byc demagogia (ten przyklad z pisaniem NameManglera jest conajmniej idiotyczny...)  ;)

Na czym polega cala technika i jak ja odpalic? Bo z tego co widze testy te tylko sprawdzaja czy funkcje zwracaja poprawne wyniki dla danych. Przy odrobinie zdrowego myslenia i doswiadczenia w kodowaniu mozna takie bledy wyeliminowac juz przy pisaniu... A co w tym jest takiego ekstra?  ;D

Offline Koshmaar

  • Użytkownik
    • Homepage

# Październik 22, 2006, 19:35:35
Cytuj
Napisac goly interfejs IMemMgr i dziecziczaca po nim implementacje MyFancyMemMgr, ktora ma m.in. zwracanie stanu. Wtedy po prostu testujesz implementacje, a w kodzie uzywasz interfejsu.

Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...!

No cóż, super pomysł, szkoda że sam na niego nie wpadłem :-) jedyna wada jest taka że czasami trzeba będzie niektóre zmienne udostępniać protectem a nie privatem, ale IMHO to niewielka cena. No, i nie będę pisał interfejsów i ich implementował, tylko najnormalniej odziedziczę MemoryMgr.


Cytuj
(...) W porownaniu do czasow kompilacji odpalenie testow jednostkowych nie kosztuje nic.

Podejrzewam że wszystkie twoje testy są zgrupowane w jednym exeku, prawda? inaczej czas dostępu dysku oraz czas załadowania ich, uruchomienia i zwolnienia pamięci sprawiłby że spokojnie czas uruchamiania testów jednostkowych byłby liczony w sekundach. Już nie wspominając o tym, że same testy mogą przeprawdzać jakieś nietrywialne działania, np. dostęp do plików, alokacja pamięci, ładowanie grafik, dźwięków czy otwieranie trybu graficznego...

U mnie to będzie tak, że będzie po jedynm exeku na test jednego podsystemu silnika (podsystemów docelowo ma być kilkanaście), dodatkowo będą one wykonywały wiele z wspomnianych czasochłonnych działań - więc podpięcie ich pod kompilację raczej odpada.

Cytuj
Koshmaar: Coś mi się wydaje, że to nasze zainteresowanie inżynierią oprogramowania bardziej nam przeszkadza niż pomaga i trzeba pomyśleć o jakiejś zmianie podejścia do sprawy, bo inaczej do końca życia pozostaniemi pisaczami frameworków Smiley Ja właśnie w wakacje wziąłem się za szóste podejście (czy, żeby zabrzmiało bardziej jak z inżynierii - iterację Smiley) do mojego kodu, napisałem nowe moduły bazowe, wczoraj napisałem nowy framework, a od dzisiaj biorę się za manager zasobów.

Nie zgadzam się do końca, odrobina wiedzy teoretycznej nt. tego co może działać, a co nie, co zostało już sprawdzone doświadczalnie oraz jak teoretycznie powinien wyglądać dobry projekt, implementacja, zarządzanie itp. naprawdę każdemu się przyda. Inna sprawa, że nie należy z tym przesadzać i np. starać się zaprojektować szczegółowo cały silnik przed przystąpieniem do implementacji, gdyż do tego trzeba mieć naprawdę spore doświadczenie i kilka silników na koncie (IMHO). Co do mnie, piszę dopiero drugi oddzielny, uniwersalny silnik, we wcześniejszych grach wchodził on w skład kodu gry, i jak czegoś brakowało, to się wtedy to pisało :-) jednak wady takiego rozwiązania są oczywiste, więc w końcu postanowiłem załatwić to raz na kilka lat/miesięcy.


Cytuj
Dobrze kombinujesz, choć nie do końca. Zarządca pamięci (i ogólnie, wszelkie podsystemy ,,systemowe'') nie nadają się do testowania z zewnątrz. Dlatego sam zarządca będzie musiał udostępniać wersję (właśnie wersję, nie interfejs) zwracającą dane do analizy. Może to być np. bardzo rozbudowane logowanie opisujące obiekty, wskaźniki, obszary pamięci. Testowanie będzie polegało na napisaniu programu używającego zarządcy w możliwie różnorodny sposób i programu analizującego (może nawet graficznie) zachowanie zarządcy. Część tego można zautomatyzować (parowanie alokacji - dealokacji, zachodzenie na siebie obszarów pamięci, odwoływanie się do nieswojego obszaru). Zrobienie takiego programu będzie prawdopodobnie dość pracochłonne. Typowe narzędzia do autotestów też nie pomogą. Ale może się przydać.

Szczerze mówiąc, nie zrozumiałem. Chcę napisać auto test MemoryMgra, który jest bardzo prostą klasą - i tak ma pozostać. Nie zgadzam się na dodawnie żadnych metod, zmiennych czy żadnej innej dodatkowej funkcjonalności. Jeśli ktoś chce go sobie przetestować, niech zorganizuje sobie jakiś sprytny sposób na dostanie się do wewnętrznych danych.

Powyższe wymagania bardzo dobrze spełnia sposób podany przez kaleefa, natomiast nie wiem co rozumiesz np. pod pojęciem "wersję (właśnie wersję, nie interfejs) zwracającą dane do analizy". Następne zdanie o logowaniu mnie niepokoi... twierdzisz że MemMgr ma logować dane o swoim stanie zaś później ktoś ma tego loga przeparsować i orzec czy wszystko jest ok, czy są jakieś błędy? Eeee :-)

Jeśli źle zrozumiałem, to czy mógłbyś podać łopatologiczny przykład co masz na myśli? Np. co trzeba by zmienić w poniższym:


class MemoryMgr
 {
  public:

   static void Initialize();
 
   static bool AllocateMemory(const char * _file, const char * _function, int _line, const void * _adress,
                              const char * _var_name, const char * _type_name, AllocationType _alloc_type,
                              int _type_size, int _array_size = -1 );

   static bool DeallocateMemory(const char * _file, const char * _function, int _line,
                                const void * _adress, AllocationType _alloc_type);

   static void DumpMemoryReport( bool show_statistics = true, bool clear_statistics = true );

  private:   

    struct TMemoryEntry
      {
       std :: string filename;
       std :: string function;
       int line;

       AllocationType type;
       const void * adress;
       std :: string variable_name;
       std :: string variable_type;

       int array_size; // equals to -1 if not array
       int type_size;
      };

    typedef std::vector<TMemoryEntry> TMemoryEntries;
    typedef TMemoryEntries :: iterator TMemoryEntriesItor;
    typedef TMemoryEntries :: const_iterator TMemoryEntriesConstItor;

    static TMemoryEntries allocated;

 };


Cytuj
Przyznam, ze mnie zaciekawiliscie  Wink Czy moglibyscie sypnac troche wiecej informacji, bo w necie ciezko znalezc, a link podany wyzej wydaje sie byc demagogia (ten przyklad z pisaniem NameManglera jest conajmniej idiotyczny...)

Chociażby to:

http://en.wikipedia.org/wiki/Test_driven_development i linki wewnątrz
http://cppunit.sourceforge.net/doc/lastest/cppunit_cookbook.html


Btw, okazało się że mówiłem o UnitTest++ a myślałem o CppUnit (http://cppunit.sourceforge.net/cppunit-wiki) ;D UnitTest++ faktycznie jest całkiem prosty.