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


Благодаря классному дядьке Кенту Беку (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)


  1. sl4mmer
    10.06.2015 17:22
    +20

    Теперь я видел все


    1. EvilBeaver
      10.06.2015 17:32

      Раскройте мысль, пожалуйста:)


      1. sl4mmer
        10.06.2015 17:40
        +4

        >1с, tdd, bdd, fluent interface,


        1. sl4mmer
          10.06.2015 17:40
          +10

          Не в обиду 1С кодерам, у меня просто шаблон затрещал


          1. EvilBeaver
            10.06.2015 18:20
            +1

            Счетчик людей понявших, что 1С-ники давно перестали быть «вещью в себе» увеличен. Это приятно.


  1. grossws
    10.06.2015 17:24
    +4

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


    1. artbear
      10.06.2015 17:26

      Соглашусь с автором, что TDD довольно часто мелькает в вакансиях, но не для 1С :)


      1. grossws
        10.06.2015 17:43
        +1

        Очень сильно зависит от области деятельности. В вебе, например, вполне себе часто, в embedded — напротив. Хотя и читал недавно забавную книжку Test Driven Development for Embedded C by James W. Grenning (PragPub), в которой рассказывается как можно использовать TDD в embedded.


        1. TimReset
          10.06.2015 18:08

          Кстати, и как книга? Я её видел и удивился, что такое есть вообще!


          1. grossws
            10.06.2015 18:51

            В принципе, прочитать стоит, если интересуетесь embedded. Но принципиально нового ничего там не увидел.

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

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

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

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

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


        1. artbear
          10.06.2015 18:28

          Указанную книгу видел, но не читал, т.к. не работаю с embedded

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


    1. EvilBeaver
      10.06.2015 17:27
      +1

      TDD работает в некоторых частных случаях, но не более

      Прямо-таки в «некоторых частных случаях»? А в общем случае не работает вообще? Мне кажется, вы что-то перегибаете палку


      1. grossws
        10.06.2015 17:47
        +1

        Много ли вы видели библиотек, более-менее серьезных фреймворков, движков БД, написанных с помощью TDD?


        1. wizi4d Автор
          10.06.2015 17:58
          -2

          Например, github.com/JetBrains/intellij-community
          Не берусь утверждать, насколько тру-TDD используется, но количество авто-тестов большое.


          1. grossws
            10.06.2015 18:30
            +4

            Откуда вы почерпнули, что они используют TDD? Отсутствие тестов означает неиспользование TDD, но не наоборот.

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


            1. wizi4d Автор
              10.06.2015 19:14
              -2

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

              Количество тестов позволяет предположить, что TDD имеет место быть.


              1. grossws
                10.06.2015 20:18
                +5

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

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

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

                philipto, не просветите нас о процессах разработке в JetBrains?


                1. philipto
                  10.06.2015 21:15
                  +3

                  в разных командах в JetBrains процесс может отличаться. Отвечает ведущий разработчик из команды IntelliJ IDEA Николай Чашников: «Команда в целом — не использует TDD. Возможно, отдельные разработчики для отдельных частей кода и используют.»


                  1. grossws
                    10.06.2015 21:19
                    +1

                    Спасибо за информацию)


        1. chaetal
          11.06.2015 16:59
          +1

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


    1. wizi4d Автор
      10.06.2015 17:44
      +3

      Я бы сказал, что:

      TDD НЕ работает в некоторых частных случаях

      Например, «тяжело» разработать UI через тестирование.
      Для всего остального вполне себе работает.


      1. artbear
        10.06.2015 18:25
        -1

        Я бы еще добавил, что TDD, как правило, не стоит использовать при подготовке прототипа.
        Прототип очень часто выкидывается полностью.


        1. j_wayne
          10.06.2015 19:15

          Он должен выкидываться. Но с позиции клиента — очень велик соблазн не выкидывать — «оно же уже работает».


      1. defuz
        10.06.2015 23:51
        +2

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


        1. wizi4d Автор
          11.06.2015 13:14
          +2

          1) Написать падающий тест
          2) Написать код, чтобы написанный тест прошел
          3) Провести рефакторинг
          4) goto 1
          Сложно как-то не правильно понимать, что такое TDD.

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


          1. artbear
            11.06.2015 13:37

            Чуть дополню, вставив шаг 4 и дополню п.1

            1) Написать тест. Убедиться в его падении.
            2) Написать код, чтобы написанный тест прошел
            3) Провести рефакторинг.
            4) Прогнать тесты
            5) goto 1


  1. KIVagant
    11.06.2015 00:01
    +1

    Возможно я ещё не переломил свой мозг как положено, но каждый раз, когда я пытаюсь использовать BDD, это выглядит как-то так:
    — пишу тест
    — пишу код
    — переделываю тест под написанный код.

    А так, на самом деле, BDD действительно помогает взять нечто «старое», покрыть тестами, затем написать новое и проверить соответствие.
    Писать же сразу с нуля тесты, а потом под них код, у меня не получается совсем (даже если это API, даже если всё кажется чистым и понятным).


    1. Lailore
      11.06.2015 01:06

      Я думаю это из за того, что вы не выполняете предварительную работу


      1. KIVagant
        11.06.2015 01:10

        Конечно, я не агитирую за «не-BDD». Возможно, в какой-то мере можно настолько продумывать код заранее, у меня хорошо получается обычно наоборот: черновик логики — декомпозиция — шлифовка — тесты.


    1. grossws
      11.06.2015 01:23
      +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).


      1. KIVagant
        11.06.2015 01:36

        Да, я понимаю эту концепцию и даже применяю её периодически. Только в моём случае это хорошо работает именно как юзер-стори. То есть, если к примеру я разрабатываю некое API и пишу тест на Behat (это под php), то результат удачен только когда я тестирую это «как видит пользователь». Вплоть до того, что мне показалось проще имитировать вызовы браузера (для REST-like API), чем вызовы методов классов. Но если пытаться использовать подобные истории как тест самой логики кода (а не внешних вызовов), то тут у меня всегда ступор. Потому что код нового продукта я меняю очень и очень быстро в процессе написания. И только когда он _уже_ выполняет некую завершенную задачу, я декомпозирую его для соблюдения разных принципов (вроде единой ответственности). И тогда только могу покрывать тестами.
        И вот на этом этапе приходится менять и те тесты, которые вроде были предназначены для проверки готового результата. Результат после шлифовки часто отличается (добавляются позитивные, негативные сценарии, параметры одних объектов становятся зависимыми самостоятельными объектами).

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


        1. grossws
          11.06.2015 01:57
          +1

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

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

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


          1. KIVagant
            11.06.2015 02:21

            Возможно в этом случае должны помогать интерфейсы (которые сами по себе уже дублируют идею BDD)


        1. sferrka
          11.06.2015 08:56
          +1

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


  1. dozent
    11.06.2015 15:26

    Почему не написать так:

    Ожидаем.Что(5).НеРавно(7);
    

    По-моему, красивее чем
    Ожидаем.Что(5).Не_().Равно(7);
    


    1. wizi4d Автор
      11.06.2015 16:19
      +1

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


      1. dozent
        11.06.2015 16:51

        Понял, спасибо!


  1. artbear
    11.06.2015 18:21
    +1

    Разобрал бинарники на исходники github.com/artbear/xUnitFor1C_2.git
    Теперь можно посмотреть на исходники и увидеть, как в 1С можно писать хороший код :)


  1. svaroha
    11.06.2015 19:52
    -1

    Вместо Не_() пишите Нет().


    1. wizi4d Автор
      11.06.2015 20:05
      +1

      Рассматривал этот вариант. На мой вкус:

      Ожидаем.Что(5).Не_().Равно(7);
      

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


      1. svaroha
        11.06.2015 20:27

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


        1. artbear
          11.06.2015 21:03

          Интересное предложение.
          Мне стало нравиться

          Ожидаем.Что(5).СовсемНе().Равно(7); 


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


          1. sferrka
            11.06.2015 21:30
            +2

            Ожидаем.Что(5).ЭтоНе().Равно(7);


            А так смысл сохраняется)


            1. wizi4d Автор
              11.06.2015 22:01

              ЭтоНе() выглядит интересно, спасибо! Обязательно попробую применить совет.
              Нужно будет только по другому обыграть другие зарезервированные слова (Истина, Ложь, Null, Неопределено) в утверждениях, возможно это окажется легче.


              1. sferrka
                11.06.2015 22:09

                Жду следующую статью с нетерпением)


          1. EvilBeaver
            13.06.2015 00:08

            Мы еще обсуждали «ВащеНе» и «НиРазуНе»


      1. SaNNy32
        06.07.2015 08:10

        Мне тоже Не_() не нравится. Может вместо Не_() использовать Отрицание()?