Entity Framework part 3: Testing the DbContext
In the previous episode we saw how to map our complex models in a database using Entity Framework and we were left with the question of how to test our mappings and our model context.
We will quickly go through those scenarios now.
Integration Testing
Integration tests are a particular type of tests which are meant to investigate the behaviour of your objects when they pass through the domain boundaries, generally exercising the network, the database, the file system, etc. In this case we are talking about the database and you simply want to make sure that your repositories are saving and retrieving the object hierarchies exactly as you defined them in code.
First of all, we need a couple of tweaks to our DbContext
class to allow to choose a different connection string (we don’t want to run our tests on the production DB!).
public class ToDoDbContext : DbContext { public ToDoDbContext() : this("ToDoDbContext") { } protected ToDoDbContext(string connectionString) : base(connectionString) { } //... }
As you remember, the connection string was defined in our Web.Config
file:
<connectionStrings> <add name="ToDoDbContext" providerName="System.Data.SqlClient" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=MyToDoDB;Integrated Security=SSPI" /> </connectionStrings>
This allows us to change the connection at runtime simply calling the relevant constructor.
Now we can go on and create a test project. Add all the relevant references to the main project and to Entity Framework and we are ready to go. First of all create an App.config
file where you can store your test connection string. This is not strictly required but it’s useful if you are running the tests on your machine and on your CI machine: the latter may need to change the connection string before running:
<connectionStrings> <add name="ToDoTestConnection" providerName="System.Data.SqlClient" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=TestToDoDB;Integrated Security=SSPI" /> </connectionStrings>
Then you need to create a test DbContext
to use for your tests; this is all you need:
public class TestToDoDbContext : ToDoDbContext { public TestToDoDbContext () : base(ConfigurationManager.ConnectionStrings["ToDoTestConnection"].ConnectionString) { } }
The problem you are facing now is how to constantly recreate and drop your database; what is more, you need to do it based on your migrations. The solution is easier than you may think and it entails hooking into Entity Framework configuration in code. If you remember the Configuration.cs
file that gets created when you enable migrations for the first time, we are doing a very similar thing here. So let’s add a TestConfiguration
class:
internal class TestConfiguration : DbMigrationsConfiguration<TestToDoDbContext> // this is a config for the Test Context! { internal TestConfiguration() { AutomaticMigrationsEnabled = false; // Here we specify where the migrations are: MigrationsAssembly = Assembly.GetAssembly(typeof(ToDoDbContext)); MigrationsNamespace = "ToDoApplication.Models.Migrations"; } protected override void Seed(TestToDoDbContext context) { // Here you can use the context to Seed your Test Database } }
As you can see we use the TestToDoDbContext
as the backing context but everything else is the same, with the addition of the lines that specify the Assembly ad the Namespace of our migrations (now you see why we kept them in a single separated folder in our app).
Let’s proceed to the testing phase. Here you need setup and teardown phases that take care of creating and destroying the database between tests. You can also do the destroy-create at fixture level if your tests are not going to step on each other toes:
[TestFixture] public class DatabaseContextTests { // I will use these to save and load from the DB: private TestToDoDbContext saveContext; private TestToDoDbContext loadContext; [TestFixtureSetUp] public void create_and_migrate_database() { var context = new TestToDoDbContext(); // let's delete in case it is still up after some failed tests context.Database.Delete(); // grab the config... var config = new TestConfiguration(); // ...pass it to the migrator var migrator = new DbMigrator(config); // and run the update! migrator.Update(); // This has the same effect as running // Update-Database from the PM Console. } [TestFixtureTearDown] public void fixture_teardown() { var context = new TestToDoDbContext(); context.Database.Delete(); } // You can test constraints: [Test] public void save_patient_without_username_throws_error() { var user = new User() { //UserName = "testuser", <- this is required! }; saveContext.Users.Add(user); try { saveContext.SaveChanges(); Assert.Fail(); } catch (Exception exception) { Assert.That(exception, Is.InstanceOf<DbEntityValidationException>()); var errors = ((DbEntityValidationException)exception).EntityValidationErrors; Assert.That(errors.Count(), Is.EqualTo(1)); } } // And you can test relationships: [Test] public void save_and_delete_patient_with_required_attributes() { var user = new User() { UserName = "Test" }; var task1 = new Task() {Description = "Test1"}; var task2 = new Task() {Description = "Test2"}; user.Tasks.Add(task1); user.Tasks.Add(task2); saveContext.Users.Add(user); saveContext.SaveChanges(); var getUser = loadContext.Users.First(); Assert.That(getUser, Is.Not.Null); Assert.That(getUser.Tasks.Count, Is.EqualTo(2)); var tasks = loadContext.Tasks; Assert.IsTrue(tasks.Any(t => t.User == user)); // if you don't have a User property on the Task you can do: var task_userids = loadContext.Database.SqlQuery<int>("SELECT User_Id FROM dbo.Tasks"); Assert.That(task_userids, Is.All.EqualTo(user.Id)); } }
And that is everything you need for testing your database code. I use this technique mainly to validate that my Model is saving correctly into the database and that all the relationships are created properly. It is also useful to validate the schema creation from scratch. Remember this is, strictly speaking, Integration Testing and not Unit Testing: we are definitely going over the “unit” boundary here; this usually means that the tests can take longer to run (and they do if you drop and recreate the database for every test), so you may want to set the automated runner to run occasionally and not at every commit.
You may have noticed that in the tests I have defined a saveContext
and a loadContext
. I usually do this to make sure that when loading I am actually loading entities in a separate context, to avoid false results due to EF caching and optimization. Sometimes I run the tests on my machine while keeping an eye on a SQL profiler, to validate the queries actually being run against the database.
This is all for now. Happy coding!
Using Vagrant with Chocolatey and Puppet to spin up virtual machines Vagrant with Windows Support!