Как тестировать модули с Entity Framework 6, если вы беспокоитесь?

Я только начинаю с юнит-тестирования и TDD в целом. Раньше я баловался, но теперь я полон решимости добавить его в свой рабочий процесс и написать лучшее программное обеспечение.

Я задал вопрос вчера, что-то вроде этого, но, похоже, это вопрос сам по себе. Я приступил к реализации класса обслуживания, который буду использовать для абстрагирования бизнес-логики от контроллеров и отображения на конкретные модели и взаимодействия данных с помощью EF6.

Проблема в том, что я уже заблокировал себя, потому что я не хотел абстрагировать EF в репозитории (он все еще будет доступен за пределами сервисов для определенных запросов и т.д.) И хотел бы протестировать мои сервисы (будет использоваться контекст EF).

Здесь, наверное, вопрос, есть ли смысл делать это? Если да, то как люди делают это в дикой природе в свете утечек абстракций, вызванных IQueryable, и многих замечательных постов Ладислава Мрнки на тему модульного тестирования, которые не являются прямыми из-за различий в поставщиках Linq при работе с реализацией в памяти применительно к конкретной базе данных.

Код, который я хочу проверить, кажется довольно простым. (это просто фиктивный код, чтобы попытаться понять, что я делаю, я хочу управлять созданием, используя TDD)

Контекст

public interface IContext
{
    IDbSet<Product> Products { get; set; }
    IDbSet<Category> Categories { get; set; }
    int SaveChanges();
}

public class DataContext : DbContext, IContext
{
    public IDbSet<Product> Products { get; set; }
    public IDbSet<Category> Categories { get; set; }

    public DataContext(string connectionString)
                : base(connectionString)
    {

    }
}

служба

public class ProductService : IProductService
{
    private IContext _context;

    public ProductService(IContext dbContext)
    {
        _context = dbContext;
    }

    public IEnumerable<Product> GetAll()
    {
        var query = from p in _context.Products
                    select p;

        return query;
    }
}

В настоящее время я собираюсь сделать несколько вещей:

  1. Контекст Mocking EF с чем-то вроде этого approach- Mocking EF при модульном тестировании или непосредственно с использованием среды моделирования на интерфейсе, таком как moq - с болью от того, что модульные тесты могут проходить, но не обязательно работать из конца в конец и подкрепить их интеграционными тестами?
  2. Может быть, использовать что-то вроде Effort, чтобы издеваться над EF - я никогда не использовал его и не уверен, что кто-то еще использует его в дикой природе?
  3. Не беспокоиться о тестировании чего-либо, что просто вызывает EF - так что, по сути, сервисные методы, которые вызывают EF напрямую (getAll и т.д.), Не тестируются модульно, а просто тестируются на интеграцию?

Кто-нибудь на самом деле делает это без репо и имеет успех?

Ответ 1

Это тема, которая меня очень интересует. Есть много пуристов, которые говорят, что вы не должны тестировать такие технологии, как EF и NHibernate. Они правы, они уже очень тщательно тестируются и, как было сказано в предыдущем ответе, часто бессмысленно тратить огромное количество времени на тестирование того, что у вас нет.

Однако у вас есть база данных внизу!. Вот почему этот подход, на мой взгляд, ломается, вам не нужно проверять правильность работы EF/NH. Вам нужно проверить, что ваши сопоставления/реализации работают с вашей базой данных. По-моему, это одна из самых важных частей системы, которую вы можете проверить.

Строго говоря, однако мы выходим из домена модульного тестирования и тестируем интеграцию, но принципы остаются теми же.

Первое, что вам нужно сделать, - это высмеять ваш DAL, чтобы ваш BLL можно было протестировать независимо от EF и SQL. Это ваши модульные тесты. Далее вам нужно разработать свои тесты интеграции, чтобы доказать свою DAL, по-моему, они кажутся важными.

Есть несколько вещей, которые следует учитывать:

  • Ваша база данных должна быть в известном состоянии с каждым тестом. Большинство систем используют для этого резервное копирование или создание сценариев.
  • Каждый тест должен быть повторяемым.
  • Каждый тест должен быть атомарным

Существует два основных подхода к настройке вашей базы данных: первая - запуск DBT-сервера UnitTest script. Это гарантирует, что ваша база данных unit test всегда будет находиться в одном состоянии в начале каждого теста (вы можете либо reset, либо выполнить каждый тест в транзакции, чтобы убедиться в этом).

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

  • Ваша база данных проще, вам не нужна целая схема для каждого теста.
  • Каждый тест более безопасен, если вы изменяете одно значение в своем создаваемом script, это не отменяет десятки других тестов.

К сожалению, ваш компромисс здесь - скорость. Для запуска всех этих тестов требуется время, чтобы запустить все эти сценарии установки/срыва.

В последнем случае очень сложно написать такое большое количество SQL для тестирования вашего ORM. Вот где я принимаю очень неприятный подход (пуристы здесь не согласятся со мной). Я использую свой ORM для создания своего теста! Вместо того, чтобы иметь отдельный script для каждого теста DAL в моей системе, у меня есть фаза тестирования, которая создает объекты, привязывает их к контексту и сохраняет их. Затем я запускаю свой тест.

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

Я, без сомнения, оглянусь назад на этот ответ через несколько лет (месяцев/дней) и не соглашусь с самим собой, поскольку мои подходы изменились - однако это мой нынешний подход.

Чтобы попытаться подвести итог всему, что я сказал выше, это мой типичный тестовый тест интеграции:

[Test]
public void LoadUser()
{
  this.RunTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    return user.UserID;
  }, id => // the ID of the entity we need to load
  {
     var user = LoadMyUser(id); // load the entity
     Assert.AreEqual("Mr", user.Title); // test your properties
     Assert.AreEqual("Joe", user.Firstname);
     Assert.AreEqual("Bloggs", user.Lastname);
  }
}

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

Редактировать 13/10/2014

Я действительно сказал, что, вероятно, пересмотрю эту модель в ближайшие месяцы. Хотя я в значительной степени поддерживаю подход, который я выступал выше, я немного обновил свой механизм тестирования. Теперь я склонен создавать объекты в TestSetup и TestTearDown.

[SetUp]
public void Setup()
{
  this.SetupTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    this.UserID =  user.UserID;
  });
}

[TearDown]
public void TearDown()
{
   this.TearDownDatabase();
}

Затем проверьте каждое свойство отдельно

[Test]
public void TestTitle()
{
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Mr", user.Title);
}

[Test]
public void TestFirstname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Joe", user.Firstname);
}

[Test]
public void TestLastname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Bloggs", user.Lastname);
}

Существует несколько причин такого подхода:

  • Нет дополнительных запросов к базе данных (одна настройка, один разрыв)
  • Тесты гораздо более гранулированы, каждый тест проверяет одно свойство
  • Логика Setup/TearDown удаляется из самих методов тестирования.

Я чувствую, что это упрощает тестовый класс, и тесты более гранулированные (однозначные утверждения хороши)

Редактировать 5/3/2015

Еще одна ревизия этого подхода. Хотя настройки уровня класса очень полезны для тестов, таких как свойства загрузки, они менее полезны там, где требуются разные настройки. В этом случае настройка нового класса для каждого случая будет чрезмерной.

Чтобы помочь в этом, теперь я имею тенденцию иметь два базовых класса SetupPerTest и SingleSetup. Эти два класса выставляют структуру по мере необходимости.

В SingleSetup мы имеем очень похожий механизм, описанный в моем первом редактировании. Пример:

public TestProperties : SingleSetup
{
  public int UserID {get;set;}

  public override DoSetup(ISession session)
  {
    var user = new User("Joe", "Bloggs");
    session.Save(user);
    this.UserID = user.UserID;
  }

  [Test]
  public void TestLastname()
  {
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Bloggs", user.Lastname);
  }

  [Test]
  public void TestFirstname()
  {
       var user = LoadMyUser(this.UserID);
       Assert.AreEqual("Joe", user.Firstname);
  }
}

Однако ссылки, которые гарантируют, что загружаются только правильные адресаты, могут использовать подход SetupPerTest

public TestProperties : SetupPerTest
{
   [Test]
   public void EnsureCorrectReferenceIsLoaded()
   {
      int friendID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriend();
         session.Save(user);
         friendID = user.Friends.Single().FriendID;
      } () =>
      {
         var user = GetUser();
         Assert.AreEqual(friendID, user.Friends.Single().FriendID);
      });
   }
   [Test]
   public void EnsureOnlyCorrectFriendsAreLoaded()
   {
      int userID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriends(2);
         var user2 = CreateUserWithFriends(5);
         session.Save(user);
         session.Save(user2);
         userID = user.UserID;
      } () =>
      {
         var user = GetUser(userID);
         Assert.AreEqual(2, user.Friends.Count());
      });
   }
}

Таким образом, оба подхода работают в зависимости от того, что вы пытаетесь проверить.

Ответ 2

Усилие Опыт Обратная связь здесь

После долгих чтений я использовал Effort в своих тестах: во время тестов Context создается фабрикой, которая возвращает версию в памяти, что позволяет мне каждый раз тестировать с чистого листа. Вне тестов фабрика преобразуется в ту, которая возвращает весь контекст.

Однако у меня есть ощущение, что тестирование на основе полнофункционального макета базы данных приводит к затягиванию тестов; вы понимаете, что вам нужно позаботиться о настройке целого ряда зависимостей, чтобы протестировать одну часть системы. Вы также склоняетесь к организации вместе тестов, которые могут быть не связаны, просто потому, что есть только один огромный объект, который обрабатывает все. Если вы не обращаете внимания, вы можете провести интеграционное тестирование вместо юнит-тестирования

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

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

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


Отредактировано для уточнения:

Я использовал Effort для тестирования приложения веб-сервиса. Каждое входящее сообщение M направляется в IHandlerOf<M> через Windsor. Castle.Windsor разрешает IHandlerOf<M> который восстанавливает зависимости компонента. Одна из этих зависимостей - DataContextFactory, которая позволяет обработчику запрашивать фабрику.

В своих тестах я непосредственно создаю экземпляр компонента IHandlerOf, макетирую все подкомпоненты SUT и обрабатывает обработанный Effort объект DataContextFactory обработчику.

Это означает, что я не занимаюсь модульным тестом в строгом смысле, поскольку БД поражен моими тестами. Однако, как я уже сказал выше, это позволило мне взяться за дело, и я мог быстро проверить некоторые точки в приложении

Ответ 3

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

Что касается тестирования кода EF - я пишу автоматизированные тесты интеграции для своих репозиториев, которые записывают различные строки в базу данных во время их инициализации, а затем вызывают мои реализации репозитория, чтобы убедиться, что они ведут себя как ожидалось (например, чтобы убедиться, что результаты отфильтрованы правильно или что они отсортированы в правильном порядке).

Это интеграционные тесты, а не модульные тесты, поскольку тесты полагаются на наличие соединения с базой данных и что в целевой базе данных уже установлена ​​последняя современная схема.

Ответ 4

Итак, вот что, Entity Framework - это реализация, поэтому, несмотря на то, что она абстрагирует сложность взаимодействия с базой данных, взаимодействие напрямую по-прежнему сопряжено с жесткой связью и почему это запутывает тестирование.

Модульное тестирование - это тестирование логики функции и каждого ее потенциального результата в изоляции от любых внешних зависимостей, которые в этом случае являются хранилищем данных. Для этого вам нужно иметь возможность контролировать поведение хранилища данных. Например, если вы хотите утверждать, что ваша функция возвращает false, если выбранный пользователь не удовлетворяет определенному набору критериев, тогда ваше хранилище данных [mocked] должно быть настроено так, чтобы всегда возвращать пользователя, который не соответствует критериям, и наоборот наоборот, для противоположного утверждения.

С учетом сказанного и принимая тот факт, что EF является реализацией, я бы предпочел идею абстрагирования репозитория. Кажется немного избыточным? Это не так, потому что вы решаете проблему, которая изолирует ваш код от реализации данных.

В DDD репозитории только когда-либо возвращают агрегированные корни, а не DAO. Таким образом, потребитель репозитория никогда не должен знать о реализации данных (как и не должен), и мы можем использовать это как пример того, как решить эту проблему. В этом случае объект, который генерируется EF, является DAO и как таковой, должен быть скрыт от вашего приложения. Это еще одно преимущество репозитория, который вы определяете. Вы можете определить бизнес-объект как его возвращаемый тип вместо объекта EF. Теперь то, что делает репо, скрывает вызовы EF и сопоставляет EF-ответ на этот бизнес-объект, определенный в сигнатуре repos. Теперь вы можете использовать это репо вместо зависимостей DbContext, которые вы вводите в свои классы, и, следовательно, теперь вы можете издеваться над этим интерфейсом, чтобы дать вам контроль, который вам нужен, чтобы изолировать ваш код.

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

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

Ответ 5

Я бы не unit test код, который у меня нет. Что вы здесь тестируете, что работает компилятор MSFT?

Тем не менее, чтобы сделать этот код пригодным для тестирования, вы почти должны сделать свой уровень доступа к данным отдельным от вашего кода бизнес-логики. Что я делаю, это взять все мои EF файлы и поместить их в (или несколько) DAO или DAL-класс, который также имеет соответствующий интерфейс. Затем я пишу свою службу, которая будет иметь объект DAO или DAL, введенный в качестве зависимости (предпочтительно, для инъекции конструктора), на который ссылается как интерфейс. Теперь часть, которая должна быть протестирована (ваш код), может быть легко протестирована, издеваясь над интерфейсом DAO и введя ее в свой экземпляр службы внутри вашего unit test.

//this is testable just inject a mock of IProductDAO during unit testing
public class ProductService : IProductService
{
    private IProductDAO _productDAO;

    public ProductService(IProductDAO productDAO)
    {
        _productDAO = productDAO;
    }

    public List<Product> GetAllProducts()
    {
        return _productDAO.GetAll();
    }

    ...
}

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

Ответ 6

Я когда-нибудь пошарил, чтобы понять эти соображения:

1- Если мое приложение обращается к базе данных, почему тест не должен? Что делать, если что-то не так с доступом к данным? Тесты должны знать это заранее и сообщать о проблеме.

2- Шаблон репозитория несколько тяжелый и требует много времени.

Итак, я придумал такой подход, который я считаю не лучшим, но выполнил мои ожидания:

Use TransactionScope in the tests methods to avoid changes in the database.

Для этого необходимо:

1- Установите EntityFramework в тестовый проект. 2- Поместите строку подключения в файл app.config тестового проекта. 3- Ссылка на dll System.Transactions в тестовом проекте.

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

Пример кода:

[TestClass]
public class NameValueTest
{
    [TestMethod]
    public void Edit()
    {
        NameValueController controller = new NameValueController();

        using(var ts = new TransactionScope()) {
            Assert.IsNotNull(controller.Edit(new Models.NameValue()
            {
                NameValueId = 1,
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }

    [TestMethod]
    public void Create()
    {
        NameValueController controller = new NameValueController();

        using (var ts = new TransactionScope())
        {
            Assert.IsNotNull(controller.Create(new Models.NameValue()
            {
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }
}

Ответ 7

Короче говоря, я бы сказал, что нет, сок не стоит сжимать, чтобы протестировать метод службы с помощью одной строки, которая извлекает данные модели. По моему опыту, люди, которые новичок в TDD, хотят протестировать абсолютно все. Старый каштан абстрагирования фасада в сторонней структуре просто для того, чтобы вы могли создать макет этого API-интерфейсов, с которым вы уклоняетесь/расширяетесь, чтобы вы могли вводить фиктивные данные, не имеет большого значения в моем сознании. У каждого есть другое представление о том, сколько единиц тестирования лучше. В наши дни я склонен быть более прагматичным и спрашиваю себя, действительно ли мой тест действительно повышает ценность конечного продукта и по какой цене.

Ответ 8

Я хочу поделиться рассмотренным и кратко обсуждаемым подходом, но покажу фактический пример, который я использую в настоящее время для помощи unit test сервисов на основе EF.

Во-первых, я бы хотел использовать провайдера in-memory из EF Core, но это касается EF 6. Кроме того, для других систем хранения, таких как RavenDB, я также был бы сторонником тестирования через базу данных в памяти поставщик. Опять же - это специально, чтобы помочь проверить код на основе EF без большой церемонии.

Вот цели, которые я придумал с шаблоном:

  • Для других разработчиков в команде должно быть просто понятно.
  • Он должен изолировать EF-код на максимально возможном уровне
  • Он не должен включать создание странных интерфейсов с множественной ответственностью (например, "общий" или "типичный" шаблон репозитория)
  • Его легко настроить и настроить в unit test

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

То, как я это достиг, было просто инкапсулированием EF-кода в выделенные классы Query и Command. Идея проста: просто оберните любой EF-код в класс и зависеть от интерфейса в классах, которые изначально использовали бы его. Основной проблемой, которую мне нужно было решить, было избежать добавления многочисленных зависимостей к классам и создания большого количества кода в моих тестах.

Здесь находится полезная простая библиотека: Mediatr. Он позволяет осуществлять простой обмен сообщениями в процессе и делает это путем развязывания "запросов" от обработчиков, которые реализуют код. Это имеет дополнительное преимущество отделить "что" от "как". Например, путем инкапсуляции EF-кода в небольшие фрагменты он позволяет заменить реализации другим поставщиком или совершенно другим механизмом, потому что все, что вы делаете, - это отправка запроса на выполнение действия.

Используя инъекцию зависимостей (с или без рамки - ваши предпочтения), мы можем легко высмеять медиатор и управлять механизмами запроса/ответа, чтобы включить модульное тестирование EF-кода.

Во-первых, скажем, у нас есть служба, которая имеет бизнес-логику, которую мы должны проверить:

public class FeatureService {

  private readonly IMediator _mediator;

  public FeatureService(IMediator mediator) {
    _mediator = mediator;
  }

  public async Task ComplexBusinessLogic() {
    // retrieve relevant objects

    var results = await _mediator.Send(new GetRelevantDbObjectsQuery());
    // normally, this would have looked like...
    // var results = _myDbContext.DbObjects.Where(x => foo).ToList();

    // perform business logic
    // ...    
  }
}

Вы начинаете видеть преимущество этого подхода? Вы не только явно, инкапсулируете весь код, связанный с EF, в дескриптивные классы, вы разрешаете расширение, устраняя проблему реализации "как" обрабатывается этот запрос - этому классу все равно, объекты поступают из EF, MongoDB или текстового файла.

Теперь для запроса и обработчика через MediatR:

public class GetRelevantDbObjectsQuery : IRequest<DbObject[]> {
  // no input needed for this particular request,
  // but you would simply add plain properties here if needed
}

public class GetRelevantDbObjectsEFQueryHandler : IRequestHandler<GetRelevantDbObjectsQuery, DbObject[]> {
  private readonly IDbContext _db;

  public GetRelevantDbObjectsEFQueryHandler(IDbContext db) {
    _db = db;
  }

  public DbObject[] Handle(GetRelevantDbObjectsQuery message) {
    return _db.DbObjects.Where(foo => bar).ToList();
  }
}

Как вы можете видеть, абстракция проста и инкапсулирована. Это также абсолютно проверяемый, потому что в тесте интеграции вы можете тестировать этот класс индивидуально - здесь нет проблем с бизнесом.

Итак, как выглядит unit test нашего сервисного сервиса? Это просто. В этом случае я использую Moq, чтобы делать издевательства (используйте все, что делает вас счастливым):

[TestClass]
public class FeatureServiceTests {

  // mock of Mediator to handle request/responses
  private Mock<IMediator> _mediator;

  // subject under test
  private FeatureService _sut;

  [TestInitialize]
  public void Setup() {

    // set up Mediator mock
    _mediator = new Mock<IMediator>(MockBehavior.Strict);

    // inject mock as dependency
    _sut = new FeatureService(_mediator.Object);
  }

  [TestCleanup]
  public void Teardown() {

    // ensure we have called or expected all calls to Mediator
    _mediator.VerifyAll();
  }

  [TestMethod]
  public void ComplexBusinessLogic_Does_What_I_Expect() {
    var dbObjects = new List<DbObject>() {
      // set up any test objects
      new DbObject() { }
    };

    // arrange

    // setup Mediator to return our fake objects when it receives a message to perform our query
    // in practice, I find it better to create an extension method that encapsulates this setup here
    _mediator.Setup(x => x.Send(It.IsAny<GetRelevantDbObjectsQuery>(), default(CancellationToken)).ReturnsAsync(dbObjects.ToArray()).Callback(
    (GetRelevantDbObjectsQuery message, CancellationToken token) => {
       // using Moq Callback functionality, you can make assertions
       // on expected request being passed in
       Assert.IsNotNull(message);
    });

    // act
    _sut.ComplexBusinessLogic();

    // assertions
  }

}

Вы можете видеть, что нам нужна только одна настройка, и нам даже не нужно ничего настраивать - это очень простой unit test. Пусть будет ясно: Это можно обойтись без чего-то вроде Mediatr (вы просто реализуете интерфейс и издеваетесь над тестированием, например IGetRelevantDbObjectsQuery), но на практике для большой базы кода со многими функциями и запросы/команды, мне нравится инкапсуляция и врожденная поддержка DI Mediatr.

Если вам интересно, как я организую эти классы, это довольно просто:

- MyProject
  - Features
    - MyFeature
      - Queries
      - Commands
      - Services
      - DependencyConfig.cs (Ninject feature modules)

Организация по фрагментам функций находится рядом, но это сохраняет все релевантный/зависимый код вместе и легко обнаруживается. Самое главное, я разделяю Запросы и Команды - следуя принципу Command/Query Separation.

Это соответствует всем моим критериям: это низкая церемония, это легко понять, и есть дополнительные скрытые преимущества. Например, как вы справляетесь с сохранением изменений? Теперь вы можете упростить свой Db-контекст, используя интерфейс ролей (IUnitOfWork.SaveChangesAsync()), и приманить вызовы к интерфейсу единой ролей, или вы можете инкапсулировать фиксацию/откат внутри вашего RequestHandlers - однако вы предпочитаете делать это зависит от вас, так как если он поддерживается. Например, у меня возникло соблазн создать единый общий запрос/обработчик, в котором вы просто передали бы объект EF, и он сохранит/обновит/удалит его, но вы должны спросить, каково ваше намерение, и помните, что если вы хотите замените обработчик другим поставщиком/реализацией хранилища, вы должны, вероятно, создать явные команды/запросы, которые представляют то, что вы намереваетесь сделать. Чаще всего отдельная служба или функция нуждаются в чем-то конкретном - не создавайте общий материал, прежде чем вам понадобится.

Конечно, в этом шаблоне есть предостережения - вы можете зайти слишком далеко с помощью простого механизма pub/sub. Я ограничил свою реализацию только абстрагированием кода, связанного с EF, но предприимчивые разработчики могли начать использовать MediatR, чтобы переходить за борт и все сообщения - все это - что-то хорошее из практики проверки кода и экспертных обзоров должно ловить. Это проблема процесса, а не проблема с MediatR, поэтому просто знайте, как вы используете этот шаблон.

Вам нужен конкретный пример того, как люди участвуют в модульном тестировании/издевательстве над EF, и это подход, который успешно работает для нас в нашем проекте, и команда очень довольна тем, насколько легко это принять. Надеюсь, это поможет! Как и все в программировании, существует множество подходов, и все зависит от того, чего вы хотите достичь. Я ценю простоту, простоту использования, ремонтопригодность и открытость - и это решение отвечает всем этим требованиям.

Ответ 9

Существует Усилия, которая является поставщиком базы данных сущности базы данных. Я на самом деле не пробовал... Хаа просто заметил, что это было упомянуто в вопросе!

В качестве альтернативы вы можете переключиться на EntityFrameworkCore, у которого есть встроенный поставщик базы данных памяти.

https://blog.goyello.com/2016/07/14/save-time-mocking-use-your-real-entity-framework-dbcontext-in-unit-tests/

https://github.com/tamasflamich/effort

Я использовал factory для получения контекста, поэтому я могу создать контекст, близкий к его использованию. Это, похоже, работает локально в визуальной студии, но не на моем сервере сборки TeamCity, но пока не знаю почему.

return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");

Ответ 10

Мне нравится отделять фильтры от других частей кода и тестировать их, как я излагаю в своем блоге здесь http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html

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

Ответ 11

Важно проверить, что вы ожидаете от структуры сущности (то есть подтвердить свои ожидания). Один из способов сделать это, который я успешно использовал, - использовать moq, как показано в этом примере (чтобы долго копировать в этот ответ):

https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking

Однако будьте осторожны... Не гарантируется, что контекст SQL возвращает вещи в определенном порядке, если у вас нет соответствующего "OrderBy" в вашем запросе linq, поэтому можно писать вещи, которые проходят при тестировании с использованием списка в памяти ( linq-to-entity), но терпят неудачу в вашей среде uat/live, когда (linq-to-sql) используется.