Як би виглядала нова мова, якби її розробили з нуля, щоб було легко TDD?


9

З деякими найпоширенішими мовами (Java, C #, Java тощо) іноді здається, що ви працюєте в розбіжності з мовою, коли хочете повністю TDD-код.

Наприклад, у Java та C # ви захочете знущатися над будь-якими залежностями своїх класів, і більшість глузуючих фреймворків рекомендують вам знущатися з інтерфейсів, а не з класів. Це часто означає, що у вас є багато інтерфейсів з однією реалізацією (цей ефект ще більш помітний, оскільки TDD змусить вас написати більшу кількість менших класів). Рішення, які дозволяють вам глузувати з конкретних класів належним чином, роблять такі речі, як зміна компілятора або перезавантаження навантажувачів класів тощо, що дуже неприємно.

То як би виглядала мова, якби вона була розроблена з нуля, щоб бути чудовою для TDD? Можливо, якимось чином мовний рівень опису залежностей (а не передача інтерфейсів конструктору) та можливість розділити інтерфейс класу, не роблячи це явно?


Як щодо мови, яка не потребує TDD? blog.8thlight.com/uncle-bob/2011/10/20/Simple-Hickey.html
робота

2
TDD не потребує мови . TDD є корисною практикою , і одне з пунктів Хікі - це те, що тестування не означає, що ви можете перестати думати .
Френк Ширар

Тестова розробка полягає в тому, щоб отримати внутрішній та зовнішній API права, і зробіть це наперед. Отже, у Java мова йде про інтерфейси - фактичні класи є побічними продуктами.

Відповіді:


6

Багато років тому я зібрав прототип, який вирішив подібне питання; ось скріншот:

Тестування нульової кнопки

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


2
Ха-ха, це дивовижно! Мені насправді дуже подобається ідея складати тести разом з кодом. Це досить нудно (хоча є дуже вагомі причини) .NET має окремі збірки з паралельними просторами імен для одиничних тестів. Це також полегшує рефакторинг, оскільки переміщуючи код, автоматично переміщується тести: P
Geoff

Але ви хочете залишити тести там? Чи залишите їх увімкненими для виробничого коду? Можливо, вони можуть бути # ifdef'd для C, інакше ми переглядаємо хіти розміру коду / час виконання.
Мауг каже, що повернемо Моніку

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

5

Це було б динамічно, а не статично типізовано. Введення качок робило б ту саму роботу, що і інтерфейси на мовах, що мають статичний тип. Крім того, його класи можуть бути модифіковані під час виконання, щоб тестовий фреймворк міг легко заглушити або знущатися над методами існуючих класів. Рубі - одна з таких мов; rspec - це її основна тестова основа для TDD.

Як динамічне тестування допоміжних засобів тестування

За допомогою динамічного введення тексту ви можете створювати макетні об’єкти, просто створивши клас, який має той самий інтерфейс (підписи методу) об'єкта-колаборатора, з якого потрібно знущатися. Наприклад, припустимо, у вас був клас, який надсилав повідомлення:

class MessageSender
  def send
    # Do something with a side effect
  end
end

Скажімо, у нас є MessageSenderUser, який використовує екземпляр MessageSender:

class MessageSenderUser

  def initialize(message_sender)
    @message_sender = message_sender
  end

  def do_stuff
    ...
    @message_sender.send
    ...
    @message_sender.send
    ...
  end

end

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

Ви хочете перевірити, що MessageSenderUser#do_stuffдзвінки надсилаються двічі. Так само, як і в статично набраній мові, ви можете створити макет MessageSender, який підраховує, скільки разів sendдзвонили. Але на відміну від статично набраної мови, вам не потрібен клас інтерфейсу. Ви просто продовжуйте створювати його:

class MockMessageSender

  attr_accessor :send_count

  def initialize
    @send_count = 0
  end

  def send
    @send_count += 1
  end

end

І використовуйте його у своєму тесті:

mock_sender = MockMessageSender.new
MessageSenderUser.new(mock_sender).do_stuff
assert_equal(mock_sender.send_count, 2)

Само по собі "набирання качок" мови, що динамічно набирається, не надто додає тестування порівняно зі статично набраною мовою. Але що робити, якщо заняття не закриті, але можуть бути змінені під час виконання? Це зміна гри. Подивимося, як.

Що робити, якщо вам не довелося використовувати ін'єкцію залежності, щоб зробити клас перевіряючим?

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

class MessageSenderUser

  def initialize
    @message_sender = MessageSender.new
  end

  def do_stuff
    ...
    @message_sender.send
    ...
    @message_sender.send
    ...
  end

end

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

Відкриті заняття дозволяють пройти тестування без введення залежності

Тестова основа на мові з динамічним набором тексту та відкритими класами може зробити TDD дуже приємним. Ось фрагмент коду з тесту rspec для MessageSenderUser:

mock_message_sender = mock MessageSender
MessageSender.should_receive(:new).and_return(mock_message_sender)
mock_message_sender.should_receive(:send).twice.with(no_arguments)
MessageSenderUser.new.do_stuff

Ось і весь тест. Якщо MessageSenderUser#do_stuffне викликати MessageSender#sendрівно два рази, цей тест не вдасться. Справжній клас MessageSender ніколи не викликається. Ми сказали тесту, що коли хтось намагається створити MessageSender, він повинен отримати замість цього насмішку MessageSender. Не потрібно вводити залежність.

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

Але що це стосується відкритих занять? Зверніть увагу на дзвінок на MessageSender.should_receive. Ми не визначали #should_receive, коли писали MessageSender, і хто це робив? Відповідь полягає в тому, що тестовий фреймворк, вносячи деякі ретельні модифікації системних класів, може зробити так, щоб через #should_receive було визначено кожен об'єкт. Якщо ви думаєте, що модифікація подібних системних класів вимагає певної обережності, ви маєте рацію. Але це ідеальна річ для того, що тут робить тестова бібліотека, і відкриті класи дозволяють це зробити.


Чудова відповідь! Ви, хлопці, починаєте розмовляти зі мною на динамічних мовах :) Я думаю, що тут ключовим є введення качки, трюк із.
Джефф

3

То як би виглядала мова, якби вона була розроблена з нуля, щоб бути чудовою для TDD?

"добре працює з TDD", безумовно, недостатньо для опису мови, тому вона може "виглядати" як що завгодно. Lisp, Prolog, C ++, Ruby, Python ... прийміть свій вибір.

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

Можливо, краще підтримати TDD за допомогою інструментів та рамок. Вбудуйте його в IDE. Створіть процес розвитку, який його заохочує.

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

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


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

0

Ну а динамічно набрані мови не вимагають явних інтерфейсів. Див. Ruby або PHP тощо.

З іншого боку, статично типізовані мови, такі як Java та C # або C ++, застосовують типи та змушують писати ці інтерфейси.

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

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

І щоб безпосередньо відповісти на ваше запитання, у Ruby та PHP є чудова знущальна інфраструктура, як вбудована в їхні рамки тестування модулів, так і поставлені окремо (див. Mockery for PHP). У деяких випадках ці рамки навіть дозволяють робити те, що ви пропонуєте, такі речі, як знущання над статичними дзвінками або ініціалізацією об'єкта без явного введення залежності.


1
Я погоджуюся, що інтерфейси - це чудовий і ключовий елемент дизайну. Однак у своєму коді я вважаю, що у 90% класів є інтерфейс і що існує лише дві реалізації цього інтерфейсу, клас та макети цього класу. Хоча це технічно саме суть інтерфейсів, я не можу не відчувати, що це неелегантно.
Джефф

Мені не дуже добре знайомі з глузуванням у Java та C #, але, наскільки я знаю, насміхнений об’єкт імітує реальний об’єкт. Я часто роблю введення залежності, використовуючи параметр типу об'єкта та надсилаючи замість метод / клас замість цього. Щось на зразок функції someName (AnotherClass $ object = null) {$ this-> anotherObject = $ об'єкт? : новий AnotherClass; } Це часто використовується трюк для введення залежності, не виходячи з інтерфейсу.
Patkos Csaba

1
Це, безумовно, динамічні мови мають перевагу перед мовами типу Java / C # стосовно мого питання. Типовий макет конкретного класу фактично створить підклас класу, це означає, що конструктор конкретного класу буде називатися, чого ви обов'язково хочете уникати (є винятки, але у них є свої проблеми). Динамічний макет просто використовує типи качок, щоб не було зв'язку між класом бетону та його макетом. Я часто кодував у Python, але це було до моїх днів TDD, можливо, саме час поглянути ще раз!
Джефф
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.