Saturday, October 16, 2010

Сверху вниз и снизу вверх. Часть IV

Cм. также
Оптимистический и пессимистический взгляд на жизнь
Сверху вниз и снизу вверх. Часть I
Сверху вниз и снизу вверх. Часть II
BFS and DFS - поиск в ширину и глубину Сверху вниз и снизу вверх. Часть III (продолжение)


В этой заметке рассмотрим технику Testing first - сначала тестировать и её связь с основной мыслью серии заметок.

Не вдаваясь в теорию приведу суть этой техники. Суть эта заключается в том, что как только мы хотим приступить к имплиментации некого модуля системы, мы должны остановится и написать сначала для него тест (unit test). Для того чтобы мы могли запустить наш unit test мы пишем по-сути вместо модуля некую заглушку. Затем на каждый вариант input-а мы пишем отдельный test case. После того, как мы покрыли все случаи мы начинаем писать собственно имлиментацию.

Адепты этого подхода видят следующие его преимущества:
а) мы должны подумать о всех случаях ещё до начала написания кода;
б) в момент написания test case-ов может быть выявлено, что этот модуль должен быть разделён на несколько подмодулей;
в) в каждый момент времени написания кода, мы можем запустить проверки и найти баги в имплиментации.

Давайте, однако, разбираться. Как я писал, есть два способа написания программы - сверху вниз и снизу вверх. Если писать программу "сверху вниз" при котором на каждом этапе мы делаем краткий обзор системы, определяя, но не детализируя любые подсистемы следующего уровня, то, возможно, такой подход имеет смысл. Так как подсистемы не детализированы, то вполне может быть, что наш "модуль" должен быть разделён как указано в пункте б). Также именно по этой причине мы явно не думали о всех возможных input-ах данного модуля, о чём говорит нам пункт а). Однако, по этим причинам значения пункта в) явно переоценено. В процессе написания кода, вдруг выясняется, что тестовое покрытие далеко неполное и должно быть расширено. В этот момент, согласно, методологии, мы должны остановить имплиментацию и вернутся к написанию тестового покрытия (test case). ИМХО, такой interupt требует колоссальных ментальных затрат (по крайней мере у меня).

Второй способ, "снизу вверх". При таком способе мы разбиваем задачу на атомы, затем под-системы первого уровня, которые соединяют атомы, затем подсистемы второго уровня, соединяющие подсистемы первого уровня и т.д. Как я писал, здесь есть два подтип. Первый подтип, как только мы идентифицировали модуль, мы его тут же имлиментируем. Т.к. мы не видим всей картины целиком, то тут все пункты перечисленные выше могут произойти. Однако, ИМХО, гораздо проще вместо подхода tesing first, сделать-таки полный дизайн, как во втором подтипе. В таком случае пункт б) (слишком крупный модуль) не должен произойти. Если он произошёл, значит мы плохо справляемся с работой, так как мы об этом уже думали. То же самое и с пунктом а) (думать о всех случаях). Пункт в) не релевантный, т.к. мы думал о нём на этапе дизайна всей системы, зачем нам дважды делать одну и ту же работу?

Таким образом при написании системы "снизу вверх" использование техники testing first явно не оправдано, нужно просто разработать полный дизайн, а не делать имплиминтацию на месте (если настаивать на последнем варианте, то такая техника имеет смысл). При написании системы "сверху вниз" эта техника "заставляет" нас делать определённые действия, в этом её несомненный плюс. Однако, эффект от его использования явно преувеличен из-за постоянных interupt-ов между имплиментацией и написанием тестового покрытия.

Последнем посте из этой серии:

5. Note first - Сначала писать комментарии.

Продолжение следует.

6 comments:

  1. Видимо, у нас очень разный опыт (в разных областях знаний, в разных командах и т.д.), поэтому данный субъективный спор о том, удобно ли начинать разработку с создания тестов, вряд ли завершится осмысленно :)

    Но я хотел бы сказать пару слов в защиту test-driven development:

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

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

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

    Такая последовательность позволяет убедиться, что внося новые изменения, команда не ломает что-то ранее починенное, что часто бывает очень критично.

    ReplyDelete
  2. Я пробовал этот подход в разных командах, всегда с отрицательным результатом. При этом для меня основной проблемой был постоянный context switch (сейчас я думаю о тесте, сейчас я думаю о коде), фактически я проделывал дважды одну и ту же работу, да и в голове всё время "застревали" несущественные детали с "параллельной" фазы, что мне лично жутко мешало думать вообще.

    Не нужно смешивать testing first от тестирования вообще. При чём тут ошибка обнаруженная заказчиком вообще? Если в проекте поддерживается unit-testing (а не везде он используется) ничего не мешает писать тесты тогда, когда удобно самому программисту, хоть до кода, хоть параллельно коду, хоть после кода.

    Ещё раз, "постоянное расширение тестовой базы проблемными случаям" не имеет отношения к импиративу testing first.

    ReplyDelete
  3. Хорошо, со вторым аргументом разобрались (хоть я и видел, как люди иногда из-за "горящих сроков" или простой лени не делают тесты после реализации функциональности, потому что "и так вроде работает").

    А что с первым аргументом? Почему бы созданием тестов не заниматься другим людям, если разработчик страдает от смены задачи?

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

    ReplyDelete
  4. В теории для этого предназначен QA. На практике, именно программист написавший код лучше всех знает как его проверить. Если использовать технику проверки white box - то, только он и может проверить (или же другой программист должен досконально разобраться в коде, что, согласитесь, будет накладно). Если использовать black box, то есть ещё теоретически 3 варианта:

    - другой программист;
    - QA
    - системный аналитик;

    Для того чтобы это был не программист, нужно иметь какую-то специально построенную системы проверок, это первое (в очень крупных фирмах их иногда делают). Второе, из моего опыта, QA с этой задачей справляется заведомо хуже из-за многих параметров, начиная с того, что у них всегда хронически нет времени, заканчивая низкой квалификацией и тем, что банально занимает много времени понять spec, в итоге разработка будет сильно тормозиться.
    Если это другой программист, то начальство в этом видит неэффективное использование ресурсов чаще всего. У меня был небольшой такой опыт, довольно много времени уходит на "притирку". После "притирки", обычно, всё равно всё скатывалось к extreme programming, когда 2 человека сидят над одним кодом, так как всегда есть баги. В итоге, экономии на этом нет, и проблема context switching всё равно не решена (т.к. нет разделения труда в итоге).
    Системный аналитик действительно может решить эту проблему. В одном из проектов, в design document нужно было описать и unit test-ы. Это решает проблему context switching, но с этим были следующие проблемы:

    - большая затрата по времени;
    - неполнота покрытия;

    Из-за большой затраты по времени, это задача выполнялась последней и часто не выполнялась вообще - design document сдавался без неё. Покрытия для white box будет всегда неполным. Однако, и для black box testing она зачастую было неполным. При том, когда эти тесты писал кто-то другой из-за неполноты покрытия я иногда неправильно выбирал как имплиментацию. Как это выглядело на практике? Времени на полное покрытие не было, поэтому писались бессистемно несколько покрытий. Я получал документ, считая, что покрытия более-менее полные. В итоге я "сильно удивлялся" когда во время имплиментации я находил целые новые классы test cases (не крайние случае).

    ReplyDelete
  5. Илья Весенний, подход testing first предназначен для того чтобы "заставить" программиста сделать проверки. Но если я их всё равно делаю, но мне это не удобно делать до написания кода, т.е. я их делаю в другом порядке, в чём проблема? Зачем говорить, что testing=testing first?

    Я просто не понимаю людей, который слепо следуют методологии, не понимая сути.

    ReplyDelete
  6. Да никто и не говорит, что testing=testing first. И слепых фанатов единственной прочитанной в своей жизни книги я тоже не очень перевариваю.

    Но и Вашу позицию в полной мере мне понять не удалось. Эти мои слова можете отнести к ясности своей заметки (я не могу согласиться или не согласиться с тем, что не понимаю). Впрочем, наверное, все мы этим страдаем - пишем на своём языке в рамках своих предположений, а не на языке читателя :)
    Спасибо за интересную беседу!

    ReplyDelete