Fast testing with SQLite in-memory database

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.

Note

This is not meant to replace running integration tests against the real RDBMS before moving to production. Rather, it is a smoke test to provide feedback to developers quickly before running the slower integration tests.

Getting ready

  1. Download and install NUnit from http://nunit.org.
  2. Download and install SQLite from http://sqlite.phxsoftware.com.

    Note

    Note: This recipe will work with other test frameworks such as MSTest, MbUnit, and xUnit. Just replace the NUnit-specific attributes with those for your preferred framework.

How to do it...

  1. Create a new, empty class library project.
  2. Add references to our Eg.Core model from Chapter 1, as well as nunit.framework, System.Data.Sqlite, log4net, NHibernate and NHibernate.ByteCode.Castle.

    Note

    System.Data.Sqlite has 32-bit and 64-bit versions. Use the appropriate file for your operating system and target platform.

  3. Add an application configuration file with NHibernate and log4net configuration sections just as we did in Chapter 2.
  4. Change the log4net configuration to use a ConsoleAppender.
  5. Add a new, static class named 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;
      }
    }
  6. Add a new, abstract class named 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();
    }
  7. Add a new, abstract class named 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);
    }
  8. Add a new class named PersistenceTests, inherited from NHibernateFixture.
  9. Decorate the PersistenceTests class with NUnit's TestFixture attribute.
  10. Add the following test method to 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);
    
    }
  11. Build the project.
  12. Start NUnit.
  13. Select File | Open Project.
  14. Select the project's compiled assembly from the binDebug folder.
  15. Click on Run.

How it works...

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.

There's more...

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:

  • Drop and recreate the database between each test. This is extremely slow for enterprise-level databases. A full set of integration tests may take hours to run, but this is the least intrusive option.
  • Roll back every transaction to prevent changes to the database. This is very limiting. For instance, even our simple Persistence test would require some significant changes to work in this way. This may require you to change business logic to suit a testing limitation.
  • Clean up on a test-by-test basis. For instance, for every insert, perform a delete. This is a manual, labor-intensive, error-prone process.

See also

  • Preloading data with SQLite
  • Using the Fluent NHibernate Persistence tester
  • Using the Ghostbusters test
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.249.210