When writing unit tests, it’s common to stop at the database access layer.
You might have a “dumb” data access layer that just passes stored procedure names and parameters to the SQL database, for instance.
It’s usually very hard to test this layer, since it requires either that your build server has access to a SQL Server instance, or that a SQL Server is running on the build server.
Plus, we’re getting out of unit tests and are entering integration tests, here.
Using a more advanced tool like Entity Framework, in order to test your complex EF queries, there are usually methods to insert fake data into a fake container, like Test Doubles, InMemory, or Effort.
Using MongoDB, you might encounter the same problem : how do I test that my complex queries are working ?
Here for instance, I get the possible colors of a product; how do I know it works using unit tests?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public IEnumerable<Color> GetColors(string productId) { var coll = db.GetCollection<BsonDocument>("products"); var aggregate = coll.Aggregate(new AggregateOptions { AllowDiskUse = true }) .Match(new BsonDocument { { "ProductId", productId }, { "Color", new BsonDocument("$ne", "") } }) .Unwind(new StringFieldDefinition<BsonDocument>("Color")) .Group(new BsonDocument { { "_id", "$Color" }, { "Count", new BsonDocument("$sum", 1) } }) .Sort(Builders<BsonDocument>.Sort.Descending("Count")) .Limit(100); return aggregate .ToList() .Select(doc => new Color { ColorValue = doc["_id"].AsString, Count = doc["Count"].AsInt32 }); } |
MongoDB has an “inMemory” storage engine, but it’s reserved to the (paid) Enterprise edition. Fortunately, since 3.2, even the Community edition has a not-very-well-documented “ephemeralForTests” storage engine, which loads up an in-memory Mongo instance, and does not store anything on the hard drive (but has poor performances). Exactly what we need!
Before running the data access layer tests, we will need to fire up an in-memory instance of MongoDB.
This instance will be common to all the tests for the layer, otherwise the test runner will fire up new tests faster than the system releases resources (file and ports locks).
You will have to extract the MongoDB binaries in your sources repository somewhere, and copy them besides your binaries on build.
The following wrapper provides a “Query” method that allows us to access the Mongo instance through command-line, bypassing the data access layer, in order to insert test data or query insertion results.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
public class MongoDaemon : IDisposable { public const string ConnexionString = "mongodb://localhost:27017"; public const string DbName = "test"; public const string Host = "localhost"; public const string Port = "27017"; private readonly string assemblyFolder; private readonly string dbFolder; private readonly string mongoFolder; private Process process; public MongoDaemon() { this.assemblyFolder = Path.GetDirectoryName(new Uri(typeof(MongoDaemon).Assembly.CodeBase).LocalPath); this.mongoFolder = Path.Combine(this.assemblyFolder, "mongo"); this.dbFolder = Path.Combine(this.mongoFolder, "temp"); // re-create db folder if it exists if (Directory.Exists(this.dbFolder)) { Directory.Delete(this.dbFolder, true); Directory.CreateDirectory(this.dbFolder); } this.process = new Process(); process.StartInfo.FileName = Path.Combine(this.mongoFolder, "mongod.exe"); process.StartInfo.Arguments = "--dbpath " + this.dbFolder + " --storageEngine ephemeralForTest"; process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; process.Start(); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public string Query(string query) { var output = string.Empty; var procQuery = new Process { StartInfo = new ProcessStartInfo { FileName = Path.Combine(this.mongoFolder, "mongo.exe"), Arguments = string.Format("--host {0} --port {1} --quiet --eval \"{2}\"", Host, Port, query), UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true }, }; procQuery.Start(); // read query output while (!procQuery.StandardOutput.EndOfStream) { output += procQuery.StandardOutput.ReadLine() + "\n"; } // wait 2 seconds max before killing it if (!procQuery.WaitForExit(2000)) { procQuery.Kill(); } return output; } protected virtual void Dispose(bool disposing) { if (disposing) { // dispose managed resources } if (process != null && !process.HasExited) { process.Kill(); } } } |
We’re using the
IClassFixture interface of xUnit to fire up a MongoDaemon instance that will be common to all our tests using it.
It means we need to clean up previously inserted test data at each run.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public class MongoDataStoreTests : IClassFixture<MongoDaemon> { private readonly MongoDaemon daemon; private readonly DataStore sut; public MongoDataStoreTests(MongoDaemon daemon) { this.daemon = daemon; this.sut = new DataStore("localhost", 27017); // cleanup previous tests data this.daemon.Query("db.products.drop()"); } [Fact] public void Colors_are_counted() { // arrange : insert test data var id = "test"; var nb = 5; for (int i = 0; i < nb; i++) { var color = string.Format("{0}{0}{0}", i, i, i); var data = "{ ProductId: '" + id + "', Color: '" + color + "' }"; this.daemon.Query("db.products.insertOne(" + data + ")"); } // act var result = this.sut.GetColors(id); // assert result.Should().HaveCount(nb); } } |
There you have it: a kind-of-easy way to test your Mongo data access layer.