company_banner

Юнит-тесты: Как протестировать то, что не тестируется

    Есть один замечательный вопрос, который возникает в любой дискуссии связанной с юнит-тестированием. «Надо ли создавать тесты для юнит тестов». Ответом на этот вопрос, как правило, служит технология Code Coverage. Действительно, если вы хотите убедиться в том, что юнит тест подготовлен правильно, вам нужно только проверить вызываются ли все ветвления в коде. Достигается это простым методом – надо подать на вход проверяемой функции все комбинации данных, которые позволят обойти эти ветвления. И академические примеры из документации это показывают.

    Но подвох в том, что реальный мир сложнее. Функции приложения могут учитывать условия не только подаваемые на вход. Как быть в этом случае?

    Еще раз про Code Coverage.


    Представим, что вам надо проверить код осуществляющий покупку товара:



    Если вы при этом создадите юнит тест, то очевидно, что вам необходимо предусмотреть вызов этой функции с различными параметрами. Для случая itemID=0 и ItemID<>0. В таком варианте будут вызваны все ветки кода, и этот юнит тест будет иметь значение Code Coverage 100%.



    Усложняем задачу


    Более сложный код, приближенный к реальности может иметь следующий вид:



    Как быть в таком случае? Внутри функции используется вызов CLR Date.Time, и мы на возврат данных этой функции никакого влияния не имеем. Как бы мы не пытались усложнить логику юнит-теста, работа функции зависит от внешних условий. Что, теперь запускать юнит тесты по расписанию два раза в день, днем и вечером, чтобы проверить все ветки кода?

    Можно конечно воспользоваться паттерном Visitor. При создании класса с методом PurchaseItem мы будем передавать некий интерфейс IGetCurrentTime который при работе программы будет иметь одну имплементацию, а при вызове юнит теста будет осуществляться подмена. Тем самым мы добьемся изоляции от внешних условий. Это неплохой вариант, который в общем то всеми рекомендуем. Но есть ли какие то другие варианты?

    Изолируемся от внешнего мира с помощью Moles


    Для Visual Studio 2010 существует замечательное дополнение Moles. Это так называемый Isolation Framework который и может помочь нам в таких случаях:



    Установить вы его можете из Extension Manager, как говорится, не выходя из Visual Studio. Изучать его возможности можно начать тут research.microsoft.com/en-us/projects/pex/documentation.aspx, а так же на Хабре есть несколько интересных и подробных статей.

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

    Перехватываем DateTime.Now


    Итак, что же надо сделать чтобы наш юнит тест не зависил от возвращаемых значений DateTime.Now?

    Первый шаг – добавляем в наш тестовый проект Moled Assembly – специальным образом подготовленную заглушку для сборки MSCorLib в которой содержится метод DateTime.Now



    Второй шаг – объявляем, что мы действительно собираемся перехватывать метод DateTime.Now:



    Третий шаг – собственно переделываем наши юнит тесты:



    Некоторые пояснения: атрибут HostType необходим для того чтобы указать подсистеме Moles Runtime что в данном юнит тесте будет осуществляться перехват внешних функций. Далее в коде указывается что метод DateTime.Now должен на самом деле браться из лямбды. В начале было указано что Moles сгенерировал для mscore специальную заглушку. Типы в этой заглушке имеют префиксы и суффиксы. Например, для типа DateTime.Now заглушка будет System.Moles.MDateTIme.NowGet. Запустив этои юнит тесты мы опять получим значение Code Coverage 100% вне зависимости от того, какое на самом деле текущее время.

    Заключение


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

    Кстати, завтра в офисе Microsoft на Крылатских холмах пройдет мероприятие Microsoft Quality Assurance Day www.microsoft.com/ru-ru/events/msqadays/index.html на котором будут обсуждаться вопросы обеспечения качества ПО. В моем докладе будет, упомянут Moles и я покажу «живьем» как эти механизмы работают. Если вас заинтересовала эта технология, обязательно посмотрите трансляцию.
    Microsoft
    402.60
    Microsoft — мировой лидер в области ПО и ИТ-услуг
    Share post

    Comments 34

      +2
      А я всегда верила в Visual Studio :)
        +7
        Женюсь.
          +1
          Осторожно, она Joulie, а не Jolie :)
          • UFO just landed and posted this here
          +2
          Вот именно поэтому статические вызовы и прочие синглетоны, — это зло.
          • UFO just landed and posted this here
              +1
              ""«Можно конечно воспользоваться паттерном Visitor. При создании класса с методом PurchaseItem мы будем передавать некий интерфейс IGetCurrentTim»""

              Ну! Да и еще просто дату передать не моги, обязательно некий интерфейс…
                0
                Да, поскольку в этом случае дата будет вычисляться всегда, а в исходном коде — нет.

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

            1. Три разных случая входных данных проверяются в рамках одного теста. Это противоречит базовым принципам построения юнит-тестов ("не более одного утверждения на тест"), т.к. мешает нормально проверять, что код работает правильно.

            2. Не ясно, какие именно утверждения проверяются тестом. Будет ли он оставаться зелёным, если из метода PurchaseItem убрать код, который выбрасывает исключение? Что-то мне подсказывает, что в таком случае тест по-прежнему будет проходить. А значит, в нынешнем виде он некорректен.

            В JUnit для проверки на выброс исключения используется параметр expected аннотации @Test. Наверняка в NUnit (или что это за фреймворк?) есть какие-то аналоги. Их и надо использовать.

            3. Метод ProcessOrder() имеет сайд-эффекты? Обращается ли он к другим классам? Если да, то их тоже надо закрывать в тесте моками. Если же Order сохраняется внутри класса, то в тесте надо проверить, что содержимое объекта изменилось после вызова PurchaseItem.

            В целом за стремление использовать юнит-тесты и за желание рассказать об этом остальным можно лишь похвалить, но я не хотел бы, чтобы новички учились их писать по подобным образцам.
              +2
              3. Это MSTest, судя по всему.
                +1
                Присоединяюсь. А где хоть один ассерт? Где название, по которому понятно, что именно свалилось, если тест красный (хотя КАК он может стать красным)? И где вообще тот самый «юнит» — мы тестируем условие времени работы, или правильность идентификатора ордера, или логику покупки?

                Ну и совсем о наболевшем:

                >> Запустив этот юнит тест мы опять получим значение Code Coverage 100% вне зависимости от того, какое на самом деле текущее время.

                Ну не говорит этот ваш coverage ничего о качестве тестирования. Использовать как индикатор — да, и то не как сиюмитный показатель, а в динамике; маленькое покрытие — да, подозрительно, надо смотреть почему. Но нельзя его как самоцель ставить, иначе получаем тесты пустых конструкторов, автопропертей и просто то самое тестирование бранчей без понимания, что именно надо бы протестрировать на самом деле.
                  0
                  Боюсь что я совершил ошибку, убрав все ассерты и прочие штуки, а так же не сделал три кейса для каждого случая. Сделано это было с единственной целью повысить — читабельность примера.
                  Еще раз повторюсь, цель статьи показать как можно залезть внутрь поведения казалось бы совсем неуправляемых вещей что можно попасть внутрь условия if (Date.Now....).

                  Если code coverage ничего не говорит о качестве тестирования, какую бы вы метрику предложили?
                    0
                    ошибку никогда не поздно исправить!
                    представьте, что ваш пост прочитает новичок, который еще ничего не знает о unit-тестах. или знает, но очень мало. чему его научит такой пример? он только запутается.
                      +1
                      Поправил.
                        0
                        Уже лучше! Ещё бы тестовые методы назвать так, чтобы было лучше видно, что именно они проверяют. Например:

                        PurchaseItemTest1 -> shouldNotProcessOrderWithInvalidId
                        PurchaseItemTest2 -> shouldProcessValidOrder
                        PurchaseItemTest3 -> shouldNotProcessOrderAtInvalidTime

                        Выражение намерения теста в его названии — это мощный приём. Он позволяет лучше сфокусироваться на задаче при написании теста. А тот, кто будет читать код этого теста, будет лучше понимать, какие данные в тесте более важны, а какие менее. Всё это позволяет сфокусировать внимание на существенных в данном контексте вещах, что улучшает читабельность и понятность кода.
                          0
                          вы мне кажется слишком многого хотите от поста про пример простейшего использования мока. уже и до БДД почти добрались…
                            0
                            Привычка! :) На работе code review провожу постоянно…
                      0
                      Возможно, стоит подобрать иной пример, который, оставаясь простым, является в то же самое время честным юниттестом, с ассертами и удачным названием.

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

                      С точки зрения оценки качества тестирования одна из показательных метрик — это грубо говоря количество багов, найденных в протестированном коде «на следующем цикле», т.е. для программистов и их юнит тестов — сколько багов нашли тестеры, для тестеров — сколько багов нашли пользователи в продакшене и т.д.

                      Простой вычислялки, которая автоматическим образом скажет, что да как, тупо нету. На одну строчку с регэскпом часто надо написать десятки тестов, а на десятки строк кода которые в принципе просто создают кучу объектов и передают их в метод нет смысла писать и одного теста.
                    –1
                    Представьте что там не Exceptions. Просто другие вызовы чего нибудь.
                    –1
                    А где в первом случае Assert на брошенное исключение?
                    И, кстати, что за идея бросать Exception, а не ArgumentException с комментарием?
                      +8
                      Сразу вспоминается:

                      public Calendar getTomorrow() {
                      Thread.sleep(1000*60*60*24);
                      return Calendar.getInstance();
                      }

                      Извините :)
                        0
                        Красиво, но «You may use, copy, reproduce, and distribute this Software for any non-commercial purpose,
                        subject to the restrictions in this MSR-LA.».
                        Пока не будет релиза дальше игрушек это исспользовать нельзя.
                          0
                          А в каких сторонних фреймворках реализованы подобные возможности стаббинга? Isolator?
                            0
                            typemock, ага
                            0
                            Мне почему-то кажется, что было уже довольно много статей по Moles: habrahabr.ru/search/?q=moles

                            К примеру для начинающего есть habrahabr.ru/blogs/net/98571/ а там внизу ссылки даже с видео.

                            Или вот более расширенная, но вполне доступная для начинаюшего статья: habrahabr.ru/blogs/net/101408/

                            А если по субжу, то после некоторого практического опыта использования Moles, я пришел к выводу, что это отличный костыль для legacy-кода.

                            Намного чаще если есть что-то «нетестируемое», значит есть проблема в архитектуре.
                              0
                              ну почему устаревшее, возьмём DirectoryServices.AccountManagement.
                              Мы сейчас написали обёртки с подменой реализации fake/real, а с Moles теоретически могло быть всё попроще.
                              Но учитывая что это ещё какаято 0.9* версия то в production такое не возьму, придётся ждать релиза, судя по последней активности ждать уже недолго.
                              0
                              Надо ли создавать тесты для юнит тестов

                              Если вы не уверены в своем unit-тесте, если у вас появляется желание проверить правильность его работы — это плохой тест, его нужно переписать.
                              Unit-тесты должны быть максимально просты, чтобы для уверенности в их правильности хватало, грубо говоря, взгляда и запуска, то есть должна быть круговая порука — 1) тесты тестируют ваш код; 2) глядя на ваш код и тесты, вы понимаете, что тесты верны. тесты и код тестируют друг друга.
                                +1
                                А почему вызов DateTime идет внутри функции, мне кажется в этом и есть загвоздка того что вы исполняете танец с бубнами чтобы написать Unit-тестирование :-)
                                Думаю вопрос решается если передавать извне DateTime :-)
                                  +3
                                  Спасибо за пост, очень интересная тулза, где-то может оказаться полезной. Но мне кажется, увлекаться такими тулзами не стоит. Надо понимать, что писать тестируемый (и поддерживаемый, ликвидный) код они не помогают, а скорее наоборот. Вот этот мануал (State of the Art Testability) можно покурить на досуге (я это делаю периодически), объясняет основные принципы на конкретном примере
                                    0
                                    Отличная ссылка, спасибо!
                                    0
                                    А можно поподробнее про «специальным образом подготовленную заглушку для сборки MSCorLib»?
                                      0
                                      Функция которая зависит от времени суток (погоды на марсе) — сама по себе должна насторожить. Вы правильно делаете, что насторожились, но переделываете/советуете неправильно.

                                      А надо ИМХО вынести логику связанную с временем за рамки функции PurchaseItem(), потому что этот метод должен покупать (и зависисеть только от параметров), иначе вы должны были ее назвать PurchaseItemBetween6PMAnd9PM(), да и то при этом лучше было бы передавать время параметром PurchaseItemBetween6PMAnd9PM(time currentTime).

                                      Я допускаю что это должно быть не так в API (но там свои методы, а ля контроллер), но в модели то всё должно быть четко и без магии (чтобы с магией не проверять).
                                        0
                                        Друзья, статья не про архитектуру и паттерны проектировоания. И я отметил в тексте что по одним из корректных методов обхода таких «острых» углов является использование Visitor.

                                        Конечно, по хорошему тесты и должны направлять разработчика создавать код который не содержит таких решений. Но в жизни всякое бывает, и в каких то случаях Moles позволяет решить эти проблемы.
                                        0
                                        Спустя время некоторые картинки в твоей статье пропали. Перезалей, пожалуйста.

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