Собственные шаблоны тестов CodeRush. Тестируем порядок вызовов методов

    В своей работе нам приходится писать много тестов. Чтобы делать это быстро, мы используем шаблоны CodeRush. В этой статье мы расскажем вам, как создавать собственные шаблоны для тестирования. В качестве примера возьмём такую задачу: протестировать правильный порядок вызовов защищенных методов класса. Будем использовать только NUnit без применения библиотек типа NMock и им подобных.




    Объект для тестирования


    Итак, начнём с подопытного объекта. Напишем простой класс, содержащий несколько методов и имеющий определённый порядок их вызовов.
    public class SimpleClass {
       public void DoAction() {
           BeforeAction();
           try {
               DoActionCore();
           }
           finally {
               AfterAction();
           }
       }
       protected virtual void BeforeAction() { }
       protected virtual void AfterAction() { }
       protected virtual void DoActionCore() { }
    }
    


    В даннном классе необходимо проверить, что в результате вызова метода DoAction гарантированно будут выполнены методы BeforeAction, DoActionCore и AfterAction в нужной последовательности и нужное количество раз.

    Тестирование будем основывать на написании наследника класса SimpleClass, который при вызове определенного метода будет фиксировать, что данный метод был вызван. Для простоты, в качестве trace-объекта возьмем обычную строку и будем дописывать туда имя вызванного метода. В тесте будем сверять полученную строку с ожидаемым результатом.

    Определим соглашение по именованию таких классов как TestИмя_Исходного_Класса. Этим мы упростим себе написание шаблона для тестов.

    Настройка шаблонов


    Кратко о шаблонах CodeRush: они позволяют быстро вставлять часто используемые фрагменты кода в редакторе, следуя определённым правилам подстановки имен, форматированию и т.д. Они сродни code-snippets, но отличаются от них бОльшей интеллектуальностью, предоставляя ряд преимуществ, таких как контексты применения, способность анализировать существующий код, наличие команд, провайдеров строк, филдов, линков и прочее.

    Теперь приступаем к написанию собственного шаблона для наследника класса, содержащего trace-строку. Откроем окно настроек, перейдём в группу Editor -> Templates, группу опций для NUnit. Заметим, что CodeRush уже содержит предопределённый набор шаблонов для написания NUnit-тестов, но нам необходимо создать другой.



    Рис.1 — Создание нового шаблона для NUnit

    Зададим шаблону короткое имя, например, stu (от stub-class) и напишем тело шаблона. Спроектируем шаблон таким образом, что при его активации имя исходного класса будет храниться в буфере обмена и после вставки будет создан код, отражающий определённые выше правила для имен.
    #region Test«Paste» (stub class)
    public class Test«Paste» :«Paste» {
     string trace = string.Empty;
     public  Test«Paste»(«Caret») : base() {
     }
     public string Trace { get { return trace; } set { trace = value; } }
    }
    #endregion
    



    Рис. 2 — Написание тела шаблона, используя предопределенные команды

    При написании шаблона используем предопределённый набор команд, выбирая их их списка. Такими командами здесь являются «Paste» и «Caret».


    Теперь важно указать, где этот шаблон может выполняться. Для этого настроим параметры использования Use, установив необходимые значения. Укажем для него параметр Line.OnEmptyLine.



    Рис. 3 — Задание областей использования шаблона

    Аналогично первому, напишем шаблон atr (add trace) для добавления trace-строки в метод.
    trace += "->«Member»";
    «Caret»
    

    В данном случае имя текущего метода будет автоматически вставляться при активации шаблона и нет необходимости предварительно копировать его в буфер обмена. Зададим такие области использования шаблона как InClass, InMethod



    Рис. 4 — Шаблон для вставки trace-строки

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

    Пишем тесты быстро


    Как это всё работает? Попробуем написать тест для SimpleClass.

    Нам необходим класс-наследник для тестов. Скопируем в буфер обмена имя SimpleClass и напечатаем stu. На месте курсора вставится шаблон для наследуемого класса, содержащий свойство Trace.



    Рис. 5 — Активация шаблона и вставка кода


    Теперь добавим trace-информацию для каждого интересующего нас метода. Для этого перекроем эти методы. В Visual Studio для этого наберем «over», затем нажмем пробел и выберем нужные имена методов из выпавшего списка, при этом поблагодарив IntelliSense за то, что вам не пришлось мучительно вспоминать их названия. В результате все необходимые методы перекрыты и пока просто вызывают базовые.


    Теперь самое время включить в работу наш шаблон atr. Пройдёмся по этим методам, активируем шаблон для вставки trace-строки. В результате мы получим методы, которые будут добавлять к свойству Trace свое имя после вызова.

    #region TestSimpleClass (stub class)
    public class TestSimpleClass : SimpleClass {
       string trace = string.Empty;
       public TestSimpleClass()
           : base() {
       }
       public string Trace { get { return trace; } set { trace = value; } }
     
       protected override void BeforeAction() {
           base.BeforeAction();
           trace += "->BeforeAction";
       }
       protected override void AfterAction() {
           base.AfterAction();
           trace += "->AfterAction";
       }
       protected override void DoActionCore() {
           base.DoActionCore();
           trace += "->DoActionCore";
       }
    }
    #endregion
     


    Помимо имени в trace-объект может быть добавлена любая другая связанная с вызовом информация, например, параметры вызова и т.д


    Вернёмся непосредственно к тесту. Создадим TextFixture для группы тестов нашего класса. Кстати, для ускорения её создания вы можете также написать свой шаблон, который будет включать в себя методы для SetUp и TearDown. Объявим экземпляр нашего класса-наследника, проинициализируем его и напишем тест на метод DoAction. Тест будет заключаться в сравнении свойства Trace класса-наследника после вызова метода с ожидаемым результатом, который отражает правильную последовательность вызовов защищенных методов класса.

    #region SimpleClassTests
    [TestFixture]
    public class SimpleClassTests {
       TestSimpleClass testClass;
       
       [SetUp]
       public void Setup() {
           testClass = new TestSimpleClass();
       }
       [TearDown]
       public void Teardown() {
           testClass = null;
       }
       [Test]
       public void DoActionMethod() {
           testClass.Trace = string.Empty;
           testClass.DoAction();
           string expectedTrace = "->BeforeAction->DoActionCore->AfterAction";
           Assert.AreEqual(expectedTrace, testClass.Trace);
     
       }
    }
    #endregion
    


    Запустим тест. Результат достигнут — мы убедились, что внутри класса всё работает как и должно.


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


    Удачи в создании удобных шаблонов тестирования!
    Developer Soft
    Компания
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 22

      +2
      Спасибо за статью, но мне опять кажется, что пример не совсем удачный.
      Результат достигнут — мы убедились, что внутри класса всё работает как и должно.
      Принято считать, что тестировать внутренности класса плохо: нарушается инкапсуляция, появляется зависимость от реализации и бедный программист, который позже будет вносить изменения или пытаться понять, как все работает.
        +4
        Я не призываю вас тестировать закрытые члены класса. Но если классу предоставляется возможность наследования, как в приведенном варианте, то тестирование его protected части вполне себе оправдано. А насчет внесения изменений и понять как все работает — в этом как-раз написанные тесты вам и помогут.
          0
          Вот как только класс будет отнаследован — тогда и будете тестировать наследника.

          А пока — вы предложили создать хрупкие тесты, завязанные на реализацию. Которые сломаются от малейшего рефакторинга.
            +2
            Вот как только класс будет отнаследован — тогда и будете тестировать наследника.

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

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

            Подходы могут быть различные, ничего в этом плохого нет — все зависит от конкретной задачи. Что б лучше понять — немного процитирую (кстати мой вариант второй).
            "… для тестирования классов применяются тестовые драйверы. Существует несколько способов реализации тестового драйвера:

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

            Тестовый драйвер реализуется в виде класса, наследуемого от тестируемого. В отличие от предыдущего способа, такому тестовому драйверу доступна не только public, но и protected часть.

            Тестовый драйвер реализуется непосредственно внутри тестируемого класса (в класс добавляются диагностические методы). Такой тестовый драйвер имеет доступ ко всей реализации класса, включая private члены. В этом случае в методы класса включаются вызовы отладочных функций и агенты, отслеживающие некоторые события при тестировании."

              +1
              «Если вы предложите лучший вариант тестирования, без завязки на реализацию, то с удовольствием выслушаю вас…»

              Лучший вариант тестирования — тестировать поведение, а не реализацию. Вне зависимости от реализованных паттернов проектирования.

              Как только у вас появится класс, который будет реализовывать шаблонные методы — так сразу вы покроете его тестами и так сразу проверите жизнеспособность реализации The Template Method Pattern.
                +1
                Как мне кажется вы говорите в большей степени про функциональное или про тестирование «черного ящика». Я же про про unit-тестирование, которое обеспечит мне покрытие кода тестами и возможность рефакторинга. В вашем случае тестирование только API класса покрытия не даст, вы можете лишь проверить результат. К тому же, если вариантов много, что бы протестировать все ветвления алгоритма вы будете вынуждены иметь целый набор эталонов для всех случаев. Так что выбирайте сами, что вам приемлемо в конкретной ситуации.
                  0
                  Я как раз и говорю о модульных тестах (которые unit-тесты).

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

                  «Я же про про unit-тестирование, которое обеспечит мне покрытие кода тестами и возможность рефакторинга.»
                  Возможность рефакторинга, как раз, ваши тесты и не обеспечат, по определению. Почему? Потому что рефакторинг, это изменение реализации кода без изменения его поведения. Таким образом, если в результате рефакторинга вы вдруг внезапно захотите отказаться от The Template Method, тогда ваши тесты внезапно поломаются, несмотря на то, что поведение кода осталось тем же самым.
                    0
                    Если целиком и полностью следовать идеологии TDD...

                    Тут вопрос не в TDD. Помимо его есть еще и TAD (Test-After Development) и в примере написан был cначал код класса, а потом уже тесты.

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

                    Это по какому определению? Если, есть гарантия, что я ничего не отломаю (а это обеспечивает наличие тестов), то я смело могу изменять структуру внутренних классов. Как реализованы тесты к возможности рефакторинга отношения не имеет. Согласен, что в теории иметь тесты только на интерфейс хорошо и удобно, но в реальности протестировать все сценарии поведения, затрагивая только public-методы может быть слишком накладно или вообще невозможно. А уж обеспечить хорошее покрытие кода вообще нереально.
                      0
                      «Если, есть гарантия, что я ничего не отломаю (а это обеспечивает наличие тестов), то я смело могу изменять структуру внутренних классов.»
                      тогда это не рефакторинг :-)

                      «затрагивая только public-методы может быть слишком накладно или вообще невозможно»
                      ненакладно и вполне реально

                      так или иначе — я свою точку зрения, которая основывается на моём опыте высказал.
                        0
                        … изменять структуру внутренних классов.»
                        тогда это не рефакторинг :-)

                        А что тогда?
                        Рефакторинг (англ. refactoring) — процесс изменения внутренней структуры программы, не затрагивающий её внешнего поведения и имеющий целью облегчить понимание её работы
                        «затрагивая только public-методы может быть слишком накладно или вообще невозможно»
                        ненакладно и вполне реально

                        Рад за вас, видимо у нас с вами разный класс решаемых задач.
                          0
                          «А что тогда? „
                          Вот именно — вы не можете изменить внутреннюю структуру, потому что вы завязаны на The Template Pattern ;-)
                          Вы не можете изменить реализацию произвольным образом — потому что ваши тесты знают о деталях реализации.
                            0
                            Насчет рефакторинга все же с вами не соглашусь.
                            Допустим, хочу сделать в нашем примере ExtractMethod для алгоритма — делается это примерно так:
                            1. Выделяю класс и пишу метод на его создание
                            2. Тестирую метод на создание экземпляра нового класса
                            3. Создаю новую TestFixture для выделенного класса и переношу туда тесты на алгоритм
                            4. В исходном тесте проверяю, что вызов был делегирован новому классу.
                            Ну, или в другой последовательности, если вы используете TDD

                            Ну вижу смысла продолжать спор, раз вы против white-box testing, тем не менее спасибо за то, что выразили свою точку зрения.
                              0
                              Я говорил не о конкретных техниках рефакторинга, а о рефакторинге в общем.

                              Сейчас вы отказаться от The Template Method не сможете, потому что на этот паттерн у вас завязаны тесты. А вот избавление от него, с переписыванием в пользу какого-то другого решения, и есть рефакторинг, который у вас провести не получится.
                                0
                                Да все получится ;). Возникнет необходимость реструктурировать код — итеративно рефакторите вместе с тестами, вот и все. Вы рассматривайте unit-тесты как что каноническое, а они так же как и код имеют тенденцию изменяться в рамках роста проекта. Это же не приемочные тесты заказчика точный на вход-выход. Модульное тестирование подразумевает полное покрытие кода (в идеале всех методов), поэтому если код меняется — соответственно и тесты по необходимости. И нужны они в первую очередь разработчику.
                                  0
                                  Я и правда подхожу к тестам слишком академично.

                                  Для меня рефакторинг — это изменение кода без изменения поведения и, как следствие, без изменения тестов. Если нужно менять тесты — значит изменилось поведение, значит это уже не рефакторинг :-)
                                    0
                                    Да, я уловил сейчас вашу мысль. Если рассматривать код отдельно от тестов, то все нормально — рефакториг кода провести можно. Вы рассматриваете код + тесты вместе, поэтому по терминологии это уже как бы и не рефакторинг. Все же объект рефакторинга — класс, а не тесты, и при смене шаблонного метода его интерфейс остается прежним, так что все же рефакторинг на мой взгляд ;)
        +3
        А как вы делаете дугообразные скриншоты? поделитесь магией =)
          +7
            –1
            Здорово, я для виндузятника есть нечто похожее? Просто линух редко под рукой…
              +3
              Каюсь, ступил. GIMP можно и на венде юзать =)
            0
            Snagit — великолепная программа
              +2
              Это да. Но вот волна для обрезания там всё-таки грубовата, на мой взгляд. Так что для этого мы используем GIMP, как уже было сказано выше :-)

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое