Немного истории
Благодаря классному дядьке Кенту Беку (Kent Beck) родилась замечательная методология test-driven development. Не смотря на необычность подхода, переворачивающего привычный процесс написания кода с ног на голову (тест на функционал создается до реализации), сейчас уже можно сказать, что разработка через тестирование стала стандартом де-факто. Практически в любых вакансиях фигурирует требование к знанию и опыту использования методики TDD и соответствующих инструментов. Почему, казалось бы, ломающая привычную парадигму мышления методология прижилась и стандартизировалась? Потому что “Жизнь слишком коротка для ручного тестирования”, а писать авто-тесты на существующий код иногда просто не возможно, ведь код, написанный в обычной парадигме, как правило совершенно тесто-не-пригодный.
Стоит отметить, что за время своего существования методология успела обзавестись ответвлением (fork) в виде BDD. Дэн Норт (Dan North) в своей статье (Introducing BDD) указал на сложности внедрения TDD среди разработчиков и для решения обозначенных проблем предложил практику, которая называется behaviour-driven development. Основной фишкой BDD можно назвать микс из TDD и DDD, которая в начале выражалась в правильном именовании тестовых методов (названия тестовых методов должны быть предложениями). Апогеем BDD, на текущий момент, можно считать рождение языка Gherkin и инструментария, который его использует (Cucumber, RSpec и т.п.).
К чему я веду и при чем тут 1С?
В мире 1С TDD только только начинает набирать популярность. Я еще не видел вакансий разработчиков 1С с требованием знания TDD. Стоит признать, что существенным препятствием является отсутствие в ядре платформы 1С инструментов для написания тестов до кода.
Так что же у нас есть на текущий момент для разработки через тестирование в мире 1С?
- xUnitFor1C — вполне себе зрелый проект, позволюящий разрабатывать в стиле TDD.
- Vanessa-behavoir — спецификации на языке Gherkin и т.п., пока что не в релизном состоянии.
А теперь вопрос, который должен возникать у любого уважающего себя члена общества: “Как лично я могу помочь… (в моем случае — миру 1С разработки перейти на передовые методологии)?”.
Прежде чем ответить на этот вопрос, я хочу коснуться темы хорошо написанных утверждений в тестах. Утверждения обозначают ожидаемое поведение нашего кода. Одного взгляда на утверждения должно быть достаточно, чтобы понять, какое поведение тест пытается до нас донести. К сожалению, классические утверждения не позволяют этого достичь. Зачастую нам приходится долго вчитываться и расшифровывать замысел автора теста.
К счастью, в последнее время появилась тенденция к применению текучих интерфейсов (fluent interface), что очень положительно сказывается на наглядности и интуитивной понятности кода. Инструментарий для тестирования так же не остался в стороне.оявились текучие утверждения, называемые так же утверждениями в стиле BDD. Они позволяют формулировать утверждения в более естественной, удобной и выразительной манере.
Впервые я столкнулся с подобным подходом в NUnit в модели утверждений на основе ограничений (Constraint-Based Assert Model).
Много позже я познакомился со связкой mocha.js + chai.js, которая у меня вызвала полнейший восторг.
Так вот, мой ответ на вопрос “Как лично я могу помочь миру 1С разработки перейти на передовые методологии?” — текучие утверждения… для начала.
Разработка текучих утверждений для платформы 1С
Как заправский разработчик через тестирование, я начал разработку с теста. Первый тестовый метод содержал всего 1 строку:
Ожидаем.Что(5).Равно(5);
Реализация оказалась на удивление простой. Переменная Ожидаем содержит объект ВнешняяОбработка (далее объект-утверждения), у этого объекта есть экспортные методы:
- Что(ПроверяемоеЗначение) — сохраняет в контексте объекта-утверждения проверяемое значение;
- Равно(ОжидаемоеЗначение) — проверяет на равенство ранее сохраненное значение с переданным ожидаемым значением. В случае неравенства выбрасывается исключение с описанием ошибки утверждения.
Каждый метод возвращает тот же самый объект-утверждения.
Следующим шагом, сигнатура метода Что была расширена необязательным параметром Сообщение, которое делает выбрасываемые утверждениями исключения более информативными.
Далее я задумался над тем, что делать с утверждением НеРавно. Должно ли быть такое утверждение? В классических утверждениях так и есть, почти каждое утверждение имеет своего антипода (Равно/НеРавно, Заполнено/НеЗаполнено и т.д.). Но только не в текучих утверждениях! Так родился тест №2:
Ожидаем.Что(5).Не.Равно(7);
Выглядит красиво, но не реализуемо на языке 1С. Еще попытка:
Ожидаем.Что(5).Не().Равно(7);
По прежнему красиво и казалось бы реализуемо. Нужно всего лишь взвести флаг отрицания в контексте объекта-утверждения, и затем любое следующее по цепи утверждение проверять с учетом этого флага. По сути нужен был XOR, на языке 1С это выглядит вот так:
РезультатУтверждения = ФлагОтрицания <> ЛогическоеВыражениеУтверждения;
Но платформа отказалась компилировать объект с методом Не(). Дело в том, что Не — зарезервированное слово, ограничение на его использование распространяется в т.ч. и на имя метода. Мозговой штурм с коллегами не позволили красиво обойти эту проблему, поэтому финальный вариант с отрицанием выглядит так:
Ожидаем.Что(5).Не_().Равно(7);
Если кто-то предложит лучшее решение обозначенной проблемы, я буду очень признателен. Вариант замены русских букв на латиницу не предлагать!
В итоге родился следующий API
Что(ПроверяемоеЗначение, Сообщение = "") — сохраняет в контексте внешней обработки проверяемое значение и дополнительное сообщение для исключений выбрасываемых утверждениями.
Не_() — отрицает любое утверждение следующее по цепи.
ЭтоИстина() — утверждает, что проверяемое значение является Истиной.
ЭтоЛожь() — утверждает, что проверяемое значение является Ложью.
Равно(ОжидаемоеЗначение) — утверждает, что проверяемое значение равно ожидаемому.
Больше(МеньшееЗначение) — утверждает, что проверяемое значение больше, чем переданное в утверждение.
БольшеИлиРавно(МеньшееИлиРавноеЗначение) / Минимум(МинимальноеЗначение) — утверждает, что проверяемое значение больше или равно переданному в утверждение.
МеньшеИлиРавно(БольшееИлиРавноеЗначение) / Максимум(МаксимальноеЗначение) — утверждает, что проверяемое значение меньше или равно переданному в утверждение.
Меньше(БольшееЗначение) — утверждает, что проверяемое значение меньше, чем переданное в утверждение.
Заполнено() — утверждает, что проверяемое значение отличается от значения по умолчанию того же типа.
Существует() — утверждает, что проверяемое значение не Null и не Неопределено.
ЭтоНеопределено() — утверждает, что проверяемое значение это Неопределено.
ЭтоNull() — утверждает, что проверяемое значение это Null.
ИмеетТип(Тип) — утверждает, что проверяемое значение имеет переданный в утверждение тип или имя типа.
Между(НачальноеЗначение, КонечноеЗначение) — утверждает, что проверяемое значение находится между переданными в утверждение значениями.
Содержит(ИскомоеЗначение) — утверждает, что проверяемое значение содержит переданное в утверждение. Применяется для строк и коллекций.
ИмеетДлину(ОжидаемаяДлина) — утверджает, что проверяемое значение имеет длину переданную в утверждение. Применяется для строк и коллекций.
Примеры использования
Ожидаем.Что(1 > 0).ЭтоИстина();
Ожидаем.Что(13 = 2).Не_().ЭтоИстина();
Ожидаем.Что(5 = 7).ЭтоЛожь();
Ожидаем.Что(5).Равно(5);
Ожидаем.Что(4).Больше(2);
Ожидаем.Что(7).БольшеИлиРавно(7);
Ожидаем.Что(НекийМассив.Количество()).Минимум(9);
Ожидаем.Что(90).МеньшеИлиРавно(100);
Ожидаем.Что(СтрДлина(НекаяСтрока)).Максимум(90);
Ожидаем.Что(55).Меньше(56);
Ожидаем.Что(1).Заполнено();
Ожидаем.Что(Новый Массив).Существует();
Ожидаем.Что(Неопределено).ЭтоНеопределено();
Ожидаем.Что(ВыборкаИзБД.НекоеПоле).ЭтоNull();
Ожидаем.Что("").ИмеетТип("Строка");
Ожидаем.Что(7).Между(1, 10);
Ожидаем.Что("Некая строка").Содержит("стр");
Ожидаем.Что("Некая строка").ИмеетДлину(12);
Пример немного сложнее:
Ожидаем.Что("Некая строка")
.Существует()
.Не_().ИмеетТип("Число")
.ИмеетДлину(12)
.Не_().Содержит("!!!");
Послесловие
Разработка доступна на github. Как, наверное, заметил читатель с пытливым умом, ссылка ведет на нечто большее, чем просто на библиотеку утверждений. Но это уже материал для следующей статьи.
Комментарии (47)
grossws
10.06.2015 17:24+4сейчас уже можно сказать, что разработка через тестирование стала стандартом де-факто. Практически в любых вакансиях фигурирует требование к знанию и опыту использования методики TDD и соответствующих инструментов.
Это не так. TDD работает в некоторых частных случаях, но не более.artbear
10.06.2015 17:26Соглашусь с автором, что TDD довольно часто мелькает в вакансиях, но не для 1С :)
grossws
10.06.2015 17:43+1Очень сильно зависит от области деятельности. В вебе, например, вполне себе часто, в embedded — напротив. Хотя и читал недавно забавную книжку Test Driven Development for Embedded C by James W. Grenning (PragPub), в которой рассказывается как можно использовать TDD в embedded.
TimReset
10.06.2015 18:08Кстати, и как книга? Я её видел и удивился, что такое есть вообще!
grossws
10.06.2015 18:51В принципе, прочитать стоит, если интересуетесь embedded. Но принципиально нового ничего там не увидел.
Основные плюсы использования TDD в этом случае в том, что проектирование будет несколько более модульным (ради возможности тестирования, но это будет и при простом написании юнит-тестов), иметь более чёткие границы ответственности софт/железо и в возможности вести разработку на хосте пока даже дев-версия железа не появилась. Недостатки — очень большое количество стабов и/или моков (даже для простых вещей типа мигания светодиодом), более сложная сборка и т. п.
Отвечу artbear здесь же. Проблема использования TDD в embedded в том, что есть вещи для которых практически невозможно сделать нормальные стабы/моки, т. к. control flow недетерминирован (те же прерывания/события). Проблемы, в общем, схожи с тестированием асинхронных приложений. Для нормального покрытия тестами нужно делать очень много тестов.
Задача по сложности сравнимая с написанием хорошей симуляции соответствующего процессора с обвязкой. Если говорить про life-critical, то там будут делать и аппаратные эмуляторы, т. к. реальное железо сильно отличается от того, что описано в datasheet'ах (достаточно заглянуть в errata чего-нибудь более-менее распространенного и не очень нового).
Вы представляете себе человека, который будет делать тестовую ситуацию с порчей памяти при DMA-передаче память-память, если в этот момент на UART порт «удачно» пришел байт с установленным старшим битом (а заодно все другие вариации, когда этого не произошло)? Я нет. Куда реалистичнее аппаратный симулятор, который будет прогонять большое количество недетерминированных тестов, варьируя параметры.
Но для большинства задач увеличение стоимости разработки на пару-тройку порядков будет мотивацией отказаться от такого серьёзного тестирования, т. к. дешевле перепрошить телевизор/плеер/телефон/whatever в сервис-центре, если вдруг что-то такое случится.
artbear
10.06.2015 18:28Указанную книгу видел, но не читал, т.к. не работаю с embedded
А в чем проблема использования TDD с embedded?
Оптимизация памяти, ресурсов?
EvilBeaver
10.06.2015 17:27+1TDD работает в некоторых частных случаях, но не более
Прямо-таки в «некоторых частных случаях»? А в общем случае не работает вообще? Мне кажется, вы что-то перегибаете палкуgrossws
10.06.2015 17:47+1Много ли вы видели библиотек, более-менее серьезных фреймворков, движков БД, написанных с помощью TDD?
wizi4d Автор
10.06.2015 17:58-2Например, github.com/JetBrains/intellij-community
Не берусь утверждать, насколько тру-TDD используется, но количество авто-тестов большое.grossws
10.06.2015 18:30+4Откуда вы почерпнули, что они используют TDD? Отсутствие тестов означает неиспользование TDD, но не наоборот.
Значительная часть проектов того же Apache покрыта тестами, но нигде не видел использования TDD, т. к. кодом владеет довольно много людей. Что-то похожее на TDD иногда используется при правке багов (сначала делается тест, потом правится баг), но тоже далеко не всегда.wizi4d Автор
10.06.2015 19:14-2писать авто-тесты на существующий код иногда просто не возможно, ведь код, написанный в обычной парадигме, как правило совершенно тесто-не-пригодный.
Количество тестов позволяет предположить, что TDD имеет место быть.grossws
10.06.2015 20:18+5писать авто-тесты на существующий код иногда просто не возможно, ведь код, написанный в обычной парадигме, как правило совершенно тесто-не-пригодный
Если вы под «обычной парадигмой» понимаете говнокод-спагетти, то его тестами покрывать сложно. Но не стоит называть это «обычной парадигмой». Просто плохое проектирование, безотносительно причин.
Повторюсь, хорошее проектирование не требует использование TDD и использование TDD не гарантирует хорошо спроектированного приложения.
Количество тестов позволяет предположить, что TDD имеет место быть.
Не позволяет, это обращение импликации. Плохое покрытие говорит о неиспользовании TDD, нормальное покрытие не говорит об использовании или неиспользовании TDD. Гораздо вероятнее, что у них просто есть требование написания тестов на новый функционал и регрессионных тестов на баги, такой подход тоже даёт более-менее хорошее покрытие кодовой базы.
philipto, не просветите нас о процессах разработке в JetBrains?philipto
10.06.2015 21:15+3в разных командах в JetBrains процесс может отличаться. Отвечает ведущий разработчик из команды IntelliJ IDEA Николай Чашников: «Команда в целом — не использует TDD. Возможно, отдельные разработчики для отдельных частей кода и используют.»
chaetal
11.06.2015 16:59+1Отсутствие примеров не доказывает принципиальную невозможность. У вас есть более существенные аргументы в подтверждение тезиса о неработоспособности TDD в каких-то (судя по формулировке, их должно быть много, но можно пояснить и на упомянутых выше примерах) конкретных применениях? Почему вы считаете, что там TDD не применимо?
wizi4d Автор
10.06.2015 17:44+3Я бы сказал, что:
TDD НЕ работает в некоторых частных случаях
Например, «тяжело» разработать UI через тестирование.
Для всего остального вполне себе работает.defuz
10.06.2015 23:51+2Простите, но у меня сложилось впечатление, будто вы не понимаете, что такое TDD. Все же покрывать код тестами и использовать TDD это не одно и то же.
wizi4d Автор
11.06.2015 13:14+21) Написать падающий тест
2) Написать код, чтобы написанный тест прошел
3) Провести рефакторинг
4) goto 1
Сложно как-то не правильно понимать, что такое TDD.
Кто-то проектирует в голове, кто-то на бумаге, я же проектирую в тестах. Видимо у меня мозг заточен по «Кент Бековски», поэтому TDD прижился.
Подавляющее большинство задач, с которыми мне приходится сталкиваться (разработка всякой разной бизнес-логики), эффективно решается test-first подходом. Главное, чтобы слой логики был отделен от слоя UI.artbear
11.06.2015 13:37Чуть дополню, вставив шаг 4 и дополню п.1
1) Написать тест. Убедиться в его падении.
2) Написать код, чтобы написанный тест прошел
3) Провести рефакторинг.
4) Прогнать тесты
5) goto 1
KIVagant
11.06.2015 00:01+1Возможно я ещё не переломил свой мозг как положено, но каждый раз, когда я пытаюсь использовать BDD, это выглядит как-то так:
— пишу тест
— пишу код
— переделываю тест под написанный код.
А так, на самом деле, BDD действительно помогает взять нечто «старое», покрыть тестами, затем написать новое и проверить соответствие.
Писать же сразу с нуля тесты, а потом под них код, у меня не получается совсем (даже если это API, даже если всё кажется чистым и понятным).Lailore
11.06.2015 01:06Я думаю это из за того, что вы не выполняете предварительную работу
KIVagant
11.06.2015 01:10Конечно, я не агитирую за «не-BDD». Возможно, в какой-то мере можно настолько продумывать код заранее, у меня хорошо получается обычно наоборот: черновик логики — декомпозиция — шлифовка — тесты.
grossws
11.06.2015 01:23+1BDD, как методология, не предназначена для покрытия существующего кода, но для написания нового. Инструменты, создаваемые для использования в BDD могут использоваться и для написания user stories/specs под имеющийся код, но это уже не BDD. Также как использования какого-нибудь xUnit фреймворка с TDD связано только тем, что в TDD он может применяться.
В BDD код создается под user stories (которые задаются в терминах домена, что не обязательно в TDD) или спецификации (опять же в терминах домена, в котором используется описываемый модуль). Т. е. в терминах BDD вы сначала пишете какой-нибудь сценарий:Given user with browser without JSESSIONID cookie set
And without X-AUTH cookie set
When he enters to restricted area
Then he receive redirect to authentication page
Потом пишете обвязку, которая из текстовых описаний в секциях Given/When/Then собирает нужные инициализирующие операции (из Given), действия над system under test (из When) и проверку постусловий (из Then).
Только после этого пишется код, который будет удовлетворять полученному тесту.
Если вы оказываетесь в ситуации, что написанный код не соответствует user stories, то либо код неверен, либо он решает другую задачу (не соответствует спецификации/user story).KIVagant
11.06.2015 01:36Да, я понимаю эту концепцию и даже применяю её периодически. Только в моём случае это хорошо работает именно как юзер-стори. То есть, если к примеру я разрабатываю некое API и пишу тест на Behat (это под php), то результат удачен только когда я тестирую это «как видит пользователь». Вплоть до того, что мне показалось проще имитировать вызовы браузера (для REST-like API), чем вызовы методов классов. Но если пытаться использовать подобные истории как тест самой логики кода (а не внешних вызовов), то тут у меня всегда ступор. Потому что код нового продукта я меняю очень и очень быстро в процессе написания. И только когда он _уже_ выполняет некую завершенную задачу, я декомпозирую его для соблюдения разных принципов (вроде единой ответственности). И тогда только могу покрывать тестами.
И вот на этом этапе приходится менять и те тесты, которые вроде были предназначены для проверки готового результата. Результат после шлифовки часто отличается (добавляются позитивные, негативные сценарии, параметры одних объектов становятся зависимыми самостоятельными объектами).
Конечно, если идёт работа внутри команды, можно заранее согласовать требования к тому же API, выполнить строго в рамках требований, а затем уже предлагать изменённые варианты. Но в одиночку я так не мыслю.grossws
11.06.2015 01:57+1У меня ситуация аналогичная. Когда требования уже сформулированы (прототип или какая-то реализация уже есть), первичное проектирование сделано, то можно написать спеку/user story и потом писать код. Но либо это уровня функциональных тестов и выше (тогда хорошо идут user stories), либо чётко очерченный модуль, с понятной спецификацией и контрактом (тогда спеку написать не составляет никакого труда, но можно и после кода).
добавляются позитивные, негативные сценарии
Это просто расширение спецификации/user stories, оно абсолютно нормально в рамках BDD.
Вообще с TDD/BDD ещё более-менее нормально в языках с динамической типизацией, но куда хуже в языках со статической. Особенно, когда есть привычка пользоваться нормальной IDE, т. к. при написании теста (или соответствующих правил для Given/When/Then) автодополнение не работает, всё красное. Не комфортно. Поэтому часто сначала пишется stub-класс, потом тест, потом заполняется stub, что уже не очень соответствует канонам TDD.KIVagant
11.06.2015 02:21Возможно в этом случае должны помогать интерфейсы (которые сами по себе уже дублируют идею BDD)
sferrka
11.06.2015 08:56+1Вы ведь все равно запускаете код, во время написания, для проверки? Или сразу пишете целиком? В любом случае, конкретно TDD служит не столько для тестирования, сколько для описания будущего интерфейса, чтобы не двигаться вслепую, а видеть цель. Совершенно необязательно писать сразу полноценный тест, достаточно контрольные точки расставить.
dozent
11.06.2015 15:26Почему не написать так:
Ожидаем.Что(5).НеРавно(7);
По-моему, красивее чем
Ожидаем.Что(5).Не_().Равно(7);
artbear
11.06.2015 18:21+1Разобрал бинарники на исходники github.com/artbear/xUnitFor1C_2.git
Теперь можно посмотреть на исходники и увидеть, как в 1С можно писать хороший код :)
svaroha
11.06.2015 19:52-1Вместо Не_() пишите Нет().
wizi4d Автор
11.06.2015 20:05+1Рассматривал этот вариант. На мой вкус:
Ожидаем.Что(5).Не_().Равно(7);
лучше чем:
Ожидаем.Что(5).Нет().Равно(7);
svaroha
11.06.2015 20:27Ну тогда вместо Не_() пишем ОтнюдьНет(). Смущает Не_ -подчеркивание — это обычно какая то мусорная временная переменная/функция. ИМХО.
artbear
11.06.2015 21:03Интересное предложение.
Мне стало нравиться
Ожидаем.Что(5).СовсемНе().Равно(7);
или еще лучше
Ожидаем.Что(5).ТочноНе().Равно(7);
sferrka
11.06.2015 21:30+2Ожидаем.Что(5).ЭтоНе().Равно(7);
А так смысл сохраняется)wizi4d Автор
11.06.2015 22:01ЭтоНе() выглядит интересно, спасибо! Обязательно попробую применить совет.
Нужно будет только по другому обыграть другие зарезервированные слова (Истина, Ложь, Null, Неопределено) в утверждениях, возможно это окажется легче.
sl4mmer
Теперь я видел все
EvilBeaver
Раскройте мысль, пожалуйста:)
sl4mmer
>1с, tdd, bdd, fluent interface,
sl4mmer
Не в обиду 1С кодерам, у меня просто шаблон затрещал
EvilBeaver
Счетчик людей понявших, что 1С-ники давно перестали быть «вещью в себе» увеличен. Это приятно.