Многие, использующие Entity Framework сталкиваются с проблемой изоляции внешних зависимостей (в данном случае БД) при написании юнит-тестов. В этом примере я покажу, как подменить DbContext прозрачно для вызывающего кода, и заполнить поддельный DbSet тестовыми данными. Такой прием полезен для тестирования кода доступа к данным в изоляции от БД. Актуально для Entity Framework 6.
Мы будем тестировать популярный паттерн: некий класс-"репозиторий", инкапсулирующий в себе запрос к DbSet, транслируемый в запрос к базе данных:
Переходим непосредственно к подмене контекста. Для этого нам понадобится еще один модуль Ninject, который достаточно создать в проекте с изолированными от БД тестами:
Данный способ - не 100% пацея и побочные эффекты могут возникнуть, поскольку в LINQ-запросе к DbSet будет использоваться LINQ to Objects, а не LINQ to Entities.
Полный пример для EF6 + SQL Server доступен на GitHub.
Мы будем тестировать популярный паттерн: некий класс-"репозиторий", инкапсулирующий в себе запрос к DbSet, транслируемый в запрос к базе данных:
public class ReportRepository { [Inject] public IContextFactory ContextFactory { get; set; } public List<Report> GetReports() { using (var context = new FooContext()) { return context.Reports.ToList(); } } }Этот код не назовешь слабосвязанным, так как присутствует зависимость от конкретной реализации контекста - класса FooContext. Нам необходимо в первую очередь избавиться от этой зависимости. Для этого модифицируем наш контекст, выделив необходимые методы и свойства в интерфейс (например, путем модификации шаблонов T4, генерирующих контекст):
public partial class FooContext : DbContext, IFooContext { public IDbSet<Report> Reports { get; set; } }
Однако, класс-репозиторий сам управляет временем жизни контекста, поэтому прямое внедрение интерфейса не подходит. Вместо этого, используем паттерн "абстрактная фабрика" (все подробности - в известной книге "Dependency Injection on .NET", сейчас практически стандарту по DI в дотнете). Итак, наша фабрика будет создавать новые экземпляры контекста:
public class ContextFactory : IContextFactory { public IFooContext Create() { return new FooContext(); } }
Внедрим фабрику в "репозиторий":
public class ReportRepository { [Inject] public IContextFactory ContextFactory { get; set; } public List<Report> GetReports() { using (var context = ContextFactory.Create()) { return context.Reports.ToList(); } } }
Теперь использующий контекст не знает ничего о конкретной реализации контекста. Осталось только сконфигурировать наш DI контейнер (здесь и далее используется Ninject):
public class AppModule : NinjectModule { public override void Load() { Bind<IContextFactory>().To<ContextFactory>(); } }
Самое время выполнить интеграционный тест, запрашивающий данные из реальной БД:
[Test] public void RealRepository_WhenCalled_ReturnsEmpty() { var kernel = new StandardKernel(); kernel.Load<AppModule>(); var repo = kernel.Get<ReportRepository>(); var reports = repo.GetReports(); Assert.IsEmpty(reports); }
Переходим непосредственно к подмене контекста. Для этого нам понадобится еще один модуль Ninject, который достаточно создать в проекте с изолированными от БД тестами:
public class MockModule : NinjectModule { public override void Load() { Rebind<IContextFactory>().To<MockContextFactory>(); } }Для создания поддельных объектов используется фреймворк Moq. Обратите внимание на реализацию MockContextFactory: вся хитрость - в методе MockDbSet<T>, который создает мок-объект, реализующий IQueryable<T>:
Этого достаточно! Мы можем обращаться к поддельному DbSet, получать тестовые данные через LINQ, при этом подмена контекста останется незамеченной для использующего контекст кода. Для проверки напишем еще один тест:public class MockContextFactory : IContextFactory { public IFooContext Create() { var mockRepository = new MockRepository(MockBehavior.Default); var mockContext = mockRepository.Create<IFooContext>(); mockContext.Setup(x => x.SaveChanges()) .Returns(int.MaxValue); List<Report> mockReports = MockReports(); var mockDbSet = MockDbSet<Report>(mockReports); mockContext.Setup(m => m.Reports).Returns(mockDbSet.Object); return mockContext.Object; } private List<Report> MockReports() { List<Report> mockReports = new List<Report>(); mockReports.Add(new Report {Id = 1, Name = "Mock Report #1"}); return mockReports; } private Mock<DbSet<T>> MockDbSet<T>(List<T> data = null) where T : class { if (data == null) data = new List<T>(); var queryable = data.AsQueryable(); var mock = new Mock<DbSet<T>>(); mock.As<IQueryable<T>>().Setup(m => m.Provider) .Returns(queryable.Provider); mock.As<IQueryable<T>>().Setup(m => m.Expression) .Returns(queryable.Expression); mock.As<IQueryable<T>>().Setup(m => m.ElementType) .Returns(queryable.ElementType); mock.As<IQueryable<T>>().Setup(m => m.GetEnumerator()) .Returns(queryable.GetEnumerator()); return mock; } }
[Test] public void RealRepository_WhenCalled_ReturnsMockData() { var kernel = new StandardKernel(); kernel.Load<AppModule>(); //Order is important. In MockModule we rebind IContextFactory. //Another option is to load MockModule only. kernel.Load<MockModule>(); var repo = kernel.Get<ReportRepository>(); var reports = repo.GetReports(); Assert.IsNotEmpty(reports); }
Полный пример для EF6 + SQL Server доступен на GitHub.
Комментариев нет:
Отправить комментарий