Як з’єднати тест об'єкта із запитами до бази даних


153

Я чув, що тестування одиниць "абсолютно приголомшливе", "дуже здорово" і "всілякі добрі речі", але 70% або більше моїх файлів передбачають доступ до бази даних (деякі читають, а деякі пишуть), і я не впевнений, як написати одиничний тест для цих файлів.

Я використовую PHP та Python, але, думаю, це питання, яке стосується більшості / всіх мов, які використовують доступ до бази даних.

Відповіді:


82

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

Для того, щоб налаштувати ваші об'єкти для глузування, вам, ймовірно, потрібно використовувати якусь інверсію схеми введення контролю / залежності, як у наступному псевдокоді:

class Bar
{
    private FooDataProvider _dataProvider;

    public instantiate(FooDataProvider dataProvider) {
        _dataProvider = dataProvider;
    }

    public getAllFoos() {
        // instead of calling Foo.GetAll() here, we are introducing an extra layer of abstraction
        return _dataProvider.GetAllFoos();
    }
}

class FooDataProvider
{
    public Foo[] GetAllFoos() {
        return Foo.GetAll();
    }
}

Тепер у вашому тестовому модулі ви створюєте макет FooDataProvider, який дозволяє викликати метод GetAllFoos без фактичного попадання в базу даних.

class BarTests
{
    public TestGetAllFoos() {
        // here we set up our mock FooDataProvider
        mockRepository = MockingFramework.new()
        mockFooDataProvider = mockRepository.CreateMockOfType(FooDataProvider);

        // create a new array of Foo objects
        testFooArray = new Foo[] {Foo.new(), Foo.new(), Foo.new()}

        // the next statement will cause testFooArray to be returned every time we call FooDAtaProvider.GetAllFoos,
        // instead of calling to the database and returning whatever is in there
        // ExpectCallTo and Returns are methods provided by our imaginary mocking framework
        ExpectCallTo(mockFooDataProvider.GetAllFoos).Returns(testFooArray)

        // now begins our actual unit test
        testBar = new Bar(mockFooDataProvider)
        baz = testBar.GetAllFoos()

        // baz should now equal the testFooArray object we created earlier
        Assert.AreEqual(3, baz.length)
    }
}

Поширений глузливий сценарій, у двох словах. Звичайно, ви все ще захочете перевірити свої фактичні дзвінки до бази даних, для чого вам потрібно буде натиснути на базу даних.


Я знаю, що це старе, але як бути зі створенням дублікату таблиці до тієї, яка вже є в БД. Таким чином ви можете підтвердити роботу дзвінків DB?
бретель

1
Я використовую PDO PHP як найнижчий рівень доступу до бази даних, за допомогою якого я витягнув інтерфейс. Потім я створив шар баз даних про додаток. Це шар, який містить всі необроблені запити SQL та іншу інформацію. Решта програми взаємодіє з цією базою даних вищого рівня. Я виявив, що це працює досить добре для тестування одиниць; Я перевіряю свої сторінки додатків на те, як вони взаємодіють із базою даних додатків. Я перевіряю свою базу даних додатків на те, як вона взаємодіє з PDO. Я припускаю, що PDO працює без помилок. Вихідний код: manx.codeplex.com
легалізувати

1
@bretterer - Створення копії таблиці добре для тесту на інтеграцію. Для тестування одиниць ви зазвичай використовуєте макетний об'єкт, який дозволить протестувати одиницю коду незалежно від бази даних.
BornToCode

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

2
@ bmay2 Ви не помиляєтесь. Моя оригінальна відповідь була написана давно (9 років!), Коли багато людей не писали свій код перевіреним способом, і коли інструментів для тестування сильно бракувало. Я б більше не рекомендував такий підхід. Сьогодні я просто створив би тестову базу даних і заповнив її даними, необхідними для тесту, та / або спроектував свій код, щоб я міг перевірити якомога більше логіки без бази даних взагалі.
Doug R

25

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

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

У своїх проектах c # я використовую NHibernate з повністю відокремленим шаром даних. Мої об'єкти живуть в базовій моделі домену та доступні з мого шару програми. Прикладний рівень спілкується з рівнем даних та рівнем моделі домену.

Шар програми також іноді називають "бізнес-шаром".

Якщо ви використовуєте PHP, створіть специфічний набір класів для ТІЛЬКИ доступу до даних. Переконайтесь, що ваші об'єкти не мають уявлення про їх збереження та з'єднайте їх у ваших класах додатків.

Іншим варіантом буде використання глузування / заглушок.


Я завжди погоджувався з цим, але на практиці через терміни і "добре, тепер додайте ще одну особливість, до 14:00 сьогодні" - це одна з найскладніших речей, яку можна досягти. Така річ є головною ціллю рефакторингу, хоча, якщо мій бос коли-небудь вирішить, він не подумав про 50 нових проблем, які потребують нової бізнес-логіки та таблиць.
Даррен Рінгер

3
Якщо ваші об'єкти щільно з'єднані з вашим шаром даних, важко зробити належне тестування одиниць. перша частина одиничного тесту - це "одиниця". Усі блоки повинні бути випробувані ізольовано. приємне пояснення
Amitābha

11

Найпростіший спосіб перевірити об'єкт з доступом до бази даних - це використання діапазонів транзакцій.

Наприклад:

    [Test]
    [ExpectedException(typeof(NotFoundException))]
    public void DeleteAttendee() {

        using(TransactionScope scope = new TransactionScope()) {
            Attendee anAttendee = Attendee.Get(3);
            anAttendee.Delete();
            anAttendee.Save();

            //Try reloading. Instance should have been deleted.
            Attendee deletedAttendee = Attendee.Get(3);
        }
    }

Це поверне стан бази даних, в основному як відкат транзакцій, щоб ви могли запустити тест стільки разів, скільки захочете, без будь-яких побічних ефектів. Ми успішно використовували цей підхід у великих проектах. Наші збірки потребують небагато часу (15 хвилин), але це не жахливо, якщо пройдуть 1800 одиничних тестів. Крім того, якщо час збирання викликає занепокоєння, ви можете змінити процес збирання, щоб мати кілька збірок, один для створення src, інший, який після цього запускається, який обробляє одиничні тести, аналіз коду, упаковку тощо ...


1
+1 Економить багато часу під час тестування одиниць шарів доступу до даних. Зауважте лише, що TS часто потребуватиме MSDTC, що може бути небажаним (залежно від того, чи знадобиться вашому додатку MSDTC)
StuartLC

Оригінальне запитання стосувалося PHP, здається, що цей приклад є C #. Середовища дуже різні.
легалізувати

2
Автор запитання заявив, що це загальне питання, що стосується всіх мов, які мають щось спільне з БД.
Ведран

9
і це дорогі друзі, називається інтеграційними тестами
АА.

10

Можливо, я можу дати вам смак нашого досвіду, коли ми почали дивитися на тестування підрозділу нашого середнього рівня, який включав тонну «бізнес-логіку» sql-операцій.

Спочатку ми створили рівень абстракції, який дозволив нам «прорізати» будь-яке розумне підключення до бази даних (у нашому випадку ми просто підтримували єдине з'єднання типу ODBC).

Як тільки це відбулося, ми змогли зробити щось подібне у нашому коді (ми працюємо в C ++, але я впевнений, що ви зрозуміли цю ідею):

GetDatabase (). ExecuteSQL ("ВСТАВЛЯЙТЕ в foo (bla, blah)")

У звичайний час роботи GetDatabase () повертає об'єкт, який подає всі наші sql (включаючи запити), через ODBC безпосередньо в базу даних.

Потім ми почали розглядати бази даних в пам'яті - найкращим, здається, є SQLite. ( http://www.sqlite.org/index.html ). Налаштування та використання надзвичайно просто, і дозволило нам підклас та переопределити GetDatabase () для пересилання sql до бази даних пам'яті, яка була створена та знищена для кожного проведеного тесту.

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

Загалом, це дуже допомогло в нашому TDD-процесі, оскільки внесення змін до виправлення певних помилок може мати досить дивні наслідки для інших (важко виявлених) областей вашої системи - через саму природу sql / баз даних.

Очевидно, що наш досвід був орієнтований на середовище розробки C ++, проте я впевнений, що ви могли б отримати щось подібне, працюючи під PHP / Python.

Сподіваюся, це допомагає.


9

Вам слід глузувати доступ до бази даних, якщо ви хочете перевірити свої класи. Зрештою, ви не хочете перевіряти базу даних в одиничному тесті. Це був би тест на інтеграцію.

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


6

У книзі xUnit Test Patterns описуються деякі способи обробки коду тестування одиниць, який потрапляє в базу даних. Я погоджуюся з іншими людьми, які говорять, що ти не хочеш цього робити, тому що це повільно, але ти повинен це робити колись, ІМО. Вилучення db-підключення для тестування матеріалів вищого рівня - хороша ідея, але ознайомтесь із цією книгою щодо пропозицій щодо речей, які ви можете зробити для взаємодії з фактичною базою даних.


4

Ваші параметри:

  • Напишіть сценарій, який видалить базу даних перед початком тестування одиниць, потім заповніть db заздалегідь заданим набором даних та запустіть тести. Ви також можете зробити це перед кожним тестом - це буде повільно, але менш схильним до помилок.
  • Введіть базу даних. (Приклад у псевдо-Java, але стосується всіх мов OO)

    клас баз даних {
     загальнодоступний запит результатів (String query) {... real db here ...}
    }

    клас MockDatabase розширює Базу даних { загальнодоступний запит результатів (рядковий запит) { повернути "макетний результат"; } }

    клас ObjectThatUsesDB { public ObjectThatUsesDB (база даних db) { this.database = db; } }

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

  • Не використовуйте БД взагалі протягом більшої частини коду (це все одно погана практика). Створіть об’єкт "база даних", який замість повернення з результатами поверне нормальні об'єкти (тобто повернеться Userзамість кортежу {name: "marcin", password: "blah"}), напишіть усі ваші тести за допомогою спеціальних побудованих реальних об'єктів і напишіть один великий тест, який залежить від бази даних, що забезпечує це перетворення працює добре.

Звичайно, ці підходи не є взаємовиключними, і ви можете їх змішувати та співставляти, як вам потрібно.


3

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

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

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

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

Вибачте, у мене немає конкретних прикладів коду для PHP / Python, але якщо ви хочете побачити приклад .NET, у мене є повідомлення, в якому описана методика, яку я використовував для цього самого тестування.


2

Зазвичай я намагаюся розбити свої тести між тестуванням об'єктів (і ORM, якщо такі є) і тестуванням db. Я перевіряю об'єктну сторону речей, знущаючись над дзвінками доступу до даних, тоді як я тестую сторону db речей, тестуючи взаємодію об'єкта з db, яка, на мій досвід, зазвичай досить обмежена.

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


2

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

Проста форма IoC може бути виконана просто за допомогою кодування до інтерфейсів. Для цього потрібна якась орієнтація на об’єкт, що відбувається у вашому коді, тому це може не стосуватися того, що ви робите (я кажу, що оскільки я все повинен продовжувати - це ваша згадка про PHP та Python)

Сподіваємось, це корисно, якщо ніщо інше у вас зараз немає пошукових термінів.


2

Я погоджуюся з першим повідомленням - доступ до бази даних повинен бути позбавлений шару DAO, який реалізує інтерфейс. Тоді ви можете перевірити свою логіку на застосуванні заглушки шару DAO.


2

Ви можете використовувати глузуючі рамки для вилучення двигуна бази даних. Я не знаю, чи є PHP / Python, але для введених мов (C #, Java тощо) є багато варіантів

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


2

Налаштування даних тестів для одиничних тестів може бути складним завданням.

Що стосується Java, якщо ви використовуєте Spring API для тестування одиниць, ви можете контролювати транзакції на рівні одиниці. Іншими словами, ви можете виконати тестові одиниці, які передбачають оновлення / вставки / видалення бази даних та відкат змін. В кінці виконання ви залишаєте все в базі даних, як було до початку виконання. Для мене це так добре, як може отримати.

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