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


170

Я только начинаю с юнит-тестов и 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 Контекст с чем-то вроде этого подхода - Mocking EF При модульном тестировании или непосредственное использование среды моделирования на интерфейсе, таком как moq, - испытываете боль, которую могут пройти модульные тесты, но не обязательно работают из конца в конец, и резервируйте их с помощью интеграционных тестов?
  2. Может быть, использовать что-то вроде Effort, чтобы издеваться над EF - я никогда не использовал его и не уверен, что кто-то еще использует его в дикой природе?
  3. Не беспокойтесь о тестировании чего-либо, что просто вызывает EF - так что, по сути, сервисные методы, которые вызывают EF напрямую (getAll и т. Д.), Не тестируются модулем, а просто проверяются на интеграцию?

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


Привет, Модика, я недавно думал об этом (из-за этого вопроса: stackoverflow.com/questions/25977388/… ). В нем я пытаюсь описать немного более формально, как я работаю в данный момент, но я хотел бы услышать, как ты делаешь это.
Сами

Привет @ samy, мы решили сделать это не с помощью модульного теста, который напрямую касался EF. Запросы были протестированы, но в качестве интеграционного теста, а не модульных тестов. Mocking EF выглядит немного грязно, но этот проект был небольшим, так что влияние на производительность множества тестов, попавших в базу данных, на самом деле не было проблемой, поэтому мы могли бы быть немного более прагматичными в этом. Я все еще не уверен на 100%, что лучший подход - быть полностью правдивым с вами, в какой-то момент вы попадете на EF (и на вашу БД), и модульное тестирование здесь не подходит мне.
Модика

Ответы:


186

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

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

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

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

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

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

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

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

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

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

И наконец, написать такой большой объем SQL для тестирования ORM может быть очень тяжелой работой. Здесь я применяю очень противный подход (пуристы здесь не согласятся со мной). Я использую свой ORM для создания своего теста! Вместо того, чтобы иметь отдельный скрипт для каждого теста 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 удалена из самих методов тестирования

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

Изменить 3/5/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
Вот другой подход к тестированию интеграции. TL; DR - использование самого приложения для настройки тестовых данных, откат транзакции для каждого теста.
Герт Арнольд

3
@ Лиат, отличный ответ. Вы подтвердили мои подозрения о тестировании EF. У меня вопрос такой; Ваш пример для очень конкретного случая, что хорошо. Однако, как вы заметили, вам может потребоваться протестировать сотни объектов. В соответствии с принципом DRY (не повторять себя), как вы масштабируете свое решение, не повторяя каждый раз один и тот же базовый код?
Джеффри А. Гочин

4
Я должен не согласиться с этим, потому что это полностью обходит проблему. Модульное тестирование - это проверка логики функции. В примере OP логика зависит от хранилища данных. Вы правы, когда говорите не проверять EF, но это не проблема. Проблема заключается в тестировании вашего кода отдельно от хранилища данных. Тестирование вашего отображения - совершенно другая тема. Чтобы проверить правильность взаимодействия логики с данными, вам необходимо иметь возможность контролировать хранилище.
Синастетик

7
Никто не застрахован от того, стоит ли вам самостоятельно тестировать Entity Framework. Что происходит, так это то, что вам нужно протестировать какой-то метод, который выполняет некоторые вещи, а также выполняет EF-вызов к базе данных. Цель состоит в том, чтобы смоделировать EF, чтобы вы могли протестировать этот метод, не требуя базы данных на сервере сборки.
Маффин Человек

4
Мне очень нравится путешествие. Спасибо за добавленные изменения со временем - это все равно что читать контроль источников и понимать, как развивалось ваше мышление. Я действительно ценю функциональное (с EF) и единичное (смоделированное EF) различие.
Том Лейс

21

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

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

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

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

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

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


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

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

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

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


Спасибо за вклад, что я могу сделать, так как мне нужно, чтобы этот проект был запущен, так как это добросовестная оплачиваемая работа, это начать с некоторых репозиториев и посмотреть, как я получу, но Effort очень интересен. Из интереса на каком уровне вы использовали усилия в своих приложениях?
Модика

2
только если Effort поддерживал транзакции должным образом
Седат Капаноглу

и усилия имеют ошибку для строк с загрузчиком CSV, когда мы используем '' вместо нуля в строках.
Сэм

13

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

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

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


Спасибо @justin, я знаю о шаблоне репозитория, но чтение таких вещей, как ayende.com/blog/4784/… и lostechies.com/jimmybogard/2009/09/11/wither-the-repository заставило меня думать, что я не Я не хочу этот уровень абстракции, но опять же, они больше говорят о подходе Query, который очень сбивает с толку.
Модика

7
@Modika Ayende выбрала плохую реализацию шаблона хранилища для критики, и в результате она на 100% верна - она ​​перегружена и не дает никаких преимуществ. Хорошая реализация изолирует проверяемые модулем части вашего кода от реализации DAL. Использование NHibernate и EF напрямую затрудняет (если не невозможно) код для модульного тестирования и приводит к жесткой монолитной кодовой базе. Я все еще несколько скептически отношусь к шаблону хранилища, однако я на 100% убежден, что вам нужно как-то изолировать свою реализацию DAL, и хранилище - лучшее, что я нашел на данный момент.
Джастин

2
@Modika Прочтите вторую статью еще раз. «Я не хочу этот уровень абстракции» - это не то, что он говорит. Кроме того, ознакомьтесь с исходным шаблоном репозитория от Фаулера ( martinfowler.com/eaaCatalog/repository.html ) или DDD ( dddcommunity.org/resources/ddd_terms ). Не верьте скептикам без полного понимания оригинальной концепции. Что они действительно критикуют, так это недавнее злоупотребление шаблоном, а не сам шаблон (хотя они, вероятно, этого не знают).
guillaume31

1
@ guillaume31 Я не против паттерна репозитория (я его понимаю), я просто пытаюсь выяснить, нужно ли мне абстрагировать то, что уже является абстракцией на этом уровне, и если я могу опустить его и протестировать против EF напрямую путем насмешки и использовать его в моих тестах на уровне выше в моем приложении. Кроме того, если я не использую репо, я получаю преимущество расширенного набора функций EF, с репо я могу этого не получить.
Модика

После того, как я изолировал DAL с репозиторием, мне нужно как-то «смоделировать» базу данных (EF). До сих пор насмешка над контекстом и различные асинхронные расширения (ToListAsync (), FirstOrDefaultAsync () и т. Д.) Вызывали у меня разочарование.
Кевин Бертон

9

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

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

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

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

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

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


8

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

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

//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();
    }

    ...
}

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


1
Спасибо за ответ, но какая разница, скажем, в репозитории, где вы прячете внутренности EF за этим на этом уровне? Я действительно не хочу абстрагироваться от EF, хотя я все еще могу делать это с интерфейсом IContext? Я новичок в этом, будьте нежны :)
Модика

3
@ Модика Репо тоже хорошо. Какой бы узор вы ни выбрали. «Я не хочу абстрагировать EF». Вы хотите тестируемый код или нет?
Джонатан Хенсон

1
@ Модика моя точка зрения, у вас не будет НИКАКОГО тестируемого кода, если вы не разделите свои проблемы. Доступ к данным и бизнес-логика ДОЛЖНЫ быть на разных уровнях, чтобы проводить хорошие ремонтопригодные тесты.
Джонатан Хенсон

2
я просто не чувствовал необходимости оборачивать EF в абстракцию репозитория, поскольку по сути IDbSets являются репозиториями и контекстом UOW, я немного обновлю свой вопрос, поскольку это может вводить в заблуждение. Проблема заключается в любой абстракции, и главное - что именно я тестирую, потому что мои очереди не будут работать в тех же границах (linq-to-entity или linq-to-objects), поэтому, если я просто проверяю, что мой сервис делает Звонок, который кажется немного расточительным, или я здесь хорошо?
Модика

1
Хотя я согласен с вашими общими соображениями, DbContext - это единица работы, и IDbSets, безусловно, предназначены для реализации репозитория, и я не единственный, кто так думает. Я могу издеваться над EF, и на каком-то уровне мне нужно будет выполнять интеграционные тесты, действительно ли это имеет значение, делаю ли я это в репозитории или далее в службе? Тесная связь с БД на самом деле не проблема, я уверен, что это произойдет, но я не собираюсь планировать то, что может не произойти.
Модика

8

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

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

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

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

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

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

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

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

Образец кода:

[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();
        }
    }
}

1
На самом деле, мне очень нравится это решение. Супер простой в реализации и более реалистичные сценарии тестирования. Спасибо!
Слопапа

1
с EF 6 вы бы использовали DbContext.Database.BeginTransaction, не так ли?
SwissCoder

5

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


1
Да прагматизму. Я до сих пор утверждаю, что качество ваших модульных тестов хуже, чем качество вашего исходного кода. Конечно, есть смысл в использовании TDD для улучшения практики кодирования, а также для повышения удобства сопровождения, но TDD может иметь уменьшающуюся ценность. Мы запускаем все наши тесты для базы данных, потому что это дает нам уверенность в том, что использование EF и самих таблиц является правильным. Тесты занимают больше времени, но они более надежны.
Savage

3

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

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

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

  • Для других разработчиков в команде должно быть просто понять
  • Он должен изолировать код EF на минимально возможном уровне
  • Он не должен включать создание странных интерфейсов с множественной ответственностью (таких как «общий» или «типичный» шаблон хранилища)
  • Это должно быть легко настроить и настроить в модульном тесте

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

Я достиг этого благодаря простой инкапсуляции кода EF в выделенные классы запросов и команд. Идея проста: просто оберните любой код 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();
  }
}

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

Так как же выглядит юнит-тест нашего сервиса объектов? Это очень просто. В этом случае я использую 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
  }

}

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

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

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

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

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

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

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


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

3

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

В качестве альтернативы вы можете переключиться на 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

Я использовал фабрику, чтобы получить контекст, чтобы я мог создать контекст, близкий к его использованию. Кажется, это работает локально в visual studio, но не на моем сервере сборки TeamCity, пока не знаю почему.

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

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

Этот помощник Moq работает ( codeproject.com/Tips/1045590/… ), если у вас есть контекст для макета. Если вы поддерживаете макетированный контекст списком, он не будет вести себя как контекст, поддерживаемый базой данных SQL.
Андрей Паштет

2

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

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


0

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.