Юнит-тесты, BDD и сила текучих утверждений (fluent assertions) в 1С

    Немного истории


    Благодаря классному дядьке Кенту Беку (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. Как, наверное, заметил читатель с пытливым умом, ссылка ведет на нечто большее, чем просто на библиотеку утверждений. Но это уже материал для следующей статьи.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 48

      +20
      Теперь я видел все
        0
        Раскройте мысль, пожалуйста:)
          +4
          >1с, tdd, bdd, fluent interface,
            +10
            Не в обиду 1С кодерам, у меня просто шаблон затрещал
              +1
              Счетчик людей понявших, что 1С-ники давно перестали быть «вещью в себе» увеличен. Это приятно.
        +4
        сейчас уже можно сказать, что разработка через тестирование стала стандартом де-факто. Практически в любых вакансиях фигурирует требование к знанию и опыту использования методики TDD и соответствующих инструментов.
        Это не так. TDD работает в некоторых частных случаях, но не более.
          0
          Соглашусь с автором, что TDD довольно часто мелькает в вакансиях, но не для 1С :)
            +1
            Очень сильно зависит от области деятельности. В вебе, например, вполне себе часто, в embedded — напротив. Хотя и читал недавно забавную книжку Test Driven Development for Embedded C by James W. Grenning (PragPub), в которой рассказывается как можно использовать TDD в embedded.
              0
              Кстати, и как книга? Я её видел и удивился, что такое есть вообще!
                0
                В принципе, прочитать стоит, если интересуетесь embedded. Но принципиально нового ничего там не увидел.

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

                Отвечу artbear здесь же. Проблема использования TDD в embedded в том, что есть вещи для которых практически невозможно сделать нормальные стабы/моки, т. к. control flow недетерминирован (те же прерывания/события). Проблемы, в общем, схожи с тестированием асинхронных приложений. Для нормального покрытия тестами нужно делать очень много тестов.

                Задача по сложности сравнимая с написанием хорошей симуляции соответствующего процессора с обвязкой. Если говорить про life-critical, то там будут делать и аппаратные эмуляторы, т. к. реальное железо сильно отличается от того, что описано в datasheet'ах (достаточно заглянуть в errata чего-нибудь более-менее распространенного и не очень нового).

                Вы представляете себе человека, который будет делать тестовую ситуацию с порчей памяти при DMA-передаче память-память, если в этот момент на UART порт «удачно» пришел байт с установленным старшим битом (а заодно все другие вариации, когда этого не произошло)? Я нет. Куда реалистичнее аппаратный симулятор, который будет прогонять большое количество недетерминированных тестов, варьируя параметры.

                Но для большинства задач увеличение стоимости разработки на пару-тройку порядков будет мотивацией отказаться от такого серьёзного тестирования, т. к. дешевле перепрошить телевизор/плеер/телефон/whatever в сервис-центре, если вдруг что-то такое случится.
                0
                Указанную книгу видел, но не читал, т.к. не работаю с embedded

                А в чем проблема использования TDD с embedded?
                Оптимизация памяти, ресурсов?
              +1
              TDD работает в некоторых частных случаях, но не более

              Прямо-таки в «некоторых частных случаях»? А в общем случае не работает вообще? Мне кажется, вы что-то перегибаете палку
                +1
                Много ли вы видели библиотек, более-менее серьезных фреймворков, движков БД, написанных с помощью TDD?
                  –2
                  Например, github.com/JetBrains/intellij-community
                  Не берусь утверждать, насколько тру-TDD используется, но количество авто-тестов большое.
                    +4
                    Откуда вы почерпнули, что они используют TDD? Отсутствие тестов означает неиспользование TDD, но не наоборот.

                    Значительная часть проектов того же Apache покрыта тестами, но нигде не видел использования TDD, т. к. кодом владеет довольно много людей. Что-то похожее на TDD иногда используется при правке багов (сначала делается тест, потом правится баг), но тоже далеко не всегда.
                      –2
                      писать авто-тесты на существующий код иногда просто не возможно, ведь код, написанный в обычной парадигме, как правило совершенно тесто-не-пригодный.

                      Количество тестов позволяет предположить, что TDD имеет место быть.
                        +5
                        писать авто-тесты на существующий код иногда просто не возможно, ведь код, написанный в обычной парадигме, как правило совершенно тесто-не-пригодный
                        Если вы под «обычной парадигмой» понимаете говнокод-спагетти, то его тестами покрывать сложно. Но не стоит называть это «обычной парадигмой». Просто плохое проектирование, безотносительно причин.

                        Повторюсь, хорошее проектирование не требует использование TDD и использование TDD не гарантирует хорошо спроектированного приложения.

                        Количество тестов позволяет предположить, что TDD имеет место быть.
                        Не позволяет, это обращение импликации. Плохое покрытие говорит о неиспользовании TDD, нормальное покрытие не говорит об использовании или неиспользовании TDD. Гораздо вероятнее, что у них просто есть требование написания тестов на новый функционал и регрессионных тестов на баги, такой подход тоже даёт более-менее хорошее покрытие кодовой базы.

                        philipto, не просветите нас о процессах разработке в JetBrains?
                          +3
                          в разных командах в JetBrains процесс может отличаться. Отвечает ведущий разработчик из команды IntelliJ IDEA Николай Чашников: «Команда в целом — не использует TDD. Возможно, отдельные разработчики для отдельных частей кода и используют.»
                            +1
                            Спасибо за информацию)
                    +1
                    Отсутствие примеров не доказывает принципиальную невозможность. У вас есть более существенные аргументы в подтверждение тезиса о неработоспособности TDD в каких-то (судя по формулировке, их должно быть много, но можно пояснить и на упомянутых выше примерах) конкретных применениях? Почему вы считаете, что там TDD не применимо?
                  +3
                  Я бы сказал, что:
                  TDD НЕ работает в некоторых частных случаях

                  Например, «тяжело» разработать UI через тестирование.
                  Для всего остального вполне себе работает.
                    –1
                    Я бы еще добавил, что TDD, как правило, не стоит использовать при подготовке прототипа.
                    Прототип очень часто выкидывается полностью.
                      0
                      Он должен выкидываться. Но с позиции клиента — очень велик соблазн не выкидывать — «оно же уже работает».
                      +2
                      Простите, но у меня сложилось впечатление, будто вы не понимаете, что такое TDD. Все же покрывать код тестами и использовать TDD это не одно и то же.
                        +2
                        1) Написать падающий тест
                        2) Написать код, чтобы написанный тест прошел
                        3) Провести рефакторинг
                        4) goto 1
                        Сложно как-то не правильно понимать, что такое TDD.

                        Кто-то проектирует в голове, кто-то на бумаге, я же проектирую в тестах. Видимо у меня мозг заточен по «Кент Бековски», поэтому TDD прижился.
                        Подавляющее большинство задач, с которыми мне приходится сталкиваться (разработка всякой разной бизнес-логики), эффективно решается test-first подходом. Главное, чтобы слой логики был отделен от слоя UI.
                          0
                          Чуть дополню, вставив шаг 4 и дополню п.1

                          1) Написать тест. Убедиться в его падении.
                          2) Написать код, чтобы написанный тест прошел
                          3) Провести рефакторинг.
                          4) Прогнать тесты
                          5) goto 1
                    +1
                    Возможно я ещё не переломил свой мозг как положено, но каждый раз, когда я пытаюсь использовать BDD, это выглядит как-то так:
                    — пишу тест
                    — пишу код
                    — переделываю тест под написанный код.

                    А так, на самом деле, BDD действительно помогает взять нечто «старое», покрыть тестами, затем написать новое и проверить соответствие.
                    Писать же сразу с нуля тесты, а потом под них код, у меня не получается совсем (даже если это API, даже если всё кажется чистым и понятным).
                      0
                      Я думаю это из за того, что вы не выполняете предварительную работу
                        0
                        Конечно, я не агитирую за «не-BDD». Возможно, в какой-то мере можно настолько продумывать код заранее, у меня хорошо получается обычно наоборот: черновик логики — декомпозиция — шлифовка — тесты.
                        +1
                        BDD, как методология, не предназначена для покрытия существующего кода, но для написания нового. Инструменты, создаваемые для использования в 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).
                          0
                          Да, я понимаю эту концепцию и даже применяю её периодически. Только в моём случае это хорошо работает именно как юзер-стори. То есть, если к примеру я разрабатываю некое API и пишу тест на Behat (это под php), то результат удачен только когда я тестирую это «как видит пользователь». Вплоть до того, что мне показалось проще имитировать вызовы браузера (для REST-like API), чем вызовы методов классов. Но если пытаться использовать подобные истории как тест самой логики кода (а не внешних вызовов), то тут у меня всегда ступор. Потому что код нового продукта я меняю очень и очень быстро в процессе написания. И только когда он _уже_ выполняет некую завершенную задачу, я декомпозирую его для соблюдения разных принципов (вроде единой ответственности). И тогда только могу покрывать тестами.
                          И вот на этом этапе приходится менять и те тесты, которые вроде были предназначены для проверки готового результата. Результат после шлифовки часто отличается (добавляются позитивные, негативные сценарии, параметры одних объектов становятся зависимыми самостоятельными объектами).

                          Конечно, если идёт работа внутри команды, можно заранее согласовать требования к тому же API, выполнить строго в рамках требований, а затем уже предлагать изменённые варианты. Но в одиночку я так не мыслю.
                            +1
                            У меня ситуация аналогичная. Когда требования уже сформулированы (прототип или какая-то реализация уже есть), первичное проектирование сделано, то можно написать спеку/user story и потом писать код. Но либо это уровня функциональных тестов и выше (тогда хорошо идут user stories), либо чётко очерченный модуль, с понятной спецификацией и контрактом (тогда спеку написать не составляет никакого труда, но можно и после кода).

                            добавляются позитивные, негативные сценарии
                            Это просто расширение спецификации/user stories, оно абсолютно нормально в рамках BDD.

                            Вообще с TDD/BDD ещё более-менее нормально в языках с динамической типизацией, но куда хуже в языках со статической. Особенно, когда есть привычка пользоваться нормальной IDE, т. к. при написании теста (или соответствующих правил для Given/When/Then) автодополнение не работает, всё красное. Не комфортно. Поэтому часто сначала пишется stub-класс, потом тест, потом заполняется stub, что уже не очень соответствует канонам TDD.
                              0
                              Возможно в этом случае должны помогать интерфейсы (которые сами по себе уже дублируют идею BDD)
                              +1
                              Вы ведь все равно запускаете код, во время написания, для проверки? Или сразу пишете целиком? В любом случае, конкретно TDD служит не столько для тестирования, сколько для описания будущего интерфейса, чтобы не двигаться вслепую, а видеть цель. Совершенно необязательно писать сразу полноценный тест, достаточно контрольные точки расставить.
                          0
                          Почему не написать так:
                          Ожидаем.Что(5).НеРавно(7);
                          

                          По-моему, красивее чем
                          Ожидаем.Что(5).Не_().Равно(7);
                          
                            +1
                            Потому что почти для каждого утверждения есть свой антипод и тогда нужно будет реализовывать в 2 раза больше утверждений. В текущей реализации любое утверждение можно инвертировать и дублировать ничего не нужно.
                              0
                              Понял, спасибо!
                            +1
                            Разобрал бинарники на исходники github.com/artbear/xUnitFor1C_2.git
                            Теперь можно посмотреть на исходники и увидеть, как в 1С можно писать хороший код :)
                              –1
                              Вместо Не_() пишите Нет().
                                +1
                                Рассматривал этот вариант. На мой вкус:
                                Ожидаем.Что(5).Не_().Равно(7);
                                

                                лучше чем:
                                Ожидаем.Что(5).Нет().Равно(7);
                                
                                  0
                                  Ну тогда вместо Не_() пишем ОтнюдьНет(). Смущает Не_ -подчеркивание — это обычно какая то мусорная временная переменная/функция. ИМХО.
                                    0
                                    Интересное предложение.
                                    Мне стало нравиться
                                    Ожидаем.Что(5).СовсемНе().Равно(7); 


                                    или еще лучше
                                    Ожидаем.Что(5).ТочноНе().Равно(7);
                                      +2
                                      Ожидаем.Что(5).ЭтоНе().Равно(7);


                                      А так смысл сохраняется)
                                        0
                                        ЭтоНе() выглядит интересно, спасибо! Обязательно попробую применить совет.
                                        Нужно будет только по другому обыграть другие зарезервированные слова (Истина, Ложь, Null, Неопределено) в утверждениях, возможно это окажется легче.
                                          0
                                          Жду следующую статью с нетерпением)
                                        0
                                        Мы еще обсуждали «ВащеНе» и «НиРазуНе»
                                      0
                                      Мне тоже Не_() не нравится. Может вместо Не_() использовать Отрицание()?
                                    0
                                    wizi4d — я бы все таки хотел заметить 2 вещи

                                    1. vanessa-behavior уже в «релизном» состоянии и уже реальные конфигурации разрабатываются с помощью этого инструмента
                                    2. в рамках vanessa-behavior мы относимся к BDD c точки зрения ожиданий заказчика от продукта — то есть этот инструмент помогает не тестированию, а структуризации хаоса требований и их валидации — то есть должен ответить автоматизировано на вопрос «А соответствует ли продукт требованиям заказчика». Собственно результаты использования релиза и сам подход будет освещен на ближайшем Инфостарте.

                                    В статье же отражён взгляд разработчика на ожидание от его будущего кода. И соответственно реализация этого подхода. Причём очень красиво в стиле DSL сделанная.

                                    и хочу попросить Вас как-то отразить эти моменты качестве update'а к статье — что к BDD можно относиться по разному с разных точек зрения. Если это конечно не сложно — неокрепшие умы могут не дочитать до данного комментария.

                                    Only users with full accounts can post comments. Log in, please.