Як ви проводите тестування одиниць за допомогою Entity Framework 6, чи варто вам турбуватися?


170

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

Хтось там насправді робить це без Репо і має успіх?


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

Привіт @samy, те, що ми вирішили зробити це, не було тестуванням одиниць, що торкнулося EF безпосередньо. Запити були протестовані, але як інтеграційний тест, а не одиничний тест. Знущання 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.05.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
@Liath, чудова реакція. Ви підтверджуєте мої підозри щодо тестування EF. Моє запитання таке; ваш приклад - це дуже конкретний випадок, що добре. Однак, як ви зазначили, можливо, вам доведеться протестувати сотні об'єктів. Відповідно до принципу DRY (не повторюй себе), як ви масштабуєте своє рішення, не повторюючи один і той же основний шаблон коду?
Джеффрі А. Гочін

4
Я з цим не погоджуюся, оскільки це повністю сторони проблеми. Тестування блоку - це тестування логіки функції. У прикладі ОП логіка має залежність від сховища даних. Ти маєш рацію, коли кажеш, що не тестувати EF, але це не проблема. Проблема полягає в тестуванні вашого коду ізольовано від сховища даних. Тестування вашого відображення - зовсім інша тема. Щоб перевірити, чи логіка правильно взаємодіє з даними, вам потрібно мати можливість керувати магазином.
Сінестетик

7
Ніхто не стоїть на огорожі з приводу того, чи слід ви самі перевіряти Entity Framework. Що трапляється, що вам потрібно протестувати якийсь метод, який виконує деякі речі, а також зробити виклик EF у базу даних. Мета - знущатися над EF, щоб ви могли протестувати цей метод, не вимагаючи бази даних на вашому сервері збірки.
The Muffin Man

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

21

Тут ви знайдете відгуки про зусилля

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

Однак у мене є відчуття, що тестування на макет бази даних має тенденцію перетягувати тести; ви розумієте, що вам потрібно подбати про встановлення цілого ряду залежностей, щоб перевірити одну частину системи. Ви також схильні до того, щоб організувати разом тести, які можуть бути не пов’язані між собою, лише тому, що є лише один величезний об'єкт, який обробляє все. Якщо ви не звернете уваги, ви можете виявити, що ви робите тестування інтеграції замість одиничного тестування

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

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

Редагувати, щоб додати : Зусилля потребують певного часу, щоб зігрітися, тому ви дивитесь на прибл. 5 секунд при запуску тесту. Це може бути проблемою для вас, якщо вам потрібен тестовий набір, щоб бути дуже ефективним.


Відредаговано для уточнення:

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

У своїх тестах я інстанціюю компонент IHandlerOf безпосередньо, знущаюся над усіма підкомпонентами SUT та передає зусилля, оброблені зусиллям, DataContextFactoryдля обробника.

Це означає, що я не здійснюю тестування в строгому сенсі, оскільки БД вражена моїми тестами. Однак, як я вже говорив вище, я дозволяв мені вдарити землю, і я міг швидко перевірити деякі моменти програми


Дякую за вклад, що я можу зробити так, як мені доведеться запустити цей проект, оскільки це робота, яка платить за добросовісні стосунки, почніть з певних репостів і подивіться, як я дістаюсь, але зусилля дуже цікаві. Не цікаво, на якому шарі ви доклали зусиль у своїх програмах?
Модіка

2
тільки якщо
намагання

і зусилля має помилку для рядків із завантажувачем csv, коли ми використовуємо '' замість null у рядках.
Сем

13

Якщо ви хочете , щоб блок коду тест , то вам необхідно ізолювати код , який ви хочете перевірити (в цьому випадку ваш сервіс) від зовнішніх ресурсів (наприклад , бази даних). Можливо, ви могли б зробити це за допомогою якихось постачальників EF в пам'яті , однак набагато більш поширеним способом є абстрагувати свою реалізацію EF, наприклад, з якоюсь схемою сховища. Без цієї ізоляції будь-які тести, які ви пишете, будуть інтеграційними тестами, а не одиничними тестами.

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

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


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

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

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

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

Після того, як я виділив DAL із сховищем, мені потрібно якось "знущатися" з бази даних (EF). Поки знущання над контекстом та різні розширення асинхронізації (ToListAsync (), FirstOrDefaultAsync () тощо) призвели до мене розчарування.
Кевін Бертон

9

Отже, ось що, Entity Framework - це реалізація, тому, незважаючи на те, що вона абстрагує складність взаємодії з базами даних, безпосередньо взаємодія все ще є тісною зв'язкою, і тому тестувати її заплутано.

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

З огляду на це, і приймаючи той факт, що EF - це реалізація, я, швидше за все, висловлюю перевагу ідеї абстрагування сховища. Здається, трохи зайвим? Це не так, тому що ви вирішуєте проблему, яка ізолює ваш код від реалізації даних.

У DDD сховища повертають лише сукупні корені, а не DAO. Таким чином, споживач сховища ніколи не повинен знати про реалізацію даних (як не слід), і ми можемо використовувати це як приклад того, як вирішити цю проблему. У цьому випадку об'єкт, що генерується EF, є DAO і як такий повинен бути прихованим від вашої програми. Це ще одна перевага сховища, яке ви визначаєте. Ви можете визначити бізнес-об’єкт як його тип повернення замість об’єкта EF. Тепер те, що робить РЕПО, - це приховати виклики до EF та відобразити відповідь EF на той бізнес-об’єкт, визначений у підписі repos. Тепер ви можете використовувати це репо замість залежності 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? Я новачок у цьому, будьте лагідні :)
Modika

3
@Modika A Repo теж добре. Яку б модель ви не хотіли. "Я не хочу дуже абстрактно використовувати EF" Ви хочете перевірити код чи ні?
Джонатан Хенсон

1
@Modika, на мою думку, ви не матимете будь-якого перевіряемого коду, якщо не розділите свої проблеми. Доступ до даних та бізнес-логіка ОБОВ'ЯЗКОВО мають бути в окремих шарах, щоб витягнути хороші ремонтопридатні тести.
Джонатан Хенсон

2
я просто не відчував необхідності перетворювати EF у абстракцію сховища, оскільки, по суті, IDbSets є репо, а контекст UOW, я трохи оновлю своє запитання, оскільки це може бути оманливим. Питання пов'язане з будь-якою абстракцією, і головний момент полягає в тому, що саме я тестую, тому що мої запити не працюватимуть у тих же межах (linq-to-subjects vs linq-to-object), тому якщо я просто тестую, що моя служба робить дзвінок, який здається трохи марнотратним чи я тут добре?
Модіка

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
Насправді мені дуже подобається таке рішення. Супер простий у реалізації та більш реалістичні сценарії тестування. Дякую!
slopapa

1
з EF 6 ви б використовували DbContext.Database.BeginTransaction, чи не так?
SwissCoder

5

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


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

3

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

По-перше, я хотів би використовувати провайдера пам'яті від EF Core, але це приблизно для EF 6. Крім того, для інших систем зберігання даних, таких як RavenDB, я також був би прихильником тестування через постачальника баз даних в пам'яті. Знову ж таки - це спеціально для тестування коду на основі EF без великої кількості церемоній .

Ось цілі, які я мав, коли я придумав схему:

  • Інші розробники в команді повинні бути зрозумілі
  • Він повинен ізолювати код EF на найменшому можливому рівні
  • Він не повинен включати створення дивних інтерфейсів з багатовідповідальністю (наприклад, "загальний" або "типовий" шаблон сховища)
  • Конфігурувати та налаштовувати під час тестування пристрою слід легко

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

Я домігся цього шляхом просто інкапсуляції коду EF у спеціальні класи Query and 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();
  }
}

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

То як виглядає одиничний тест нашої функції функціональних можливостей? Це дуже просто. У цьому випадку я використовую 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, використовуючи рольовий інтерфейс (IUnitOfWork.SaveChangesAsync()) та знущатися над дзвінками до інтерфейсу однієї ролі, або ти можеш інкапсулювати здійснення чи відкат всередині своїх RequestHandlers - однак ти вважаєш за краще робити це залежно від тебе, поки це неможливо. Наприклад, я спокусився створити єдиний загальний запит / обробник, де ви просто передатимете об'єкт EF, і він збереже / оновить / видалить його - але ви повинні запитати, який ваш намір, і пам’ятати, що якщо ви хочете поміняйте обробник з іншим постачальником / реалізацією пам’яті, ви, ймовірно, повинні створити явні команди / запити, які відображають те, що ви маєте намір робити. Частіше всього для одного сервісу чи функції знадобиться щось конкретне - не створюйте загальних речей, перш ніж у вас виникне потреба.

Є , звичайно , застережень до цієї моделі - ви можете зайти надто далеко з простим механізмом паб / к південь. Я обмежив свою реалізацію лише абстрагуванням коду, пов'язаного з EF, але авантюрні розробники могли почати використовувати MediatR для того, щоб перебирати за борт і повідомляти про все - щось хороша практика перегляду коду та експертні огляди повинні вловлювати. Це проблема процесу, а не проблема з MediatR, тому просто усвідомлюйте, як ви використовуєте цей шаблон.

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


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

3

Є зусилля, яке є базовим постачальником даних бази даних об'єкта пам'яті. Я насправді не пробував цього ... Хаа щойно помітив, що це було зазначено у питанні!

Крім того, ви можете перейти на 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

Я використовував завод, щоб отримати контекст, тому я можу створити контекст, близький до його використання. Здається, це працює локально у візуальній студії, але не на моєму сервері збірки 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, тому можливо записувати речі, які проходять під час тестування за допомогою списку в пам'яті ( linkq-to-subjekti), але не виходить у вашому середовищі uat / live, коли (linq-to-sql) звикає.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.