Thomas Bandt

Über mich | Kontakt | Archiv

Fluent NHibernate - Mappings testen

Die Konfiguration von NHibernate über die XML-Dateien war mir immer ein Graus, weshalb ich mich auch nie ernsthaft damit beschäftigt habe. Erst als vor ein paar Monaten der Drang Lazy Loading zu verwenden immer größer wurde und sich eine Implementierung mit Linq to Sql als eher schwierig darstellte, wechselte ich zu NHiberate. Genauer zu Fluent NHibernate. Damit ist die Konfiguration/das Mapping wirklich ein Kinderspiel:

   1:  public class CompanyMap : ClassMap<Company>
   2:  {
   3:      public CompanyMap()
   4:      {
   5:          Table("Companies");
   6:          Id(x => x.ID);
   7:          Map(x => x.Created).Not.Nullable();
   8:          Map(x => x.Modified).Nullable();
   9:          Map(x => x.Name).Not.Nullable().Length(50);
  10:          Map(x => x.TimeZoneID).Column("TimeZone").Not.Nullable();
  11:          Map(x => x.Language).CustomType(typeof(Language)).Not.Nullable();
  12:          References(x => x.Client).Column("FKClientID").Not.Nullable().LazyLoad();
  13:          HasMany(x => x.Users)
  14:              .Table("Users")
  15:              .KeyColumn("FKCompanyID")
  16:              .Inverse()
  17:              .LazyLoad()
  18:              .Access.CamelCaseField();
  19:      }
  20:  }

Nun kann man einfach hergehen, lustig loskonfigurieren und gucken was passiert. Bei normalen Properties wird das auch funktinieren, schwierig wird es aber bei Referenzen, Listen usw. - also allem, was eher unlustige Seiteneffekte erzeugen kann. Hier war schnell klar, dass ich diesen Part mit Tests abgedeckt haben möchte.

Die Grundvoraussetzung: eine Testdatenbank.

Gleiches Problem wie damals schon mit Linq to Sql - keine Lust, diese selbst anzulegen und zu pflegen. Aber auch hier kein Problem: NHibernate kann aus dem Schema, welches sich aus der Konfiguration ergibt, die Datenbankobjekte selbst erzeugen. Aber Achtung: nicht die Datenbank selbst! Während Linq to Sql einfach eine komplette neue Datenbank anlegt und auch löschen kann, geht das hier nur mit den Objekten innerhalb der Datenbank. Damit kann man viel Zeit vertrödeln - also gleich eine leere Test-Datenbank anlegen.

Damit sich die Zeremonie per Test Fixture in Grenzen hält, habe ich auch hier wieder eine Basisklasse erstellt, die ich für meine Tests verwende:

   1:  public class SqlServerDatabaseTest : IDisposable
   2:  {
   3:   
   4:      private Configuration Configuration;
   5:      private static ISessionFactory SessionFactory;
   6:      protected ISession Session;
   7:   
   8:      public SqlServerDatabaseTest()
   9:      {
  10:   
  11:          if (Configuration == null)
  12:          {
  13:              SessionFactory = Fluently.Configure()
  14:                  .Database(MsSqlConfiguration.MsSql2008
  15:                      .ConnectionString(c => c.FromConnectionStringWithKey("TestDatabaseConnectionString")))
  16:                  .Mappings(m => m.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly()))
  17:                  .ExposeConfiguration(BuildSchema)
  18:                  .BuildSessionFactory();
  19:          }
  20:   
  21:          Session = SessionFactory.OpenSession();
  22:          new SchemaExport(Configuration).Execute(true, true, false, Session.Connection, Console.Out);
  23:   
  24:      }
  25:   
  26:      private void BuildSchema(Configuration configuration)
  27:      {
  28:          Configuration = configuration;
  29:          new SchemaExport(configuration).Drop(false, true);
  30:          new SchemaExport(configuration).Create(false, true);
  31:      }
  32:          
  33:      public void Dispose()
  34:      {
  35:          Session.Dispose();
  36:      }
  37:   
  38:  }

Ich habe mich übrigens für die Verwendung einer "echten" SQL-Server-Datenbank entschieden, weil das Verhalten einer - in vielen Beispielen oft verwendeten - In-Memory-Datenbank mit SQLite doch von dem abweicht bzw. abweichen kann. So bin ich auf der sicheren Seite, dass keine von der verwendeten Datenbank abhängigen Seiteneffekte auftreten und die Ergebnisse verfälschen.

Die Tests

Fluent NHibernate beglückt uns mit einem Feature, was sich Persistence specification testing nennt. Damit ist es wirklich kinderleicht zu testen, ob ein bestimmter Teil des Objektes auch wirklich gemappt wird.

Damit das funktioniert, erzeuge ich mir zunächst gültige Testdaten, speichere diese in der Datenbank ab und fahre dann den Test dagegen. Das Erzeugen der Testdaten vorab erspart einiges an grauen Haaren, die man sich einfängt, wenn man nach dem Vorschlag im oben verlinkten Wiki-Artikel zu "Testing references" verfährt.

Die Ausnahme: Private Setter

Anlass dieses Posts ist eine Umstellung, die ich gestern vorgenommen habe. Ich habe beschlossen, Listen von Objekten nur noch als IEnumerable<> herauszugeben, im Fall der Company also die Liste der User, die ihrzugeordnet sind:

   1:  public class Company : EntityBase
   2:  {
   3:   
   4:      public Company()
   5:      {
   6:          users = new List<User>();
   7:      }
   8:   
   9:      public virtual Client Client { get; set; }
  10:      public virtual string Name { get; set; }
  11:      public virtual int TimeZoneID { get; set; }
  12:      public virtual Language Language { get; set; }
  13:   
  14:      private readonly IList<User> users;
  15:   
  16:      public virtual IEnumerable<User> Users
  17:      {
  18:          get { return users; }
  19:      }
  20:   
  21:      public virtual void AddUser(User user)
  22:      {
  23:          users.Add(user);
  24:      }
  25:   
  26:  }

Bei der Konfiguration ist das kein Problem - man kann für diesen Fall eine "Access Strategy" wählen. Trotzdem habe ich mehr als eine Stunde lang gelaubt, dass irgendetwas nicht funktioniert, denn der Test mit PersistenceSpecification funktioniert einfach nicht mehr. Bis ich dann nach langer Suche bei Google auf die Antwort gestoßen bin. Es ist also einfach nicht implementiert - auf gut Deutsch: sofern kein öffentlicher Setter da ist, lässt sich das Ding nicht testen.

Für diese Fälle gehe ich nun so vor, wie ich es auch bereits mit Linq to Sql gehalten habe, ich speichere manuell ein Objekt und lade es mir dann wieder um zu sehen, ob der Vorgang erfolgreich war. Auch hier kommt mir wieder das Verwenden von richtigen Testdaten zugute.

Nachfolgend noch einmal ein komplettes Test Fixture:

   1:  [TestFixture]
   2:  public class CompanyMapping : SqlServerDatabaseTest
   3:  {
   4:   
   5:      private Client TestClient;
   6:      private Company TestCompany;
   7:      private User TestUser;
   8:   
   9:      [TestFixtureSetUp]
  10:      public void Setup()
  11:      {
  12:   
  13:          TestClient = TestData.GetValidClient();
  14:          TestClient.ID = (int)Session.Save(TestClient);
  15:   
  16:          TestCompany = TestData.GetValidCompany(TestClient);
  17:          TestCompany.ID = (int)Session.Save(TestCompany);
  18:   
  19:          TestUser = TestData.GetValidUser(TestClient, TestCompany);
  20:          TestUser.ID = (int)Session.Save(TestUser);
  21:   
  22:          Session.Flush();
  23:   
  24:      }
  25:   
  26:      [TestFixtureTearDown]
  27:      public void CleanUp()
  28:      {
  29:          Dispose();
  30:      }
  31:   
  32:      [Test]
  33:      public void PersistenceSpecification()
  34:      {
  35:   
  36:          new PersistenceSpecification<Company>(Session)
  37:              .CheckProperty(c => c.ID, TestCompany.ID)
  38:              .CheckProperty(c => c.Created, TestCompany.Created)
  39:              .CheckProperty(c => c.Modified, TestCompany.Modified)
  40:              .CheckProperty(c => c.Name, TestCompany.Name)
  41:              .CheckProperty(c => c.TimeZoneID, TestCompany.TimeZoneID)
  42:              .CheckProperty(c => c.Language, TestCompany.Language)
  43:              .CheckReference(c => c.Client, TestCompany.Client)
  44:              .VerifyTheMappings(TestCompany);
  45:   
  46:      }
  47:   
  48:      [Test]
  49:      public void HasMany_Users()
  50:      {
  51:   
  52:          var company = Session.Linq<Company>().Single(c => c.ID == TestCompany.ID);
  53:          Assert.That(company.Users.Count(), Is.EqualTo(0));
  54:   
  55:          company.AddUser(TestUser);
  56:          Session.Update(company);
  57:   
  58:          company = Session.Linq<Company>().Single(c => c.ID == TestCompany.ID);
  59:          Assert.That(company.Users.Count(), Is.EqualTo(1));
  60:   
  61:      }
  62:   
  63:  }

That's it :-)



« Zurück  |  Weiter »