Модульное тестирование с использованием EF Code First DataContext

Это скорее решение/обходной путь, чем актуальный вопрос. Я размещаю его здесь, так как не смог найти это решение при переполнении стека или действительно после большого количества поисков в 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();

Дайте мне знать, если у вас есть какие-либо отзывы об этом, однако всегда есть улучшения, которые нужно внести.

5
задан Community 23 May 2017 в 10:29
поделиться