poniedziałek, 23 sierpnia 2010

SQLite, NHibernate i testy jednostkowe

Oglądając screencasty z Summer Of NHibernate, zacząłem poznawanie NHibernate'a i chciałem wypróbować świeżo zdobytą wiedzę w praktyce, jednak po drodze natrafiłem na kilka trudności i problemów. Dlatego też w tym poście chciałbym napisać jak sobie to wszystko poukładałem, żeby działało tak jakbym sobie tego życzył.

SQLite
Na początek trzeba zaopatrzyć się w dotnetowego wrappera na bibliotekę SQLite (pobranie i zainstalowanie samej biblioteki uważam za krok oczywisty i nie wymagający komentarza). W moim przypadku wybór padł na System.Data.SQLite. Nazwa co prawda dziwna, ale pozwala na odróżnienie od innego providera SQLite.NET. Ma to kolosalne znaczenie przy współpracy z NHibernate (o tym później).
Warto pobrać wersję instalacyjną, dzięki niej od razu dostajemy możliwość pracy z bazami SQLite z poziomu Server Explorera w Visual Studio, co przydaje się później to tworzenia schematów bazy. Dość sporo czasu poświęciłem na znalezieniu menadżera baz danych SQLite, który umożliwiłby mi podstawową edycję baz, jednak w momencie, gdy zorientowałem się, że to samo mam dostępne w Server Explorerze straciły one na znaczeniu. Ale muszę przyznać, że trochę się rozczarowałem dostępnymi opcjami. Udało mi się znaleźć dwa programy godne jakiejkolwiek uwagi: SQLite Administrator oraz SQLite2009 Pro Enterprise Manager, jednak oba zawiodły mnie pod względem usability i strasznie ciężko mi się w nich pracowało. Na szczęście obecne rozwiązanie wystarcza mi w 100%.
Do projektu, w którym będziemy korzystać z NHibernate'a i połączenia z bazą dołączamy referencję do pliku System.Data.SQLite.dll.

NHibernate
Archiwum pobieramy stąd i rozpakowujemy :) Następnie dołączamy potrzebne biblioteki do projektu. I tu pierwszy plus ujemny NHibernate'a. Żeby wszystko się ładnie skompilowało trzeba dodać całkiem sporo bibliotek, a dokładniej: NHibernate.dll, Antlr3.Runtime.dll, Iesi.Collections.dll, NHibernate.ByteCode.Castle.dll. No ale cóż, jak mus to mus, jednak po dołączeniu jeszcze bibliotek testujących tworzy się całkiem długa lista referencji, a co gorsza biblioteki te są wymagane w runtimie dlatego muszą zostać skopiowane razem z plikiem wykonywalnym. 
Same mapowania omówię w którymś z następnych postów razem ze strukturą bazy danych. Teraz przedstawię konfigurację samego NHibernate'a odpowiedzialną za połączenie z bazą danych. Plik konfiguracyjny został zaczerpnięty z przykładowych szablonów dostępnych w domyślnej instalacji NHibernate'a:


NHibernate.Connection.DriverConnectionProvider
NHibernate.Driver.SQLite20Driver
Data Source="D:\Code\DSP\SoH\test.db";Version=3;New=True
NHibernate.Dialect.SQLiteDialect
true=1;false=0
NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle
true
 


Jednak ten szablon zakłada, że wykorzystywany jest inny provider SQLite niż ten, z którego ja korzystałem. Domyślnie do connection.driver_class przypisana jest wartość NHibernate.Driver.SQLite, co wydawało mi się wartością jak najbardziej rozsądną. Jednak gdy korzystamy z System.Data.SQLite ma to być NHibernate.Driver.SQLite20Driver. Koniec kropka i nie ma zmiłuj. 
Na razie większość swojej wiedzy czerpię ze screencastów Summer Of NHibernate i jestem z nich bardzo zadowolony. Jednak lojalnie muszę ostrzec, że już kilka razy zdarzyło mi się, że podana składnia nie chciała zadziałać. Ma to związek z tym, że są one oparte o starszą wersję NHibernate'a, nie ma to jednak większego wpływu na pozytywne wrażenia.
Opierając się właśnie na powyższych screencastach poprawne działanie testuję poprzez serię testów jednostkowych. W czasie nauki pojawia się jednak konieczność testowania operacji dodawania, usuwania oraz aktualizowania rekordów bazy. Aby zapewnić, że każdy test będzie pracował na takiej samej bazie danych (zawierającej te same, zdefiniowane przez nas rekordy) należy przed każdym testem przywracać jej strukturę. W tym miejscu pojawia się miejsce dla:

NDbUnit
Bibliotekę można pobrać stąd (strona zawiera dobry tutorial, na którym zresztą się oparłem). Ma  ona na celu utrzymanie jednolitego i z góry znanego stanu bazy danych przed każdym testem jednostkowym. Współpracuje z wieloma typami baz danych (oczywiście uwzględnia SQLite) oraz z całkiem pokaźną grupą narzędzi do testów jednostkowych (również wykorzystywany przeze mnie NUnit).
Do projektu dodajemy referencję do dwóch plików: obowiązkowego NDbUnit.Core.dll oraz charakterystycznego dla wykorzystywanej bazy danych, w moim przypadku NDbUnit.SqlLite.dll. 
Następnie należy przygotować DataSet zawierający opis struktury bazy danych, którą chcielibyśmy przywracać przed każdym testem. Można to zrobić w prosty sposób z poziomu Visual Studio. Dodajemy do projektu nowy plik typu DataSet (z grupy Data) i nadajemy mu nazwę na przykład Database.xsd. Następnie z Server Explorera przeciągamy do niego te tabele, które mają być aktualizowane w bazie i zapisujemy (można usunąć pliki stworzone automatycznie przez VS).
Teraz należy stworzyć plik zawierający dane testowe. Jeśli już mamy dane w bazie danych najłatwiej posłużyć się metodami udostępnionymi przez NDbUnit. Wystarczy raz wykonać kawałek kodu podobny do poniższego:
string _connectionString = @"data source=D:\Code\DSP\SoH\test.db";
var mySqlDatabase = 
new NDbUnit.Core.SqlLite.SqlLiteUnitTest(_connectionString);
mySqlDatabase.ReadXmlSchema(@"..\..\TestData\Database.xsd");
System.Data.DataSet ds = mySqlDatabase.GetDataSetFromDb();
ds.WriteXml(@"..\..\TestData\TestData.xml");
Mając już te dwa pliki można przygotować sobie strukturę testów jednostkowych. Przed całym Test Fixture (zestaw testów, nie wiem jak to przetłumaczyć na polski) wczytujemy zarówno schemat bazy jak i same dane, dzięki temu mamy bazę danych w stanie wyjściowym:
private INDbUnitTest sqlDatabase;
string connectionString = @"data source=D:\Code\DSP\SoH\test.db";

[TestFixtureSetUp]
public void TestFixtureSetup()
{
 sqlDatabase = new NDbUnit.Core.SqlLite.SqlLiteUnitTest(connectionString);
 sqlDatabase.ReadXmlSchema(@"..\..\TestData\Database.xsd");
 sqlDatabase.ReadXml(@"..\..\TestData\TestData.xml");
}
Następnie przed każdym testem przywracamy pożądany stan bazy:
[SetUp]
public void SetUp()
{
  sqlDatabase.PerformDbOperation(DbOperationFlag.CleanInsertIdentity);
}
I na koniec wszystkich testów pamiętamy, aby przywrócić dane (mogą nam się przydać poza testami).
[TestFixtureTearDown]
public void TestFixtureTearDown()
{
  sqlDatabase.PerformDbOperation(DbOperationFlag.CleanInsertIdentity);
}
Dzięki takiemu podejściu wszystko jest zautomatyzowane i nie musimy się przejmować przywracaniem domyślnego stanu w ciele każdego testu, w którym mamy zamiar modyfikować zawartość bazy.
Wydaje mi się, że takie postępowanie można zastosować jedynie do testowych baz danych. Nie uważam, że byłoby pożądane pracowanie w taki sposób z bazami produkcyjnymi. W końcu tam dane mogą się zmieniać cały czas i nigdy nie będziemy na bieżąco. 

Troszkę się napracowałem, żeby wszystko działało tak jak trzeba, parę razy mocno się zirytowałem, ale za każdym razem okazywało się, że to tylko i wyłącznie wina mojego niedbalstwa lub przeoczenia istotnych faktów. Tak to bywa, jak się nie pisało niczego większego przez ponad pół roku. Ale mam nadzieję, że powoli będę się rozkręcał.

2 komentarze:

  1. Zdaje się, że pomyliłeś link NHibernate z NDbUnit. Poza tym ciekawie. :)

    OdpowiedzUsuń
  2. Faktycznie... Już poprawione. Dzięki za czujność :)

    OdpowiedzUsuń