DUnit (biblioteka obsługująca unit testy pod Delphi) posiada miłą opcję wykrywania
przecieków pamięci podczas wykonywania testów;
tzn. sprawdza całkowitą ilość zaalokowanej pamięci przed uruchomieniem testu i po jego zakończeniu,
a gdy druga wartość jest większa od pierwszej, zgłasza (opcjonalnie) błąd wykonania testu.
Aby włączyć tę funkcjonalność w DUnit,
należy skorzystać z pozycji Fail TestCase if memory leaked pod menu Options.
Niestety, program może zacząć zgłaszać błędy, na pierwszy rzut oka, nieprawidłowo.
Mianowicie wtedy, gdy podczas testu alokowana jest pamięć,
która jest zwalniania nie w samym teście, ale później, np. przy zamykaniu programu.
Dzieje się tak często z wywołaniami systemowymi, bibliotekami zewnętrznymi, czy interfejsami GUI
(choć, rzecz jasna, może się pojawić przy korzystaniu z własnego kodu).
Wszak typowym rozwiązaniem stosowanym w programowaniu jest alokacja pamięci w sposób "leniwy" -
przy pierwszym wywołaniu procedury/funkcji czy przy pierwszym odwołaniu do obiektu/klasy,
które wymagają dodatkowej pamięci.
A pamięć w ten sposób przydzielona często jest zwalniana dopiero wtedy, gdy klasa/moduł
usuwane są z pamięci systemu podczas zamykania aplikacji (np. w sekcji finalization modułu).
Załóżmy np., że testujemy kod bazodanowy (MySQL via DBExpress).
W teście tworzymy połączenie, wykonujemy operacje na danych i zwalniamy połączenie.
Niestety, ponieważ DBExpress alokuje pamięć przy tworzeniu połączenia po raz pierwszy,
i nie zwalnia jej dopóki aplikacja nie zakończy działania, otrzymujemy błąd przeciekania pamięci:
procedure TYACDBTests.TestMemory;
var
LConnection: TSQLConnection;
begin
LConnection := CreateSQLConnection;
Check(TRUE);
FreeAndNIL(LConnection);
end;
Kod jest w pełni poprawny (gdzie CreateSQLConnection tworzy obiekt, ustawia odpowiednia parametry i nawiązuje połączenie),
ale nadal otrzymujemy informację o przeciekającej pamięci...
Jednym z możliwych rozwiązań jest dodanie kodu,
który wymusiłby tę pierwszą alokację jeszcze przed uruchomieniem testów,
aby w trakcie testów pamięć była już przydzielona
(a więc i uwzględniona w obliczeniach zajętości pamięci przed testem).
Kod taki można umieścić np. w części inicjalizacyjnej modułu:
var
LConnection: TSQLConnection;
initialization
LConnection := CreateSQLConnection;
FreeAndNIL(LConnection);
end.
Teraz, gdy tworzymy połączenie TSQLConnection w teście,
nie alokuje już ono więcej pamięci, zatem i DUnit nie raportuje przecieku... ;-)
Tego typu sytuacje są w miarę łatwo wykrywalne (choć nie w 100%) -
gdy w pierwszym przebiegu DUnit zgłasza przeciek, ale przy drugim już nie
(oba przebiegi wykonujemy podczas jednego uruchomienia programu DUnit).
Wtedy mamy dużą szansę na to, że pamięć jest alokowana w pierwszym przebiegu,
a w drugim jest już tylko wykorzystywana.
I jest to dobry kandydat na rozwiązanie opisane powyżej,
choć nie zawsze ustalenie, jaki kod należy uruchomić wcześniej, jest tak proste jak wyżej...
Góra
|