Подтвердить что ты не робот

Поддельный 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 = "[email protected]", Ramal = "1234" },
        new Apurador() { Matricula = "4321", Nome = "Samla Souza Pereira", Email = "[email protected]", Ramal = "4321" },
        new Apurador() { Matricula = "4213", Nome = "Valderli Souza Pereira", Email = "[email protected]", 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);
}

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

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

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

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

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

Пример:

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

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

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

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

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

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

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

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

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

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

жаль так долго:)

Ответ 3

Как отметил Ладислав Мрнка, вы должны протестировать 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>

Во-вторых, установите инициализатор db с 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, вызовите метод установки в своем конструкторе. Если вы используете MSTest, поставьте TestInitializeAttribute на этот метод. Если nunit.......

Ответ 4

Вы можете создать Fake DbContext, используя Effort для EF 6+. См. https://effort.codeplex.com/. Усиление означает E ntity F ramework F ake O bjectContext R ealization T оол.

Для статьи с рабочим образцом см. 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#xx5122027xx.

Ответ 5

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

Так как я должен был это сделать, я оставляю это здесь, это может помочь некоторым людям. Я совершенно новичок в С#/.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;
        }
    }

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

ContextFactory.Entity.Users.Each(((TestDbSet<User>) ContextFactory.Entity.Users).RefreshNavigationProperty);