Как бы выглядел новый язык, если бы он был разработан с нуля, чтобы быть простым для 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 - полезная практика , и один из моментов Хикки заключается в том, что только потому, что вы тестируете, не означает, что вы можете перестать думать .
Фрэнк Шиарар

Test Driven Development - это разработка правильного внутреннего и внешнего API , и делайте это заранее. Поэтому в Java это является все об интерфейсах - фактические классы побочных продуктов.

Ответы:


6

Много лет назад я собрал прототип, который решал аналогичный вопрос; вот скриншот:

Тестирование нулевой кнопки

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


2
Хаха, это потрясающе! Мне на самом деле очень нравится идея поставить тесты вместе с кодом. Это довольно утомительно (хотя на то есть веские причины) в .NET иметь отдельные сборки с параллельными пространствами имен для модульных тестов. Это также облегчает рефакторинг, потому что перемещение кода автоматически перемещает тесты: P
Джефф

Но вы хотите оставить там тесты? Вы бы оставили их включенными для производственного кода? Возможно, они могут быть # ifdef'd для C, в противном случае мы смотрим на хиты размера кода / времени выполнения.
Mawg говорит восстановить Monica

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

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 определен для каждого объекта. Если вы думаете, что изменение системных классов, таких как это, требует некоторой осторожности, вы правы. Но это идеальная вещь для того, что делает здесь тестовая библиотека, и открытые классы делают это возможным.


Отличный ответ! Вы, ребята, начинаете рассказывать мне о динамических языках :). Я думаю, что ключевой момент здесь является утиная типизация, трюк с .new, возможно, также будет использоваться в статически типизированном языке (хотя это будет гораздо менее элегантно).
Джефф

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 для PHP). В некоторых случаях эти инфраструктуры даже позволяют вам делать то, что вы предлагаете, например, имитировать статические вызовы или инициализацию объектов без явного введения зависимости.


1
Я согласен, что интерфейсы великолепны и являются ключевым элементом дизайна. Тем не менее, в моем коде я нахожу, что 90% классов имеют интерфейс и что есть только две реализации этого интерфейса, класс и макеты этого класса. Хотя технически в этом и заключается смысл интерфейсов, я не могу не чувствовать, что это не элегантно.
Джефф

Я не очень знаком с имитацией в Java и C #, но насколько я знаю, имитируемый объект имитирует реальный объект. Я часто делаю внедрение зависимостей, используя параметр типа объекта и вместо этого отправляя макет методу / классу. Что-то вроде функции someName (AnotherClass $ object = null) {$ this-> anotherObject = $ object? : новый AnotherClass; } Это часто используемый прием для внедрения зависимости без использования интерфейса.
Паткос Чаба

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