Fast testing with the SQLite in-memory database

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.

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

Download and install NUnit from http://nunit.org.

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 2, Models and Mapping
  3. Install the NUnit package using the NuGet Package Manager Console by executing the following command:
    Install-Package NUnit
    
  4. Install SQLite using the NuGet Package Manager Console by executing the following command:
    Install-Package System.Data.SQLite.Core
    

    Further information about SQLite can be found at https://system.data.sqlite.org.

    Note

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

  5. Install the log4net package using NuGet Package Manager Console by executing the following command:
    Install-Package log4net
    
  6. Add an application configuration file with NHibernate and log4net configuration sections just as we did in Chapter 1, The Configuration and Schema.
  7. Change the log4net configuration to use a ConsoleAppender.
  8. Add a new class named 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)
      {
      }
    }
  9. Add a new, static class named 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; }
    }
  10. Add a new, abstract class named 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();
    }
  11. Add a new, abstract class named 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);
    }
  12. Add a new class named PersistenceTests, inheriting NHibernateFixture.
  13. Decorate the PersistenceTests class with NUnit's TestFixture attribute.
  14. 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);
    }
  15. Build the project.
  16. Start NUnit.
  17. Select File | Open Project.
  18. Select the project's compiled assembly from the binDebug folder.
  19. Click on Run.

How it works...

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.

There's more...

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:

  • 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, after every test, use SQL to delete all rows in all tables. This is error-prone and potentially very slow.

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.119.128.113