Integration tests, NHibernate and Sqlite, part 2 – in file database
Integration tests, NHibernate and Sqlite, part 3 – enhanced in memory database
In previous post I introduced a database scope that builds the database schema only once per test suit run. Some performance was sacrificed to achieve this: the database had to be stored in a file. Database stored in memory is obviously faster than database stored in a file. Is there a way how to move the database back to memory?
Yes, we can build a master database in-memory and then make copy of this master database for each test. The problem is how to duplicate the master database. Sqlite provides Online Backup API, which is not available in System.Data.SQLite wrapper yet.
Sqlite Backup API
Fortunately it is quite easy to wrap the required API:
[DllImport("System.Data.SQLite.DLL", CallingConvention = CallingConvention.Cdecl)] internal static extern IntPtr sqlite3_backup_init(IntPtr destDb, byte[] destname, IntPtr srcDB, byte[] srcname); [DllImport("System.Data.SQLite.DLL", CallingConvention = CallingConvention.Cdecl)] internal static extern int sqlite3_backup_step(IntPtr backup, int pages); [DllImport("System.Data.SQLite.DLL", CallingConvention = CallingConvention.Cdecl)] internal static extern int sqlite3_backup_finish(IntPtr backup); public static void Backup(SQLiteConnection source, SQLiteConnection destination) { IntPtr sourceHandle = GetConnectionHandle(source); IntPtr destinationHandle = GetConnectionHandle(destination); IntPtr backupHandle = sqlite3_backup_init(destinationHandle, SQLiteConvert.ToUTF8("main"), sourceHandle, SQLiteConvert.ToUTF8("main")); sqlite3_backup_step(backupHandle, -1); sqlite3_backup_finish(backupHandle); }
There is only one catch. Handles needed by Sqlite API are stored as private fields in System.Data.SQLite wrapper. But it can be retrieved by this little trick:
private static IntPtr GetConnectionHandle(SQLiteConnection source) { object sqlLite3 = GetPrivateFieldValue(source, "_sql"); object connectionHandle = GetPrivateFieldValue(sqlLite3, "_sql"); IntPtr handle = (IntPtr)GetPrivateFieldValue(connectionHandle, "handle"); return handle; } private static object GetPrivateFieldValue(object instance, string fieldName) { var filedType = instance.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); object result = filedType.GetValue(instance); return result; }
Yes, it is a hacky solution but it works well and it can be viewed as temporary replacement for the missing wrapper part.
SqliteInMemoryPrivateScope
Modification of SqliteInFilePrivateScope is easy with such helper method.
The master database is duplicated by the new wrapper method:
public SqliteInMemoryPrivateScope() { EnsureMasterDatabaseExistence(); this.privateConnection = new SQLiteConnection(connectionString); this.privateConnection.Open(); SqliteBackup.Backup(masterConnection, this.privateConnection); }
And a connection to the master database must be held by the scope instance. It is similar to SqliteInMemorySharedScope:
private static SQLiteConnection masterConnection; private void EnsureMasterDatabaseExistence() { // to support asynchronous scenario lock (configurationSync) { if (configuration == null) { configuration = BuildConfiguration (); sessionFactory = configuration.BuildSessionFactory(); masterConnection = new SQLiteConnection(connectionString); masterConnection.Open(); SchemaExport schemaExport = new SchemaExport(configuration); schemaExport.Execute(false, true, false, masterConnection, TextWriter.Null); } } }
Now we have a database scope that builds configuration, session factory and database schema only once per test suit run which saves a lot of time. In addition the database is stored in memory so integration tests run blazingly fast. The isolation between tests is very good and the test can be executed in parallel. What to want more?:).
Full implementation of SqliteInMemoryPrivateScope can be found here.
Complete source code can be downloaded here.