Automated testing with Entity Framework takes some Effort

Our goal at VI Company is to write quality software. We have several processes in place to achieve and monitor this quality; continuous integration, code reviews and automated tests to name a few. In this article I want to take a closer look at automated testing, particularly in an area that is inherently difficult to test.

Edges of abstraction

A typical code base consists of several components or units that work closely together. Each unit has its own responsibility. With the right amount of abstraction between these units (e.g. interfaces, dependency injection) we can isolate each part and put it to the test. We test each unit in a controlled environment where we dictate the input and can assert the output, thereby verifying that the unit works correctly.

It's not feasible to have each unit wrapped in abstraction in order to test it. Layer upon layer of abstraction is like empty space; it's invisible, untouchable. But in the end, we want our application to have a concrete, observable effect. We want to send an HTTP response across the wire, or write a change to the database. These side-effects happen at the application boundaries. It's at these boundaries where all of our hard work becomes visible to the outside world. It is where the abstraction disappears. But without abstraction, unit testing becomes difficult.

Database communication

The database is an important subsystem of the application. Although technically part of the application as a whole, it is an application boundary. It's where transient data becomes a permanent record. It's where a user's comments are put on record, where historical trading quotes are kept for years on end. It's also one of the boundaries where unit testing is a challenge.

The communication with the database has been abstracted many times, by many people. We use Entity Framework at VI Company as an abstraction. We don't write SQL statements by hand, or iterate over data rows any more. Instead, we communicate with an abstract representation of our database, provided by Entity Framework (EF). It saves us a lot of boilerplate code and time.

But EF is still a difficult dependency to work with in unit tests. It requires an actual database in order to work. We don't want to use our main development database for this, because we want to be in complete control of the data. But we also don't want to maintain a separate database for testing purposes. Ultimately, a unit test is about testing a unit in isolation, and having a cross-boundary database dependency completely defeats this purpose. Moving EF behind another abstraction layer isn't an option either. This would only introduce more complexity to the application, without actually solving the issue.

So what can we do to get around this database dependency?

Putting in some Effort

When testing with Entity Framework, ideally we want to have a temporary in-memory database. A database that is created on the fly when running a test, populating it only with the data required for that test. That's exactly what Effort gives us. This open-source library provides an in-memory backing store for EF, specifically with automated testing in mind.

Now, it has to be said that we're not completely replacing the dependency on Entity Framework in our tests. We are merely replacing the biggest dependency of EF itself, the database. So when using Effort, we're still communicating through EF and all of its logic. We're not testing our unit in complete isolation; EF is still included in the mix. So while it technically has become integration testing rather than unit testing at this point, it has become a lot more manageable.

Preparation

The use of Effort is quite straightforward, but we have to prepare the generated EF context class first. By default, Entity Framework generates a parameterless constructor, which passes the connection string name to its base constructor:

public partial class Entities : DbContext
{
    public Entities()
        : base("name=Entities")
    {
    }

    // Snip
}

We're going to add a new constructor which accepts a DbConnection. We'll use this constructor in our tests. Since EF generates the context as a partial class, we can conveniently declare a new constructor in a separate file:

public partial class Entities
{
    // This constructor is used by Effort in the tests.
    public Entities(DbConnection connection)
        : base(connection, true)
    {
    }
}

In our test we call this new constructor and pass in a connection object provided by Effort. In this case we ask Effort to create a transient connection. This creates an in-memory database which is deleted as soon as the connection object is disposed. Effort also provides a 'persistent' option, which creates an in-memory database that lives during the entire application life cycle.

[Test]
public void AutomatedTestWithEffort()
{
    // Arrange
    var connection = EntityConnectionFactory.CreateTransient("name=Entities");
    var entities = new Entities(connection);

    // Act
    // Assert
}

We pass in the name of the connection string, just like the default constructor of EF does. Effort uses this connection string to generate a database based on the metadata available in the .edmx file of Entity Framework.

Using the in-memory database

Now we have a fully-functional EF context backed by a temporary database. The next step in our test would be to add the required DTO's to the context and call SaveChanges() to generate primary keys. That's all there is to it. Now we're ready to test our unit:

[Test]
public void AutomatedTestWithEffort()
{
    // Arrange
    var connection = EntityConnectionFactory.CreateTransient("name=Entities");
    var entities = new Entities(connection);

    entities.Users.Add(new User { EmailAddress = "john@example.com" });
    entities.Users.Add(new User { EmailAddress = "harry@example.com" });
    entities.Users.Add(new User { EmailAddress = "george@example.com" });
    entities.SaveChanges();
    
    var sut = new UserRepository(entities);
    
    // Act
    var user = sut.FindByEmailAddress("george@example.com");
    
    // Assert
    Assert.AreEqual("george@example.com", user.EmailAddress);
}

This is of course a very basic example which demonstrates a querying scenario. But if we're testing a scenario where we write to the database, we can also assert whether the EF context contains the new data at then end of the test.

Limitations

Although Effort provides us with a lot of functionality, it does have a few limitations. For example, it doesn't handle stored procedure calls. To get around this, you can wrap your Entity Framework context in a Humble Object and mock this in your tests. It also doesn't handle views and triggers. But since we try to limit the amount of logic in our database to just data integrity checks, we rarely have to use these anyway.

Conclusion

Automated tests are a very powerful tool in assuring the quality and correctness of a code base. However, in order to accurately test, we need some level of abstraction. But the application boundaries are anything but abstract and therefore make testing more difficult. Effort provides us with a way to more easily test code that integrates with Entity Framework. Instead of not covering code that uses the database because 'it is hard', we now have a way to test another layer of the application with little effort.