С некоторыми наиболее распространенными языками (Java, C #, Java и т. Д.) Иногда кажется, что вы работаете вразрез с языком, когда вы хотите полностью TDD своего кода.
Например, в Java и C # вы захотите имитировать любые зависимости ваших классов, и большинство фальшивых фреймворков рекомендуют вам имитировать интерфейсы, а не классы. Это часто означает, что у вас есть много интерфейсов с одной реализацией (этот эффект еще более заметен, потому что TDD заставит вас писать большее количество меньших классов). Решения, которые позволяют правильно смоделировать конкретные классы, делают такие вещи, как изменение компилятора или переопределение загрузчиков классов и т. Д., Что довольно неприятно.
Итак, как бы выглядел язык, если бы он был разработан с нуля, чтобы быть отличным для TDD? Возможно, каким-то образом способ описания зависимостей на уровне языка (вместо передачи интерфейсов в конструктор) и возможность разделения интерфейса класса без явного указания?
Ответы:
Много лет назад я собрал прототип, который решал аналогичный вопрос; вот скриншот:
Идея заключалась в том, что утверждения соответствуют самому коду, и все тесты выполняются в основном при каждом нажатии клавиши. Поэтому, как только вы пройдете тест, вы увидите, что метод становится зеленым.
источник
Это будет динамически, а не статически типизировано. Утиная типизация будет тогда выполнять ту же работу, что и интерфейсы на статически типизированных языках. Кроме того, его классы могут быть модифицируемыми во время выполнения, чтобы тестовая среда могла легко заглушать или имитировать методы в существующих классах. Руби является одним из таких языков; rspec - это главная тестовая среда для TDD.
Как динамическая типизация помогает при тестировании
С помощью динамической типизации вы можете создавать фиктивные объекты, просто создавая класс с тем же интерфейсом (сигнатурами методов), что и объект коллектора, который вам нужно макетировать. Например, предположим, у вас был какой-то класс, который отправлял сообщения:
Допустим, у нас есть MessageSenderUser, который использует экземпляр MessageSender:
Обратите внимание на использование здесь внедрения зависимостей , основного элемента модульного тестирования. Мы вернемся к этому.
Вы хотите проверить, что
MessageSenderUser#do_stuff
звонки отправляются дважды. Как и в случае статически типизированного языка, вы можете создать фиктивный MessageSender, который подсчитывает, сколько разsend
был вызван. Но в отличие от статически типизированного языка, вам не нужен класс интерфейса. Вы просто идете вперед и создаете это:И используйте это в своем тесте:
Само по себе «типизирование по типу утки» динамически типизированного языка не так уж много добавляет к тестированию по сравнению со статически типизированным языком. Но что, если классы не закрыты, но могут быть изменены во время выполнения? Это изменит правила игры. Посмотрим как.
Что если вам не нужно использовать внедрение зависимостей, чтобы сделать класс тестируемым?
Предположим, что MessageSenderUser будет когда-либо использовать MessageSender только для отправки сообщений, и вам нет необходимости разрешать замену MessageSender каким-либо другим классом. В рамках одной программы это часто имеет место. Давайте перепишем MessageSenderUser, чтобы он просто создавал и использовал MessageSender без внедрения зависимостей.
MessageSenderUser теперь проще в использовании: никому не нужно его создавать, чтобы создать MessageSender для его использования. В этом простом примере это не выглядит большим улучшением, но теперь представьте, что MessageSenderUser создается более одного раза или имеет три зависимости. Теперь у системы есть множество случайных примеров просто для того, чтобы порадовать модульные тесты, а не потому, что это вообще обязательно улучшает дизайн.
Открытые классы позволяют тестировать без внедрения зависимостей
Тестовая среда на языке с динамической типизацией и открытыми классами может сделать TDD довольно привлекательным. Вот фрагмент кода из теста rspec для MessageSenderUser:
Вот и весь тест. Если
MessageSenderUser#do_stuff
не вызватьMessageSender#send
ровно дважды, этот тест не пройден. Настоящий класс MessageSender никогда не вызывается: мы сказали тесту, что всякий раз, когда кто-то пытается создать MessageSender, он должен вместо этого получить наш поддельный MessageSender. Внедрение зависимостей не требуется.Приятно сделать так много в таком простом тесте. Всегда приятнее не использовать внедрение зависимостей, если это не имеет смысла для вашего дизайна.
Но какое это имеет отношение к открытым классам? Обратите внимание на призыв к
MessageSender.should_receive
. Мы не определили #should_receive при написании MessageSender, так кто же это сделал? Ответ заключается в том, что тестовая среда, внося некоторые осторожные изменения в системные классы, способна заставить ее выглядеть так, как через #should_receive определен для каждого объекта. Если вы думаете, что изменение системных классов, таких как это, требует некоторой осторожности, вы правы. Но это идеальная вещь для того, что делает здесь тестовая библиотека, и открытые классы делают это возможным.источник
«хорошо работает с TDD», безусловно, недостаточно для описания языка, поэтому он может «выглядеть» как угодно. Lisp, Prolog, C ++, Ruby, Python ... выбирайте сами.
Кроме того, не ясно, что поддержка TDD лучше всего обрабатывается самим языком. Конечно, вы можете создать язык, в котором каждая функция или метод имеет связанный тест, и вы можете встроить поддержку для обнаружения и выполнения этих тестов. Но фреймворки модульного тестирования уже хорошо обрабатывают часть обнаружения и выполнения, и трудно понять, как правильно добавить требование теста для каждой функции. Тесты также нуждаются в тестах? Или есть два класса функций - обычные, которым нужны тесты, и тестовые функции, которые в них не нуждаются? Это не выглядит очень элегантно.
Может быть, лучше поддерживать TDD с помощью инструментов и платформ. Постройте это в IDE. Создайте процесс разработки, который поощряет это.
Кроме того, если вы разрабатываете язык, хорошо подумать надолго. Помните, что TDD - это всего лишь одна методология, и не каждый предпочитает работать. Это может быть трудно вообразить, но возможно, что будут еще лучшие пути. Как дизайнер языка, хотите ли вы, чтобы люди отказывались от вашего языка, когда это происходит?
Все, что вы действительно можете сказать, чтобы ответить на вопрос, - это то, что такой язык будет способствовать тестированию. Я знаю, что это мало помогает, но я думаю, что проблема в вопросе.
источник
Ну, динамически типизированные языки не требуют явных интерфейсов. Смотрите Ruby или PHP и т. Д.
С другой стороны, статически типизированные языки, такие как Java и C # или C ++, принудительно применяют типы и вынуждают вас писать эти интерфейсы.
То, что я не понимаю, в чем ваша проблема с ними. Интерфейсы являются ключевым элементом дизайна и используются во всех шаблонах проектирования и при соблюдении принципов SOLID. Я, например, часто использую интерфейсы в PHP, потому что они делают дизайн явным, а также усиливают дизайн. С другой стороны, в Ruby у вас нет возможности принудительно применить тип, это типизированный язык утки. Но все же, вы должны представить интерфейс там, и вы должны абстрагировать дизайн в своем уме, чтобы правильно реализовать его.
Итак, хотя ваш вопрос может показаться интересным, это означает, что у вас есть проблемы с пониманием или применением методов внедрения зависимостей.
И чтобы непосредственно ответить на ваш вопрос, Ruby и PHP имеют отличную инфраструктуру для моделирования, как встроенную в свои модули модульного тестирования, так и предоставляемую отдельно (см. Mockery для PHP). В некоторых случаях эти инфраструктуры даже позволяют вам делать то, что вы предлагаете, например, имитировать статические вызовы или инициализацию объектов без явного введения зависимости.
источник