Integration tests, NHibernate and Sqlite, part 2 – in file database
Integration tests, NHibernate and Sqlite, part 3 – enhanced in memory database
Source code can be downloaded here.
Purpose of unit test is to determine if specific unit of code fits for its use. Units are the smallest testable parts of a system. Integration tests evaluate that unit or subsystem is well integrated with other units or subsystems. NHibernate is quite large and complex system and if you use it in your test then it is an integration test – your test evaluates, among other things, that the unit (or something larger) plays well with NH. That is the reason why I will use integration tests term in this post, although somebody may disagree:).
Embedded databases are suitable for integration testing. It is mainly for performance and deployment reasons. Such well established and popular database is Sqlite which has some interesting features and I will use it in this post.
To test something with NHibernate we need some domain model. Since my imagination is not very playful, I will use very ordinary “Order” example:
public class Order { public Order() { Rows = new Iesi.Collections.Generic.HashedSet<OrderRow>(); } public virtual int Id { get; set; } public virtual string ShipName { get; set; } public virtual DateTime RequiredDate { get; set; } public virtual Iesi.Collections.Generic.ISet<OrderRow> Rows { get; set; } } public class OrderRow { public virtual int Id { get; set; } public virtual decimal Price { get; set; } public virtual string Product { get; set; } }
Mapped with Fluent NHibernate:
public class OrderMap : ClassMap<Order> { public OrderMap() { Id(x => x.Id); HasMany(x => x.Rows).Cascade.AllDeleteOrphan(); Map(x => x.RequiredDate); Map(x => x.ShipName); } } public class OrderRowMap : ClassMap<OrderRow> { public OrderRowMap() { Id(x => x.Id); Map(x => x.Price); Map(x => x.Product); } }
Naive implementation
It is a boring and useless example. But we need just something to feed NHibernate and the domain model could be even a little bit simpler and more useless than this crap.
We build NH configuration, session factory and database schema before each test:
string connectionString = "Data Source=:memory:;Version=3;New=True;"; var config = Fluently.Configure() .Database(SQLiteConfiguration.Standard.ConnectionString(connectionString)) .Mappings(m => { m.FluentMappings.AddFromAssembly(typeof(Order).Assembly); }) .BuildConfiguration(); var sessionFactory = config.BuildSessionFactory() var connection = new SQLiteConnection(connectionString); connection.Open(); // build the database schema var schemaExport = new SchemaExport(config); schemaExport.Execute(false, true, false, connection, TextWriter.Null); // voiala, session is ready for the test var session = sessionFactory.OpenSession(connection);There is something worth noting in this code. Sqlite cleans up in-memory database when all connections to that database are closed. This is the reason why the connection is explicitly opened and then provided to Execute and OpenSession methods. The database would be empty without this connection sharing.
Everything goes nice. Tests are pretty good isolated because they receive own database – they run in own sandboxes and cannot cause any harm by destroying data needed by other tests.
Some troubles will appear as soon as the domain complexity and number of integration tests get a little bit higher. Building configuration, session factory and database schema may take several seconds and execution time of test suit may become intolerable very quickly.
Speedup
It would be nice to do time consuming operation only once per a test suit run. Two things can be shared between tests without losing advantages of test isolation: NH configuration and session factory. This can save significant amount of time. The database still must be created for each individual test.
Here is the complete example factored into class:
public class SqliteInMemorySharedScope { private static ISessionFactory sessionFactory; private static NHibernate.Cfg.Configuration configuration; private static IDbConnection connection; private const string connectionString = "Data Source=:memory:;Version=3;New=True;"; public SqliteInMemorySharedScope() { if (configuration == null) { configuration = Fluently.Configure() .Database(SQLiteConfiguration.Standard.ConnectionString(connectionString)) .Mappings(m => { m.FluentMappings.AddFromAssembly(typeof(Order).Assembly); }) .BuildConfiguration(); } if (sessionFactory == null) sessionFactory = configuration.BuildSessionFactory(); if (connection == null) connection = new SQLiteConnection(connectionString); var schemaExport = new SchemaExport(configuration); schemaExport.Execute(false, true, false, connection, TextWriter.Null); } public ISession OpenSession() { ISession session = sessionFactory.OpenSession(connection); return session; } }And here is how to use that:
var scope1 = new SqliteInMemorySharedScope(); using (var session1 = scope.OpenSession()) { var row = new OrderRow() { Product = "Product1", Price = 10, }; session1.Save(row); session1.Flush(); } var scope2 = new SqliteInMemorySharedScope() using (var session2 = scope2.OpenSession()) { Assert.AreEqual(0, session2.CreateQuery("from OrderRow").List().Count); }The configuration and session factory are built when scope1 is instantiated. When scope2 is created then only database schema must be established again.
Why SqliteInMemorySharedScope?
Sqlite – Sqlite database engine.
InMemory – database is stored in-memory.
Shared – connection is shared between all scopes.
There must be a better naming scheme but I am not able to find anything that is reasonable. Any idea?
No comments:
Post a Comment