Есть ли причина, по которой тесты не пишутся inline с кодом, который они тестируют?

91

Недавно я немного читал о грамотном программировании , и это заставило меня задуматься ... Хорошо написанные тесты, особенно спецификации в стиле BDD, могут лучше объяснить, что делает код, чем проза, и имеют большое преимущество проверка собственной точности.

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

Крис Деверо
источник
33
Некоторые языки программирования, такие как python с doctest, позволяют вам это делать.
Саймон Бергот
2
Вы можете почувствовать, что спецификации в стиле BDD лучше, чем проза, объясняют код, но это не значит, что комбинация двух не лучше.
JeffO
5
Половина аргументов здесь применима и к встроенной документации.
CodesInChaos
3
@Simon doctests слишком просты для серьезного тестирования, в основном потому, что они не предназначены для этого. Они были предназначены для того, чтобы иметь примеры кода в документации, которые могут быть проверены автоматически. Теперь некоторые люди используют их и для модульного тестирования, но в последнее время (как и в прошлые годы) это заняло много проблем, потому что оно имеет тенденцию заканчиваться хрупкими беспорядками, чрезмерно многословной «документацией» и другими беспорядками.
7
Проектирование по контракту позволяет использовать встроенные спецификации, которые упрощают тестирование.
Фурманатор

Ответы:

89

Единственное преимущество, которое я могу придумать для встроенных тестов, - это уменьшение количества записываемых файлов. С современными IDE это действительно не так уж важно.

Однако существует ряд очевидных недостатков встроенного тестирования:

  • Это нарушает разделение интересов . Это может быть дискуссионным, но для меня функциональность тестирования - это другая ответственность, чем ее реализация.
  • Вам придется либо вводить новые языковые функции, чтобы различать тесты / реализации, либо вы рискуете размыть грань между ними.
  • С большими исходными файлами труднее работать: труднее читать, труднее понять, вам, скорее всего, придется иметь дело с конфликтами контроля источников.
  • Я думаю, что было бы труднее надеть вашу шляпу "тестера", так сказать. Если вы посмотрите на детали реализации, у вас возникнет соблазн пропустить реализацию определенных тестов.
vaughandroid
источник
9
Это интересно. Полагаю, преимущество, которое я вижу, состоит в том, что, когда у вас есть «кодер», вы хотите думать о тестах, но хорошо, что обратное неверно.
Крис Деверо
2
Кроме того, возможно (и, возможно, желательно), чтобы один человек создавал тесты, а второй - для реализации кода. Помещение тестов в линию делает это более трудным.
Джим Натт
6
понизил бы, если бы мог. Как это вообще может быть ответом? Реализаторы не пишут тесты? Люди пропускают тесты, если они смотрят на детали реализации? «Просто слишком сложно» Конфликты на больших файлах ?? И каким образом тест можно спутать с деталями реализации ???
Бхарал
5
@bharal Кроме того, по отношению к "Просто слишком трудно", мазохизм является добродетелью дурака. Я хочу, чтобы все было легко, за исключением проблемы, которую я на самом деле пытаюсь решить.
deworde
3
Модульный тест можно считать документацией. Это предполагает, что модульные тесты должны быть включены в код по той же причине, что и комментарии, - чтобы улучшить читаемость. Однако проблема заключается в том, что, как правило, много юнит-тестов и много накладных расходов на реализацию тестов, которые не определяют ожидаемые результаты. Даже комментарии в коде должны быть краткими, с большими пояснениями, удаленными из пути - в блок комментариев вне функции, в отдельный файл или, возможно, в проектный документ. Модульные тесты являются IMO редко, если когда-либо достаточно короткими, чтобы держать в тестируемом коде как комментарии.
Steve314
36

Я могу думать о некоторых:

  • Читаемость. Встраивание «реального» кода и тестов затруднит чтение реального кода.

  • Код наворочен. Смешение «реального» кода и тестового кода в одни и те же файлы / классы / что угодно может привести к получению больших скомпилированных файлов и т. Д. Это особенно важно для языков с поздним связыванием.

  • Возможно, вы не хотите, чтобы ваши клиенты / клиенты видели ваш тестовый код. (Мне не нравится эта причина ... но если вы работаете над проектом с закрытым исходным кодом, тестовый код вряд ли поможет клиенту в любом случае.)

Теперь есть возможные обходные пути для каждой из этих проблем. Но ИМО, проще не идти туда в первую очередь.


Стоит заметить, что в первые дни Java-программисты делали подобные вещи; например, включение main(...)метода в класс для облегчения тестирования. Эта идея почти полностью исчезла. В отрасли принято внедрять тесты отдельно, используя какой-либо фреймворк.

Стоит также отметить, что Literate Programming (как задумал Кнут) никогда не завоевывал популярность в индустрии разработки программного обеспечения.

Стивен С
источник
4
+1 Проблемы с читабельностью - тестовый код может быть пропорционально больше кода реализации, особенно в проектах ОО.
Fuhrmanator
2
+1 за указание на использование тестовых фреймворков. Я не могу представить, чтобы использовать хороший тестовый фреймворк одновременно с рабочим кодом.
joshin4colours
1
RE: Возможно, вы не хотите, чтобы ваши клиенты / клиенты видели ваш тестовый код. (Мне не нравится эта причина ... но если вы работаете над проектом с закрытым исходным кодом, тестовый код вряд ли поможет клиенту в любом случае.) - Может быть желательно запустить тесты на клиентской машине. Выполнение тестов может помочь быстро определить, в чем заключается проблема, и идентифицировать различия в
среде
1
@sixtyfootersdude - это довольно необычная ситуация. И, предполагая, что вы разрабатываете закрытый исходный код, вы не захотите включать ваши тесты в стандартный бинарный дистрибутив на всякий случай. (Вы должны создать отдельный пакет, содержащий тесты, которые вы хотите, чтобы заказчик выполнил.)
Стивен С.
1
1) Вы пропустили первую часть моего ответа, где я назвал три фактические причины? Там была какая-то «критическая мысль» ... 2) Вы пропустили вторую часть, где я сказал, что Java-программисты раньше делали это, но сейчас нет? И очевидный вывод, что программисты перестали это делать ... по уважительной причине?
Стивен С
14

На самом деле, вы можете думать о дизайне по контракту как об этом. Проблема в том, что большинство языков программирования не позволяют вам писать код, подобный следующему :( Очень легко протестировать предварительные условия вручную, но условия публикации являются реальной проблемой без изменения способа написания кода (огромное отрицательное значение для IMO).

У Майкла Фезерса есть презентация на эту тему, и он упоминает, что это один из многих способов улучшить качество кода.

Даниэль Каплан
источник
13

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

Создание: тесты и код могут быть написаны в разное время разными людьми.

Контроль: если для определения требований используются тесты, вы, безусловно, хотите, чтобы они подчинялись другим правилам о том, кто может их изменить и когда, чем в действительности код.

Возможность повторного использования: если вы поместите тесты в строку, вы не сможете использовать их с другим фрагментом кода.

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

Возможность выбора. Хранение тестов отдельно от кода облегчает выбор тестов, которые вы хотите запустить.

Например, у вас может быть небольшой набор тестов, которые относятся только к коду, над которым вы сейчас работаете, и большой набор, который тестирует весь проект.

Калеб
источник
Я озадачен вашими причинами: TDD уже говорит, что создание теста происходит раньше (или одновременно) как производственный код, и должно выполняться одним и тем же кодером! Они также намекают, что тесты очень похожи на требования. Конечно, эти возражения не применимы, если вы не подписаны на догму TDD (что было бы приемлемо, но вы должны прояснить это!). Кроме того, что именно является «многоразовым» тестом? Не являются ли тесты по определению специфичными для кода, который они тестируют?
Андрес Ф.
1
@AndresF. Нет, тесты не являются специфическими для кода, который они тестируют; они специфичны для поведения, которое они проверяют. Итак, скажем, у вас есть модуль Widget с полным набором тестов, которые проверяют, что Widget работает правильно. Ваш коллега предлагает BetterWidget, который делает то же самое, что и Widget, но в три раза быстрее. Если тесты для Widget встроены в исходный код Widget таким же образом, как Literate Programming встраивает документацию в исходный код, вы не можете очень хорошо применить эти тесты к BetterWidget, чтобы убедиться, что он ведет себя так же, как Widget.
Калеб
@AndresF. Не нужно указывать, что вы не следуете TDD. это не космический дефолт. Что касается точки повторного использования. При тестировании системы вы заботитесь о входах и выходах, а не о внутренних элементах. Когда вам необходимо создать новую систему, которая ведет себя точно так же, но реализована по-другому, здорово иметь тесты, которые можно запускать как на старой, так и на новой системе. это случалось со мной не раз, иногда вам нужно работать над новой системой, пока старая все еще работает, или даже запускать их рядом. Посмотрите, как Facebook тестировал «реагирующее волокно» с помощью тестов реагирования, чтобы достичь паритета.
user1852503
10

Вот некоторые дополнительные причины, которые я могу придумать:

  • наличие тестов в отдельной библиотеке облегчает связывание только этой библиотеки с вашей средой тестирования, а не с рабочим кодом (этого может избежать какой-то препроцессор, но зачем создавать такую ​​вещь, когда более простое решение - писать тесты в отдельное место)

  • тесты функции, класса, библиотеки обычно пишутся с точки зрения «пользователей» (пользователя этой функции / класса / библиотеки). Такое «использование кода» обычно пишется в отдельном файле или библиотеке, и тест может быть более понятным или «более реалистичным», если он имитирует эту ситуацию.

Док Браун
источник
5

Если бы тесты были встроены, было бы необходимо удалить код, необходимый для тестирования, когда вы отправляете продукт своему клиенту. Таким образом, дополнительное место, где вы храните свои тесты, просто разделяет код, который вам нужен, и код, который нужен вашему клиенту .

MHR
источник
9
Не возможно. Это потребует дополнительной фазы предварительной обработки, так же как и LP. Это можно легко сделать на языке C или, например, на языке compile-to-js.
Крис Деверо
+1 за указание на это мне. Я отредактировал свой ответ, чтобы представить это.
13:30
Есть также предположение, что размер кода имеет значение в каждом случае. То, что это имеет значение в некоторых случаях, не означает, что это важно во всех случаях. Существует множество сред, в которых программисты не стремятся оптимизировать размер исходного кода. Если бы это было так, они бы не создавали так много классов.
zumalifeguard
5

Эта идея просто сводится к методу «Self_Test» в контексте объектно-ориентированного или объектно-ориентированного проектирования. При использовании скомпилированного объектно-ориентированного языка, такого как Ada, весь код самопроверки будет помечен компилятором как неиспользованный (никогда не вызванный) во время производственной компиляции, и, следовательно, все это будет оптимизировано - ничего из этого не появится в получившийся исполняемый файл.

Использование метода «Self_Test» - очень хорошая идея, и если бы программисты действительно интересовались качеством, они бы все это делали. Однако одна важная проблема заключается в том, что метод «Self_Test» должен иметь строгую дисциплину, поскольку он не может получить доступ к каким-либо деталям реализации и вместо этого должен полагаться только на все другие опубликованные методы в спецификации объекта. Очевидно, что в случае сбоя самопроверки реализация должна будет измениться. Самопроверка должна строго проверять все опубликованные свойства методов объекта, но никогда не полагаться ни на какие детали какой-либо конкретной реализации.

Объектно-ориентированные и объектно-ориентированные языки часто предоставляют именно этот тип дисциплины в отношении методов, внешних по отношению к тестируемому объекту (они обеспечивают реализацию спецификации объекта, предотвращая любой доступ к деталям его реализации и вызывая ошибку компиляции, если обнаружена любая такая попытка). ). Но все внутренние методы объекта имеют полный доступ к каждой детали реализации. Таким образом, метод самотестирования находится в уникальной ситуации: он должен быть внутренним методом по своей природе (очевидно, что самотестирование - это метод тестируемого объекта), но ему необходимо получить всю дисциплину компилятора внешнего метода ( он должен быть независим от деталей реализации объекта). Немногие, если какие-либо языки программирования предоставляют возможность дисциплинировать объект внутренний метод, как если бы это был внешний метод. Так что это важный вопрос дизайна языка программирования.

При отсутствии надлежащей поддержки языка программирования лучший способ сделать это - создать объект-компаньон. Другими словами, для каждого объекта, который вы кодируете (назовем его «Big_Object»), вы также создаете второй объект-компаньон, имя которого состоит из стандартного суффикса, объединенного с именем «реального» объекта (в данном случае, «Big_Object_Self_Test»). "), и чья спецификация состоит из одного метода (" Big_Object_Self_Test.Self_Test (This_Big_Object: Big_Object) return Boolean; "). В этом случае объект-компаньон будет зависеть от спецификации основного объекта, и компилятор будет полностью применять всю дисциплину этой спецификации в отношении реализации объекта-компаньона.

commenter8
источник
4

Это происходит в ответ на большое количество комментариев, в которых говорится, что встроенные тесты не выполняются, поскольку трудно или невозможно удалить тестовый код из сборок выпуска. Это неправда. Почти все компиляторы и ассемблеры уже поддерживают это, скомпилированные языки, такие как C, C ++, C #, это делается с помощью так называемых директив компилятора.

В случае c # (я считаю, что и c ++, синтаксис может немного отличаться в зависимости от того, какой компилятор вы используете), это то, как вы можете это сделать.

#define DEBUG //  = true if c++ code
#define TEST /* can also be defined in the make file for c++ or project file for c# and applies to all associated .cs/.cpp files */

//somewhere in your code
#if DEBUG
// debug only code
#elif TEST
// test only code
#endif

Поскольку при этом используются директивы компилятора, код не будет существовать в исполняемых файлах, которые создаются, если флаги не установлены. Это также способ заставить программы «один раз написать, дважды скомпилировать» для нескольких платформ / оборудования.

Джон
источник
2

Мы используем встроенные тесты с нашим кодом Perl. Есть модуль Test :: Inline , который генерирует тестовые файлы из встроенного кода.

Я не особенно хорош в организации моих тестов, и нашел, что они легче и с большей вероятностью будут поддерживаться, когда встроены

Отвечая на пару вопросов, поднятых:

  • Встроенные тесты написаны в разделах POD, поэтому они не являются частью реального кода. Интерпретатор игнорирует их, поэтому код не раздувается.
  • Мы используем Vim fold для скрытия разделов теста. Единственное, что вы видите, - это одна строка над каждым тестируемым методом +-- 33 lines: #test----. Когда вы хотите работать с тестом, вы просто расширяете его.
  • Модуль Test :: Inline «компилирует» тесты в обычные TAP-совместимые файлы, чтобы они могли сосуществовать с традиционными тестами.

Для справки:

MLA
источник
1

Erlang 2 действительно поддерживает встроенные тесты. Любое логическое выражение в коде, которое не используется (например, присвоено переменной или передано), автоматически рассматривается как тест и оценивается компилятором; если выражение ложно, код не компилируется.

Марк Рендл
источник
1

Другая причина разделения тестов состоит в том, что вы часто используете дополнительные или даже другие библиотеки для тестирования, чем для реальной реализации. Если вы смешиваете тесты и реализацию, компилятор не может отловить случайное использование библиотек тестов в реализации.

Кроме того, тесты, как правило, содержат больше строк кода, чем тестируемых частей реализации, поэтому вам будет сложно найти реализацию между всеми тестами. :-)

Ханс-Петер Стёрр
источник
0

Это не правда Гораздо лучше размещать свои юнит-тесты рядом с производственным кодом, когда производственный код особенно, когда производственная рутина чиста.

Например, если вы разрабатываете под .NET, вы можете поместить свой тестовый код в рабочую сборку, а затем использовать Scalpel, чтобы удалить их перед отправкой.

zumalifeguard
источник