Running a full range of tests for a large NHibernate application can take some time. In this recipe, we will show you how to use SQLite's in-memory database to speed up this process.
Download and install NUnit from http://nunit.org.
Eg.Core
model from Chapter 2, Models and MappingNUnit
package using the NuGet Package Manager Console by executing the following command:Install-Package NUnit
Install-Package System.Data.SQLite.Core
Further information about SQLite can be found at https://system.data.sqlite.org.
log4net
package using NuGet Package Manager Console by executing the following command:Install-Package log4net
NHibernate
and log4net
configuration sections just as we did in Chapter 1, The Configuration and Schema.log4net
configuration to use a ConsoleAppender
.TestConnectionProvider
with the following code:public class TestConnectionProvider : DriverConnectionProvider { [ThreadStatic] private static IDbConnection _connection; public static void CloseDatabase() { if (_connection != null) _connection.Dispose(); _connection = null; } public override IDbConnection GetConnection() { if (_connection == null) { _connection = Driver.CreateConnection(); _connection.ConnectionString = ConnectionString; _connection.Open(); } return _connection; } public override void CloseConnection(IDbConnection conn) { } }
NHibernateSessionFactoryProvider
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 NHibernateSessionFactoryProvider() { _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
with 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
, inheriting BaseFixture
, with the following code:protected ISessionFactory SessionFactory { get { return NHibernateSessionFactoryProvider.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 = NHibernateSessionFactoryProvider.SessionFactory; var session = CurrentSessionContext.Unbind(sessionFactory); session.Close(); } private void BuildSchema() { var cfg = NHibernateSessionFactoryProvider.Configuration; var schemaExport = new SchemaExport(cfg); schemaExport.Create(false, true); }
PersistenceTests
, inheriting 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.NHibernateSessionFactoryProvider
loads a NHibernate configuration from the App.config
and 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 might 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, normally SQLite's in-memory databases only support 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.
At the start of each test, we close any lingering connections. This ensures we get a fresh and empty database. When NHibernate first calls GetConnection()
, we open a new connection. We then return the same connection for each subsequent call, ignoring any calls to CloseConnection()
. Finally, when the test is completed, we dispose the database connection, thus 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 the memory.
The test uses the session from 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 it, we load it from the database rather than just returning the instance from the first level cache. This gives us a true test of what we have persisted in the database. Once we have fetched the movie 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.
Since SQLite version 3.3, in-memory databases can actually handle multiple connections, which means that the special connection handling used in this recipe isn't strictly needed. Instead, we could just use a connection string, with the cache=shared
setting, for example:
FullUri=file:mydatabase.db?mode=memory&cache=shared
However, there is one caveat with this approach, which is the fact that SQLite doesn't allow simultaneous write access for multiple connections. A test method that opens multiple sessions may therefore fail in a way that the production code wouldn't. In many ways, the "singleton connection" approach better resembles a production scenario.
SQLite's speed and small memory footprint makes it great for providing quick test feedback. However, since it's in some ways a bit limited, it is best to run all tests against the production database engine (but not the production database!) before deploying the application. There are a few approaches to testing with a real RDBMS, each with its share of issues:
18.119.128.113