Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Copy in-memory SQLite Database to make unit tests faster

In C#/nHibernate-projects I am using SQLite to unit test my code, aproximately using the method described here: http://ayende.com/blog/3983/nhibernate-unit-testing.

However, I find that building and configuring the in-memory database typically takes about 150ms. I have lots of unit test so this rapidly adds up.

I want to build and configure the database once, store it in a static variable, and copy it every time a unit test needs a database.

How do I back-up an in-memory database?

I first tried to create a named in-memory database. According to https://www.sqlite.org/inmemorydb.html this is possible. I used to have:

    private const string ConnectionString = "Data Source=:memory:;Version=3;";

Connection strings I tried are:

    private const string ConnectionString = "FullUri=file:memorydb.db?mode=memory&cache=shared";
    private const string ConnectionString2 = "FullUri=file:memorydb2.db?mode=memory&cache=shared";

So now I just have to find out how to quickly copy content from one to another? I'm almost there: I can create two in-memory databases, and call "BackupDatabase" to copy the database.

The unit test however, behaves like the "instance" database has no tables, even the "prototype" database does.

        private static ISessionFactory _prototypeSessionFactory;
        private const string InstanceConnectionString = "FullUri=file:memorydb.db?mode=memory&cache=shared";

        private const string PrototypeConnectionString = "FullUri=file:memorydb2.db?mode=memory&cache=shared";
        private SQLiteConnection _instanceConnection;
        private ISessionFactory _instanceSessionFactory;

        public DatabaseScope(Assembly assembly)
        {
            var prototyeConfiguration = SQLiteConfiguration.Standard.ConnectionString(PrototypeConnectionString);
            var cfg = Fluently
                .Configure()
                .Database(prototyeConfiguration)
                .Mappings(m => m.HbmMappings.AddFromAssembly(assembly));
            cfg.ExposeConfiguration(BuildSchema);
            _prototypeSessionFactory = cfg.BuildSessionFactory();

            var instanceConfiguration = SQLiteConfiguration.Standard.ConnectionString(InstanceConnectionString);
            _instanceSessionFactory = Fluently
                .Configure()
                .Database(instanceConfiguration)
                .BuildSessionFactory();

            CopyDatabase();
        }

        private void CopyDatabase()
        {
            var cnnIn = new SQLiteConnection(PrototypeConnectionString);
            var cnnOut = new SQLiteConnection(InstanceConnectionString);
            cnnIn.Open();
            cnnOut.Open();
            cnnIn.BackupDatabase(cnnOut, "main", "main", -1, null, -1);
            cnnIn.Close();
            cnnOut.Close();
        }
like image 568
realbart Avatar asked Jun 26 '15 12:06

realbart


People also ask

Should I use in memory database for testing?

This database provider allows Entity Framework Core to be used with an in-memory database. While some users use the in-memory database for testing, this is generally discouraged; the SQLite provider in in-memory mode is a more appropriate test replacement for relational databases.

Does SQLite cache in memory?

SQLite provides an in-memory cache which you size according to the maximum number of database pages that you want to hold in memory at any given time. Berkeley DB also provides an in-memory cache that performs the same function as SQLite.

Does SQLite store in memory?

SQLite in-memory databases are databases stored entirely in memory, not on disk. Use the special data source filename :memory: to create an in-memory database. When the connection is closed, the database is deleted.

Should you mock a database?

Mocking and stubbing are the cornerstones of having quick and simple unit tests. Mocks are useful if you have a dependency on an external system, file reading takes too long, the database connection is unreliable, or if you don't want to send an email after every test.


2 Answers

I ended up with this working code. My unit test duration went from over ten minutes to under two minutes. (Code slightly simplified for readability)

using System;
using System.Data;
using System.Data.SQLite;
using System.IO;
using System.Reflection;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Mapping;
using NHibernate.Tool.hbm2ddl;

namespace TestHelper.DbHelper.SqLite
{
    public class DatabaseScope : IDisposable
    {
        private static Assembly _prototypeAssembly;
        private const string PrototypeConnectionString = "FullUri=file:prototype.db?mode=memory&cache=shared";
        private static ISessionFactory _prototypeSessionFactory;
        private static SQLiteConnection _prototypeConnection;

        private const string InstanceConnectionString = "FullUri=file:instance.db?mode=memory&cache=shared";
        private ISessionFactory _instanceSessionFactory;
        private SQLiteConnection _instanceConnection;

        public DatabaseScope(Assembly assembly)
        {
            InitDatabasePrototype(assembly);
            InitDatabaseInstance();
        }

        private void InitDatabasePrototype(Assembly assembly)
        {
            if (_prototypeAssembly == assembly) return;

            if (_prototypeConnection != null)
            {
                _prototypeConnection.Close();
                _prototypeConnection.Dispose();
                _prototypeSessionFactory.Dispose();
            }

            _prototypeAssembly = assembly;

            _prototypeConnection = new SQLiteConnection(PrototypeConnectionString);
            _prototypeConnection.Open();

            _prototypeSessionFactory = Fluently
                .Configure()
                .Database(SQLiteConfiguration.Standard.ConnectionString(PrototypeConnectionString))
                .Mappings(m => m.HbmMappings.AddFromAssembly(assembly))
                .ExposeConfiguration(cfg => new SchemaExport(cfg).Execute(false, true, false, _prototypeConnection, null))
                .BuildSessionFactory();
        }

        private void InitDatabaseInstance()
        {
            _instanceSessionFactory = Fluently
                .Configure()
                .Database(SQLiteConfiguration.Standard.ConnectionString(InstanceConnectionString))
                .Mappings(m => m.HbmMappings.AddFromAssembly(_prototypeAssembly))
                .BuildSessionFactory();

            _instanceConnection = new SQLiteConnection(InstanceConnectionString);
            _instanceConnection.Open();

            _prototypeConnection.BackupDatabase(_instanceConnection, "main", "main", -1, null, -1);
        }

        public ISession OpenSession()
        {
            return _instanceSessionFactory.OpenSession(_instanceConnection);
        }

        public void Dispose()
        {
            _instanceConnection.Close();
            _instanceConnection.Dispose();
            _instanceSessionFactory.Dispose();
        }
    }
}
like image 111
realbart Avatar answered Sep 28 '22 21:09

realbart


What I have observed with SQLite in memory databases is that as soon as you close the connection, everything in the db is gone. So to do what you want,

  1. Create session factory for the backup database, open session and build schema don't close this session until you finish your entire test suite

  2. Create session factory for your target database, open session and use the connection from this session object and the connection from session created from step 1 to copy data

  3. Use the session created on step 2 for test and close it once test is finished

Another solution is to use the single session to perform multiple tests (all the tests in single test fixture) then you do not need to create new session factory per test, but per testfixture

like image 22
Low Flying Pelican Avatar answered Sep 28 '22 23:09

Low Flying Pelican