Поддельный DbContext Entity Framework 4.1 для тестирования

Я использую этот учебник для подделки моего DbContext и тестирования: http://refactorthis.wordpress.com/2011/05/31/mock-faking-dbcontext-in-entity-framework-4-1-with-a-generic-repository/

Но я должен изменить реализацию FakeMainModuleContext для использования в моих контроллерах:

public class FakeQuestiona2011Context : IQuestiona2011Context
{
    private IDbSet<Credencial> _credencial;
    private IDbSet<Perfil> _perfil;
    private IDbSet<Apurador> _apurador;
    private IDbSet<Entrevistado> _entrevistado;
    private IDbSet<Setor> _setor;
    private IDbSet<Secretaria> _secretaria;
    private IDbSet<Pesquisa> _pesquisa;
    private IDbSet<Pergunta> _pergunta;
    private IDbSet<Resposta> _resposta;

    public IDbSet<Credencial> Credencial { get { return _credencial ?? (_credencial = new FakeDbSet<Credencial>()); } set { } }
    public IDbSet<Perfil> Perfil { get { return _perfil ?? (_perfil = new FakeDbSet<Perfil>()); } set { } }
    public IDbSet<Apurador> Apurador { get { return _apurador ?? (_apurador = new FakeDbSet<Apurador>()); } set { } }
    public IDbSet<Entrevistado> Entrevistado { get { return _entrevistado ?? (_entrevistado = new FakeDbSet<Entrevistado>()); } set { } }
    public IDbSet<Setor> Setor { get { return _setor ?? (_setor = new FakeDbSet<Setor>()); } set { } }
    public IDbSet<Secretaria> Secretaria { get { return _secretaria ?? (_secretaria = new FakeDbSet<Secretaria>()); } set { } }
    public IDbSet<Pesquisa> Pesquisa { get { return _pesquisa ?? (_pesquisa = new FakeDbSet<Pesquisa>()); } set { } }
    public IDbSet<Pergunta> Pergunta { get { return _pergunta ?? (_pergunta = new FakeDbSet<Pergunta>()); } set { } }
    public IDbSet<Resposta> Resposta { get { return _resposta ?? (_resposta = new FakeDbSet<Resposta>()); } set { } }

    public void SaveChanges()
    {
        // do nothing (probably set a variable as saved for testing)
    }
}

И мой тест таков:

[TestMethod]
public void IndexTest()
{
    IQuestiona2011Context fakeContext = new FakeQuestiona2011Context();
    var mockAuthenticationService = new Mock<IAuthenticationService>();

    var apuradores = new List<Apurador>
    {
        new Apurador() { Matricula = "1234", Nome = "Acaz Souza Pereira", Email = "acaz@telecom.inf.br", Ramal = "1234" },
        new Apurador() { Matricula = "4321", Nome = "Samla Souza Pereira", Email = "samla@telecom.inf.br", Ramal = "4321" },
        new Apurador() { Matricula = "4213", Nome = "Valderli Souza Pereira", Email = "valderli@telecom.inf.br", Ramal = "4213" }
    };
    apuradores.ForEach(apurador => fakeContext.Apurador.Add(apurador));

    ApuradorController apuradorController = new ApuradorController(fakeContext, mockAuthenticationService.Object);
    ActionResult actionResult = apuradorController.Index();

    Assert.IsNotNull(actionResult);
    Assert.IsInstanceOfType(actionResult, typeof(ViewResult));

    ViewResult viewResult = (ViewResult)actionResult;

    Assert.IsInstanceOfType(viewResult.ViewData.Model, typeof(IndexViewModel));

    IndexViewModel indexViewModel = (IndexViewModel)viewResult.ViewData.Model;

    Assert.AreEqual(3, indexViewModel.Apuradores.Count);
}

Я делаю это правильно?

5 ответов

Решение

К сожалению, вы не делаете это правильно, потому что эта статья не так. Притворяется, что FakeContext сделает ваш код тестируемым, но это не так. Как только вы выставляете IDbSet или же IQueryable к вашему контроллеру, и вы фальсифицируете набор в коллекции памяти, вы никогда не можете быть уверены, что ваш модульный тест действительно проверяет ваш код. Очень просто написать запрос LINQ в вашем контроллере, который пройдет ваш модульный тест (потому что FakeContext использует LINQ-to-Objects), но терпит неудачу во время выполнения (потому что ваш реальный контекст использует LINQ-to-Entities). Это делает всю цель вашего юнит-тестирования бесполезной.

Мое мнение: не беспокойтесь о фальшивом контексте, если вы хотите выставить наборы контроллеру. Вместо этого используйте интеграционные тесты с реальной базой данных для тестирования. Это единственный способ проверить, что запросы LINQ, определенные в контроллере, выполняют то, что вы ожидаете.

Конечно, если вы хотите просто позвонить ToList или же FirstOrDefault на ваших множествах ваших FakeContext будет служить вам хорошо, но как только вы сделаете что-нибудь более сложное, вы можете довольно быстро найти ловушку (просто поместите строку "Невозможно перевести в выражение магазина" в Google - все эти проблемы появятся только при запуске Linq-to-entity, но они пройдут ваши тесты с Linq-to-objects).

Это довольно распространенный вопрос, поэтому вы можете проверить некоторые другие примеры:

"К сожалению, вы делаете это неправильно, потому что эта статья неверна. Она делает вид, что FakeContext сделает ваш код тестируемым, но не сделает"

Я создатель поста в блоге, на который вы ссылаетесь. Проблема, которую я вижу здесь, заключается в неправильном понимании основ модульного тестирования N-Layered. Мой пост не предназначен для непосредственного тестирования логики контроллера.

Модульный тест должен выполняться точно так, как следует из названия, и тестировать "Один блок". Если я тестирую контроллер (как вы делаете выше), я забываю все о доступе к данным. Я должен был удалить все вызовы контекста базы данных в моем уме и заменить их вызовом метода черного ящика, как если бы эти операции были мне неизвестны. Это код вокруг тех операций, которые я заинтересован в тестировании.

Пример:

В моем приложении MVC мы используем шаблон хранилища. У меня есть хранилище, скажем CustomerRepository: ICustomerRepository, которое будет выполнять все операции с моей базой данных клиентов.

Если бы я должен был проверить свои контроллеры, хотел бы я, чтобы тесты проверяли мой репозиторий, мой доступ к базе данных и саму логику контроллера? конечно, нет! в этом конвейере много "единиц". Что вы хотите сделать, это создать поддельное хранилище, которое реализует ICustomerRepository, чтобы позволить вам тестировать логику контроллера в отдельности.

Насколько я знаю, это нельзя сделать только в контексте базы данных. (за исключением, может быть, для использования Microsoft Moles, которые вы можете проверить, если хотите). Это просто потому, что все запросы выполняются вне контекста в вашем классе контроллера.

Если бы я хотел проверить логику CustomerRepository, как бы я это сделал? Самый простой способ - использовать поддельный контекст. Это позволит мне убедиться, что когда я пытаюсь получить клиента по идентификатору, он фактически получает клиента по идентификатору и так далее. Методы репозитория очень просты, и проблема "Невозможно перевести в выражение магазина" обычно не возникает. Хотя в некоторых незначительных случаях это может (иногда из-за неправильно написанных запросов linq) в этих случаях важно также выполнять интеграционные тесты, которые будут проверять ваш код на всем пути к базе данных. Эти проблемы будут найдены в интеграционном тестировании. Я использовал эту технику N-Layered довольно давно и не нашел никаких проблем с этим.

Интеграционные тесты

Очевидно, что тестирование вашего приложения на базе самой базы данных является дорогостоящим упражнением, и, как только вы получите десятки тысяч тестов, это станет кошмаром, с другой стороны, оно лучше всего имитирует, как код будет использоваться в "реальном мире". Эти тесты также важны (от пользовательского интерфейса до базы данных) и будут выполняться как часть интеграционных тестов, а НЕ модульных тестов.

Acaz, что тебе действительно нужно в твоем сценарии, так это репозиторий, который можно смоделировать / подделать. Если вы хотите протестировать свои контроллеры так, как вы это делаете, тогда ваш контроллер должен принимать объект, который оборачивает функциональность базы данных. Затем он может вернуть все, что вам нужно, чтобы протестировать все аспекты функциональности вашего контроллера.

см. http://msdn.microsoft.com/en-us/library/ff714955.aspx

Чтобы протестировать сам репозиторий (обсуждаемый, если необходимо, во всех случаях), вы захотите либо подделать контекст, либо использовать что-то в соответствии с рамками Moles.

LINQ по своей природе сложно проверить. Тот факт, что запрос определяется вне контекста с использованием методов расширения, дает нам большую гибкость, но создает кошмар для тестирования. Оберните ваш контекст в хранилище, и эта проблема исчезнет.

прости так долго:)

Как отметил Ладислав Мрнка, вы должны тестировать Linq-to-Entity, но не Linq-to-Object. Обычно я использовал Sql CE в качестве тестовой БД и всегда воссоздаю базу данных перед каждым тестом. Это может сделать тест немного медленным, но пока я в порядке с производительностью для моих 100+ модульных тестов.

Сначала измените настройку строки подключения с помощью SqlCe в файле App.config вашего тестового проекта.

<connectionStrings>
    <add name="MyDbContext"
       connectionString="Data Source=|DataDirectory|MyDb.sdf"
         providerName="System.Data.SqlServerCe.4.0"
         />
</connectionStrings>

Во-вторых, установите инициализатор БД с DropCreateDatabaseAlways.

Database.SetInitializer<MyDbContext>(new DropCreateDatabaseAlways<MyDbContext>());

И затем принудительно инициализируйте EF перед выполнением каждого теста.

public void Setup() {
    Database.SetInitializer<MyDbContext>(new DropCreateDatabaseAlways<MyDbContext>());

    context = new MyDbContext();
    context.Database.Initialize(force: true);
}

Если вы используете xunit, вызовите метод Setup в своем конструкторе. Если вы используете MSTest, поместите TestInitializeAttribute в этот метод. Если монахиня.......

Вы можете создать Fake DbContext, используя Effort для EF 6+. Смотрите https://effort.codeplex.com/. Усилие означает "Формальная работа", "Конечный текст", "Реализация".

Для статьи с рабочим образцом, пожалуйста, см. http://www.codeproject.com/Tips/1036630/Using-Effort-Entity-Framework-Unit-Testing-Tool или http://www.codeproject.com/Articles/460175/Two-strategies-for-testing-Entity-Framework-Effort?msg=5122027.

Я знаю, что мы не должны этого делать, но иногда, в любом случае, вы должны это сделать (например, ваш начальник может спросить вас об этом и не передумает).

Так как я должен был сделать это, я оставляю это здесь, это могло бы помочь некоторым людям. Я довольно новичок в C# / .net и все, так что это далеко от оптимизации / очистки, я думаю, но, похоже, работает.

следующий MSDN Найдите здесь отсутствующий класс и, немного подумав, мне удалось добавить односторонние свойства: ключевыми элементами здесь являются AddNavigationProperty и RefreshNavigationProperties. Если у кого-нибудь есть предложения по улучшению этого кода, я с радостью приму их

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;

namespace MockFactory
{
    public class TestDbSet<TEntity> : DbSet<TEntity>, IQueryable, IEnumerable<TEntity>, IDbAsyncEnumerable<TEntity>
        where TEntity : class
    {
        public readonly ObservableCollection<TEntity> _data;
        private readonly IQueryable _query;
        private readonly Dictionary<Type, object> entities;

        public TestDbSet()
        {
            _data = new ObservableCollection<TEntity>();
            _query = _data.AsQueryable();

            entities = new Dictionary<Type, object>();
        }

        public override ObservableCollection<TEntity> Local
        {
            get { return _data; }
        }

        IDbAsyncEnumerator<TEntity> IDbAsyncEnumerable<TEntity>.GetAsyncEnumerator()
        {
            return new TestDbAsyncEnumerator<TEntity>(_data.GetEnumerator());
        }

        IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
        {
            return _data.GetEnumerator();
        }

        Type IQueryable.ElementType
        {
            get { return _query.ElementType; }
        }

        Expression IQueryable.Expression
        {
            get { return _query.Expression; }
        }

        IQueryProvider IQueryable.Provider
        {
            get { return new TestDbAsyncQueryProvider<TEntity>(_query.Provider); }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _data.GetEnumerator();
        }

        public void AddNavigationProperty<T>(DbSet<T> dbSet) where T : class
        {
            entities.Add(typeof (T), dbSet);
        }

        public void RefreshNavigationProperty(TEntity item)
        {
            foreach (var entity in entities)
            {
                var property = item.GetType().GetProperty(entity.Key.Name);

                var type =
                    (int)item.GetType().GetProperty(entity.Key.Name.Replace(typeof(TEntity).Name, "")).GetValue(item);

                var dbSets = (IEnumerable<object>)entity.Value.GetType().GetField("_data").GetValue(entity.Value);

                var dbSet = dbSets.Single(x => (int)x.GetType().GetProperty("Id").GetValue(x) == type);
                property.SetValue(item, dbSet);
            }
        }

        public override TEntity Add(TEntity item)
        {
            RefreshNavigationProperty(item);
            _data.Add(item);
            return item;
        }

        public override TEntity Remove(TEntity item)
        {
            _data.Remove(item);
            return item;
        }

        public override TEntity Attach(TEntity item)
        {
            _data.Add(item);
            return item;
        }

        public override TEntity Create()
        {
            return Activator.CreateInstance<TEntity>();
        }

        public override TDerivedEntity Create<TDerivedEntity>()
        {
            return Activator.CreateInstance<TDerivedEntity>();
        }
    }
}

Затем вы можете создать свой контекст

 public TestContext()
        {
            TypeUsers = new TestDbSet<TypeUser>();
            StatusUsers = new TestDbSet<StatusUser>();

            TypeUsers.Add(new TypeUser {Description = "FI", Id = 1});
            TypeUsers.Add(new TypeUser {Description = "HR", Id = 2});

            StatusUsers.Add(new StatusUser { Description = "Created", Id = 1 });
            StatusUsers.Add(new StatusUser { Description = "Deleted", Id = 2 });
            StatusUsers.Add(new StatusUser { Description = "PendingHR", Id = 3 });


            Users = new TestDbSet<User>();

            ((TestDbSet<User>) Users).AddNavigationProperty(StatusUsers);
           ((TestDbSet<User>)Users).AddNavigationProperty(TypeUsers);

        }

        public override DbSet<TypeUser> TypeUsers { get; set; }
        public override DbSet<StatusUser> StatusUsers { get; set; }
        public override DbSet<User> Users { get; set; }
        public int SaveChangesCount { get; private set; }

        public override int SaveChanges(string modifierId)
        {
            SaveChangesCount++;
            return 1;
        }
    }

Наконец, не забудьте в своем тесте обновить свойства навигации перед выполнением утверждения (должен быть лучший способ, но я не смог его найти)

ContextFactory.Entity.Users.Each(((TestDbSet<User>) ContextFactory.Entity.Users).RefreshNavigationProperty);
Другие вопросы по тегам