Unit Testing – Layer of Indirection

Introduction

One unit test pattern The Art of Unit Testing, 2nd edition discusses is the layer of indirection pattern (this is my name for the pattern since the book does not give it a name). Its purpose like many other patterns in the book is to make your code less dependent on objects you have little or no control over. I find that adding a layer of indirection to an object is effective an effective technique when the dependency is difficult to fake.

Repository Example

Imagine you had to test the repository class shown in Listing 1. The repository is dependent on the Context class and the IEntity interface. The Context class is derived from DbContext and is shown in Listing 2. The IEntity interface and the Customer class that derives from it are shown in Listing 3.

public class Repository<TEntity, TId> where TEntity : class, IEntity<TId> where TId : IComparable<TId>
    {
        private readonly Context context;

        public Repository(Context context)
        {
            this.context = context;
        }

        public TEntity GetSingle(TId id)
        {
            var comparable = id as IComparable<TId>;
            return this.context.Set<TEntity>().AsEnumerable().Single(e => comparable.CompareTo(e.Id) == 0);
        }

        public IEnumerable<TEntity> GetMany(Func<TEntity, bool> criteria)
        {
            return this.context.Set<TEntity>().AsEnumerable().Where(criteria);
        }
    }

Listing 1


public class Context : DbContext
    {
        public Context() : base("context")
        {
            
        }

        public DbSet<Customer> CustomerSet { get; set; }
    }

Listing 2

public interface IEntity<TId> where TId : IComparable<TId>
    {
        TId Id { get; set; }
    }

public class Customer : IEntity<int>
    {
        public int Id { get; set; }

        public string FirstName { get; set; }

        public string LastName { get; set; }
    }

Listing 3

Obviously the repository class is not unit-testable as it has a dependency on the Entity Framework. I find that the strategy most start out with is to figure out a way to inject a fake DbContext into the repository. An example of this approach can be found here. This strategy is overly complex and it would be easier to write integration tests against a real database as shown in Listing 4. Eventually I would tire of this approach as I had to support more entities and repositories. Most importantly, the extra code doesn’t help me vet the design. In other words, it is easy to lose the forest for the trees when trying to fake DbContext .

[TestClass]
    public class CustomerRepositoryIntegrationTests
    {
        [AssemblyInitialize]
        public static void AssemblyInitialize(TestContext testContext)
        {
            Database.SetInitializer(new DropCreateDatabaseAlways<Context>());
            new Context().Database.Initialize(true);
        }

        [TestMethod]
        public void GetSingle_ReturnsTheCustomer()
        {
            var context = new Context();
            var repo = new Repository<Customer, int>(context);
            
            using (new TransactionScope(TransactionScopeOption.Required))
            {
                var customer = new Customer();
                context.CustomerSet.Add(customer);
                context.SaveChanges();

                var customerId = customer.Id;
                Assert.AreEqual(customerId, repo.GetSingle(customerId).Id);
            }
        }

        [TestMethod]
        public void GetMany_ReturnsTheCustomers()
        {
            var context = new Context();
            var repo = new Repository<Customer, int>(context);

            using (new TransactionScope(TransactionScopeOption.Required))
            {
                var customers = new[] { new Customer(), new Customer() };
                context.CustomerSet.AddRange(customers);
                context.SaveChanges();

                Assert.AreEqual(2, repo.GetMany(c => string.IsNullOrWhiteSpace(c.FirstName)).Count());
            }
        }
    }

Listing 4

By adding a layer of indirection, you can break the dependency between the repository and Entity Framework without resorting to an integration test only approach, in-memory databases or obscure tools. The only tool you need is a free mocking framework like NSubstitute.

Layer of Indirection Example

The IRepositoryDataSource interface shown in Listing 5 is the layer of indirection. Listing 6 shows the modified Repository class that makes use of the new interface. Since the Entity Framework dependency is gone, the class is unit-testable. The simplified unit tests are shown in Listing 7. The unit tests use NSubstitute to do the faking.

public interface IRepositoryDataSource<TEntity, TId> where TEntity : class, IEntity<TId> where TId : IComparable<TId>
    {
        IEnumerable<TEntity> Entities { get; }  
    }

Listing 5

public class Repository<TEntity, TId> where TEntity : class, IEntity<TId> where TId : IComparable<TId>
    {
        private readonly IRepositoryDataSource<TEntity, TId> dataSource;

        public Repository(IRepositoryDataSource<TEntity, TId> dataSource)
        {
            this.dataSource = dataSource;
        }

        public TEntity GetSingle(TId id)
        {
            var comparable = id as IComparable<TId>;
            return this.dataSource.Entities.Single(e => comparable.CompareTo(e.Id) == 0);
        }

        public IEnumerable<TEntity> GetMany(Func<TEntity, bool> criteria)
        {
            return this.dataSource.Entities.Where(criteria);
        }
    }

Listing 6

[TestClass]
    public class CustomerRepositoryUnitTests
    {
        [TestMethod]
        public void GetSingle_ReturnsTheCustomer()
        {
            var customer = new Customer { Id = 1 };
            
            var fakeDataSource = Substitute.For<IRepositoryDataSource<Customer, int>>();
            fakeDataSource.Entities.Returns(new[] { customer });

            var repository = new Repository<Customer, int>(fakeDataSource);
            repository.GetSingle(customer.Id);
        }

        [TestMethod]
        public void GetMany_ReturnsTheCustomers()
        {
            var customers = new[] { new Customer(), new Customer() };

            var fakeDataSource = Substitute.For<IRepositoryDataSource<Customer, int>>();
            fakeDataSource.Entities.Returns(customers);

            var repository = new Repository<Customer, int>(fakeDataSource);
            Assert.AreEqual(2, repository.GetMany(c => string.IsNullOrWhiteSpace(c.FirstName)).Count());
        }
    }

Listing 7

Now that the unit tests have vetted the design, a “real” repository data source can be written. This class is shown in Listing 8. Listing 9 shows the amended integration tests that use the RepositoryDataSource class.

public class RepositoryDataSource<TEntity, TId> : IRepositoryDataSource<TEntity, TId> where TEntity : class, IEntity<TId> where TId : IComparable<TId>
    {
        private readonly DbContext context;

        public RepositoryDataSource(DbContext context)
        {
            this.context = context;
        }

        public IEnumerable<TEntity> Entities
        {
            get
            {
                return this.context.Set<TEntity>().AsEnumerable();
            }
        }
    }

Listing 8

[AssemblyInitialize]
        public static void AssemblyInitialize(TestContext testContext)
        {
            Database.SetInitializer(new DropCreateDatabaseAlways<Context>());
            new Context().Database.Initialize(true);
        }

        [TestMethod]
        public void GetSingle_ReturnsTheCustomer()
        {
            var context = new Context();
            var dataSource = new RepositoryDataSource<Customer, int>(context);
            var repo = new Repository<Customer, int>(dataSource);

            using (new TransactionScope(TransactionScopeOption.Required))
            {
                var customer = new Customer();
                context.CustomerSet.Add(customer);
                context.SaveChanges();

                var customerId = customer.Id;
                Assert.AreEqual(customerId, repo.GetSingle(customerId).Id);
            }
        }

        [TestMethod]
        public void GetMany_ReturnsTheCustomers()
        {
            var context = new Context();
            var dataSource = new RepositoryDataSource<Customer, int>(context);
            var repo = new Repository<Customer, int>(dataSource);

            using (new TransactionScope(TransactionScopeOption.Required))
            {
                var customers = new[] { new Customer(), new Customer() };
                context.CustomerSet.AddRange(customers);
                context.SaveChanges();

                Assert.AreEqual(2, repo.GetMany(c => string.IsNullOrWhiteSpace(c.FirstName)).Count());
            }
        }

Listing 9

The Need for Integration Testing

Integration tests are still necessary because you still need to see how the system works with real dependencies at some point. Also having separate unit and integration tests create a “safe green zone” as The Art of Unit Testing puts it. Basically this means you can run unit tests many times a day and they should always pass (green). Integration tests are probably not run as often because they usually take longer and they may need a deployable build to work.

Conclusion

When a difficult to fake object is making unit testing difficult, consider adding a layer of indirection. This allows you to create an interface around the difficult to fake object that you control. This control allows you to do away with the dependency. Again, this pattern is discussed in detail in the The Art of Unit Testing, 2nd edition. I recommend reading the book from cover to cover.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s