Поддельный DbContext Entity Framework 4.1 для тестирования - PullRequest
46 голосов
/ 02 августа 2011

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

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

[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 ]

121 голосов
/ 02 августа 2011

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

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

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

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

62 голосов
/ 15 ноября 2011

"К сожалению, вы делаете это неправильно, потому что эта статья неверна. Она делает вид, что 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 сложно тестировать. Тот факт, что запрос определяется вне контекста с использованием методов расширения, дает нам большую гибкость, но создает кошмар для тестирования. Оберните ваш контекст в хранилище, и эта проблема исчезнет.

извините так долго :) 1040 *

23 голосов
/ 02 августа 2011

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

1 голос
/ 11 сентября 2015

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

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

0 голосов
/ 14 августа 2015

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

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