Это скорее решение/обходной путь, чем актуальный вопрос. Я размещаю его здесь, так как не смог найти это решение при переполнении стека или действительно после большого количества поисков в Google.
Проблема:
У меня есть веб-приложение MVC 3, сначала использующее код EF 4, для которого я хочу написать модульные тесты. Я также использую NCrunch для запуска модульных тестов «на лету» по мере написания кода, поэтому я хотел бы избежать здесь использования реальной базы данных.
Другие решения:
IDataContext
Я считаю, что это наиболее приемлемый способ создания контекста данных в памяти. Это эффективно включает в себя написание интерфейса IMyDataContext для вашего MyDataContext, а затем использование интерфейса во всех ваших контроллерах. Примером этого является здесь.
Это путь, по которому я пошел изначально, и я даже дошел до написания шаблона T4 для извлечения IMyDataContext из MyDataContext, поскольку мне не нравится поддерживать дублирующийся зависимый код.
Однако я быстро обнаружил, что некоторые операторы Linq терпят неудачу при использовании IMyDataContext вместо MyDataContext. В частности, подобные запросы вызывают исключение NotSupportedException
var siteList = from iSite in MyDataContext.Sites
let iMaxPageImpression = (from iPage in MyDataContext.Pages where iSite.SiteId == iPage.SiteId select iPage.AvgMonthlyImpressions).Max()
select new { Site = iSite, MaxImpressions = iMaxPageImpression };
Мое решение
На самом деле это было довольно просто. Я просто создал подкласс MyInMemoryDataContext для MyDataContext и переопределил все IDbSet<..> свойства, как показано ниже:
public class InMemoryDataContext : MyDataContext, IObjectContextAdapter
{
/// Whether SaveChanges() was called on the DataContext
public bool SaveChangesWasCalled { get; private set; }
public InMemoryDataContext()
{
InitializeDataContextProperties();
SaveChangesWasCalled = false;
}
///
/// Initialize all MyDataContext properties with appropriate container types
///
private void InitializeDataContextProperties()
{
Type myType = GetType().BaseType; // We have to do this since private Property.Set methods are not accessible through GetType()
// ** Initialize all IDbSet properties with CollectionDbSet instances
var DbSets = myType.GetProperties().Where(x => x.PropertyType.IsGenericType && x.PropertyType.GetGenericTypeDefinition() == typeof(IDbSet<>)).ToList();
foreach (var iDbSetProperty in DbSets)
{
var concreteCollectionType = typeof(CollectionDbSet<>).MakeGenericType(iDbSetProperty.PropertyType.GetGenericArguments());
var collectionInstance = Activator.CreateInstance(concreteCollectionType);
iDbSetProperty.SetValue(this, collectionInstance,null);
}
}
ObjectContext IObjectContextAdapter.ObjectContext
{
get { return null; }
}
public override int SaveChanges()
{
SaveChangesWasCalled = true;
return -1;
}
}
В этом случае мой CollectionDbSet является слегка измененной версией FakeDbSet здесь(который просто реализует IDbSet с базовым ObservableCollection и ObservableCollection.AsQueryable()).
Это решение прекрасно работает со всеми моими модульными тестами и, в частности, с NCrunch, запускающим эти тесты на лету.
Полные интеграционные тесты
Эти модульные тесты проверяют всю бизнес-логику, но есть один существенный недостаток: ни один из ваших операторов LINQ не гарантирует работу с вашим реальным MyDataContext. Это связано с тем, что тестирование в контексте данных в памяти означает, что вы заменяете поставщика Linq-To-Entity, но поставщика Linq-To-Objects (как очень хорошо указано в ответе на этот вопрос SO) .
Чтобы исправить это, я использую Ninject в своих модульных тестах и настраиваю InMemoryDataContext для привязки вместо MyDataContext в своих модульных тестах. Затем вы можете использовать Ninject для привязки к фактическому MyDataContext при запуске интеграционных тестов (через настройку в app.config).
if(Global.RunIntegrationTest)
DependencyInjector.Bind().To().InSingletonScope();
else
DependencyInjector.Bind().To().InSingletonScope();
Дайте мне знать, если у вас есть какие-либо отзывы об этом, однако всегда есть улучшения, которые нужно внести.