Примечание автора: это перевод статьи Боба Мартина.
На написание этой статьи меня вдохновила статья Марка Симана «The IsNullOrWhiteSpace trap» (@ploeh). Статья Марка кратко и хорошо изложена. Пожалуйста, прочитайте сначала её, прежде чем продолжать читать данную.
Ловушка, о которой рассказывает Марк, это частный случай более общей ловушки, которую я называю воровством золота. Я могу продемонстрировать эту ловушку, возвращаясь обратно к статье Марка.

Заметьте, что первый тест, который написал Марк выглядел следующим образом:

[InlineData("Seven Lions Polarized"  , "LIONS POLARIZED SEVEN"  )]
[InlineData("seven lions polarized"  , "LIONS POLARIZED SEVEN"  )]
[InlineData("Polarized seven lions"  , "LIONS POLARIZED SEVEN"  )]
[InlineData("Au5 Crystal Mathematics", "AU5 CRYSTAL MATHEMATICS")]
[InlineData("crystal mathematics au5", "AU5 CRYSTAL MATHEMATICS")]

Он уже попал в ловушку. Почему? Потому что он уже украл золото.

Золото и тернии


Основная функциональность, которую Марк пытается описать — это упорядочение слов по алфавиту. Естественно, его тесты и отражают эту функциональность. Основная функциональность — золото и он его своровал.
Проблема в том, что золото защищено невидимой тернистой изгородью, которая опутает любого, ничего о ней не подозревающего программиста, который, будучи ослеплённым золотом, попытается его украсть. Что за тернистая изгородь? В случае Марка это null и пустая строка в качестве входных данных.
Я следую дисциплине TDD вот уже как пятнадцать лет. Я выучил многое об этой невидимой тернистой изгороди. Я выучил урок о том, что она всегда где-то рядом. Я понял, что если вы попытаетесь своровать золото слишком рано, то невидимая изгородь воспрепятствует вашему прогрессу и разорвёт ваши усилия на куски [1]. Так что, стратегия которой я научился следовать состоит в том, чтобы отвести глаза от золота на время, пока я прощупываю изгородь и расчищаю от неё путь.

Разведка и расчистка


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

Поведения-исключения


Это поведения, которые обнаруживают некорректный ввод, с которым основная функциональность никогда не должна столкнуться. Такие поведения возвращают коды ошибок, логируют ошибки и\или выбрасывают исключения.
В случае Марка, обработка null — единственное исключительное поведение. Но в более сложных приложениях, обнаружение исключительных случаев может быть гораздо более сложным. Конечно эти случаи включают обработку входных данных. Но они также включают нарушение семантики, такие как удаление несуществующей записи, или добавление записи, которая уже существует.

Дегенеративные поведения


Здесь речь идёт о входных данных, которые заставляют основную функциональность делать «ничего». Я поставил «ничего» в кавычки, потому что иногда «ничего» может быть относительно сложным.
В случае Марка, пустые строки и строки, состоящие из пробелов являются дегенеративными входными данными. В конечном счёте он решил проблему таких строк сложным набором условий и операций, которые возвращали пустую строку в случае с одним пробельным символом или пустой строкой в качестве входных данных, в остальных случаях все пробельные символы удалялись [2].
В целом, дегенеративные условия, это штуки вроде пробелов, пустых строк, пустых коллекций, массивов нулевой длины и т.д. В более сложных приложениях, дегенеративный случай может быть достаточно сложным и требовать сложной обработки. Рассмотрите, например, Java-компилятор, который обрабатывает исходные файлы, которые содержат тысячи строк, которые состоят из точек с запятыми и комментариев. Каким должен быть результат обработки?

Вспомогательные поведения


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

Все эти тесты я пишу первыми и делаю их проходящими. Я избегаю каких-либо тестов, которые близки к основной функциональности до тех пор, пока я окончательно не окружу проблему проходящими тестами, описывающими всё, кроме основной функциональности. Затем, и только затем я забираю золото.

[1] Действительно, всего лишь позавчера я потратил четыре часа, буду опутанным терниями, которые я упустил и не расчистил как следует. В конце концов git reset — hard оказался моим единственным выходом.

[2] Покрыл ли он все возможные условия? Что насчёт табуляции, переводов строк, бэкспэйсов, непечатаемых символов?

Комментарии (4)


  1. SamKrew
    06.04.2015 09:27
    -1

    Спасибо за интересную статью, но качество перевода принуждает читать оригинал.


    1. EngineerSpock Автор
      06.04.2015 10:05

      Конкретные претензии в личку, если вас не затруднит. Оригинал всегда читать лучше.


  1. eaa
    06.04.2015 17:31

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


    1. EngineerSpock Автор
      07.04.2015 08:51

      Знаю, что Мартин рекомендует её сначала прочитать, но я понимаю простую мысль Мартина и без чтения той статьи.