Running a full range of tests for a large NHibernate application can take some time. In this recipe, I will show you how to use SQLite's in-memory database to speed up this process.
Eg.Core
model from Chapter 1, as well as nunit.framework
, System.Data.Sqlite
, log4net
, NHibernate
and NHibernate.ByteCode.Castle
.ConsoleAppender
.NHConfigurator
with the following code:private const string CONN_STR = "Data Source=:memory:;Version=3;New=True;"; private static readonly Configuration _configuration; private static readonly ISessionFactory _sessionFactory; static NHConfigurator() { _configuration = new Configuration().Configure() .DataBaseIntegration(db => { db.Dialect<SQLiteDialect>(); db.Driver<SQLite20Driver>(); db.ConnectionProvider<TestConnectionProvider>(); db.ConnectionString = CONN_STR; }) .SetProperty(Environment.CurrentSessionContextClass, "thread_static"); var props = _configuration.Properties; if (props.ContainsKey(Environment.ConnectionStringName)) props.Remove(Environment.ConnectionStringName); _sessionFactory = _configuration.BuildSessionFactory(); } public static Configuration Configuration { get { return _configuration; } } public static ISessionFactory SessionFactory { get { return _sessionFactory; } }
BaseFixture
using the following code:protected static ILog log = new Func<ILog>(() => { log4net.Config.XmlConfigurator.Configure(); return LogManager.GetLogger(typeof(BaseFixture)); }).Invoke(); protected virtual void OnFixtureSetup() { } protected virtual void OnFixtureTeardown() { } protected virtual void OnSetup() { } protected virtual void OnTeardown() { } [TestFixtureSetUp] public void FixtureSetup() { OnFixtureSetup(); } [TestFixtureTearDown] public void FixtureTeardown() { OnFixtureTeardown(); } [SetUp] public void Setup() { OnSetup(); } [TearDown] public void Teardown() { OnTeardown(); }
NHibernateFixture
, inherited from BaseFixture
, with the following code:protected ISessionFactory SessionFactory { get { return NHConfigurator.SessionFactory; } } protected ISession Session { get { return SessionFactory.GetCurrentSession(); } } protected override void OnSetup() { SetupNHibernateSession(); base.OnSetup(); } protected override void OnTeardown() { TearDownNHibernateSession(); base.OnTeardown(); } protected void SetupNHibernateSession() { TestConnectionProvider.CloseDatabase(); SetupContextualSession(); BuildSchema(); } protected void TearDownNHibernateSession() { TearDownContextualSession(); TestConnectionProvider.CloseDatabase(); } private void SetupContextualSession() { var session = SessionFactory.OpenSession(); CurrentSessionContext.Bind(session); } private void TearDownContextualSession() { var sessionFactory = NHConfigurator.SessionFactory; var session = CurrentSessionContext.Unbind(sessionFactory); session.Close(); } private void BuildSchema() { var cfg = NHConfigurator.Configuration; var schemaExport = new SchemaExport(cfg); schemaExport.Create(false, true); }
PersistenceTests
, inherited from NHibernateFixture
.PersistenceTests
class with NUnit's TestFixture
attribute.PersistenceTests
:[Test] public void Movie_cascades_save_to_ActorRole() { Guid movieId; Movie movie = new Movie() { Name = "Mars Attacks", Description = "Sci-Fi Parody", Director = "Tim Burton", UnitPrice = 12M, Actors = new List<ActorRole>() { new ActorRole() { Actor = "Jack Nicholson", Role = "President James Dale" } } }; using (var session = SessionFactory.OpenSession()) using (var tx = session.BeginTransaction()) { movieId = (Guid)session.Save(movie); tx.Commit(); } using (var session = SessionFactory.OpenSession()) using (var tx = session.BeginTransaction()) { movie = session.Get<Movie>(movieId); tx.Commit(); } Assert.That(movie.Actors.Count == 1); }
binDebug
folder.NHConfigurator
loads an NHibernate configuration from the App.config
, then overwrites the dialect, driver, connection provider, and connection string properties to use SQLite instead. It also uses the thread static session context to provide sessions to code that may rely on NHibernate contextual sessions. Finally, we remove the
connection.connection_string_name
property, as we have provided a connection string value.
The magic of SQLite happens in our custom TestConnectionProvider
class. Typically, a connection provider will return a new connection from each call to GetConnection()
, and close the connection when CloseConnection()
is called. However, each SQLite in-memory database only supports a single connection. That is, each new connection creates and connects to its own in-memory database. When the connection is closed, the database is lost. When each test begins, we close any lingering connections. This ensures we will get a fresh, empty database. When NHibernate first calls GetConnection()
, we open a new connection. We return this same connection for each subsequent call. We ignore any calls to CloseConnection()
. Finally, when the test is completed, we dispose the database connection, effectively disposing the in-memory database with it.
This provides a perfectly clean database for each test, ensuring that remnants of a previous test cannot contaminate the current test, possibly altering the results.
In BaseFixture
, we configure log4net and set up some virtual methods that can be overridden in inherited classes.
In NHibernateFixture
, we override OnSetup
, which runs just before each test. For code that may use contextual sessions, we open a session and bind it to the context. We also create our database tables with NHibernate's schema export. This, of course, opens a database connection, establishing our in-memory database.
We override OnTeardown
, which runs after each test, to unbind the session from the session context, close the session, and finally close the database connection. When the connection is closed, the database is erased from memory.
The test uses the session from the NHibernateFixture
to save a movie with an associated ActorRole
. We use two separate sessions to save, and then fetch the movie to ensure that when we fetch the movie, we load it from the database rather than just returning the instance from the first level cache. This gives us a true tests of what we have persisted in the database. Once we've fetched the movie back from the database, we make sure it still has an ActorRole
. This test ensures that when we save a movie, the save cascades down to ActorRoles
in the Actors
list as well.
While SQLite in-memory databases are fast, the SQLite engine has several limitations. For example, foreign key constraints are not enforced. Its speed makes it great for providing quick test feedback, but because of the limitations, before deploying the application, it is best to run all tests against the production database engine. There are a few approaches to testing with a real RDBMS, each with significant issues, which are as follows: