Модульное тестирование — личный опыт

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

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



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

    А заметил я это следующим образом: начинаю писать код с желанием покрыть его юнит-тестами. В другой ситуации накатал бы несколько страниц говнокода, лишь бы работало и выполняло то что требуется. Но из-за намерения использования юнит-тесты приходится выделять интерфейсы, создавать на всё отдельные службы, абстрагироваться от UI, применять всяческие шаблоны типа IoC, DependencyInjection, ослабляющие связанность системы и т. п.

    Я знаю идею про то что сперва нужно писать тесты, а потом уже код, и поначалу так и делал. Но когда объем кода очень большой — уже не до этого. В итоге тесты я стал писать только в том случае, когда в коде обнаруживаются ошибки. Заглючил метод — пишу на него тест. Глюков нет — не пишу. Таким образом я как бы реализую правило «не писать тесты для тривиального кода».

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

    И именно это стремление к потенциальной возможности написания юнит-тестов задает нижнюю планку качества проектирования приложения.

    Случалось работать с унаследованным кодом — нужно было его поддерживать и улучшать. Обычно выяснялось, что это «банка с червяками» — там всё взаимосвязано так, что изменения в одном месте самым непредсказуемым образом откликались в других местах. Возникало естественное желание (по науке, как в книжках) перед изменением такого кода покрыть его тестами. Но как покрывать, если для создания экземпляра класса, бывает, требуется создать десяток других классов, которые еще тянут за собой сотню других? И даже mockи не всегда помогают. Вот и получается, что для запутанного кода создать полноценные тесты крайне сложно — он изначально для этого не предназначен.

    Еще по теме: статья habrahabr.ru/blogs/development/51706

    Резюме: пишите код так, как будто вы его на самом деле собираетесь покрывать юнит-тестами :)
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну, и что?
    Реклама
    Комментарии 50
    • +1
      Смысловая нагрузка топика — «модульные тесты хороши», для чего это тут? Я то думал будет какой-то реальный опыт.
      • +6
        Смысл в том, что хороши не только сами тесты, а намерение их использовать
        • +1
          Это да, понимание об этом приходит после попытки покрыть тестами «легаси» код, в результате чего покрытие тестами становится частью рефакторинга, а так же полезно для более детального понимания какой-нибудь системы которая расширяется / дорабатывается.
      • +9
        И возразить то нечего.
        Спасибо, Кэп.
        • 0
          Наивно пологать, что какой-то код тривиален. Например, с первого взягляда этот код — тривиален, но не уж-то в нём нет потенциального бага?
          if (isGarageAvailable() & carCounter++ > availableSpace) {
               throw new GarageIsFullException();
          }
          
          • +3
            Что тривиально или нет — может определить только сам программист по ходу дела, исходя из личного опыта и понимания текущего проекта. По-моему, общий критерий «тривиальности» сформулировать не удастся.
            • –1
              хороший пример
              • +5
                Если бы я такой «тривиальный» код встретил в рабочем проекте, то немедленно по рукам автору надавал бы…
              • 0
                Поясните учащемуся — тут дело в пост-инкременте или в &?
                • +1
                  Мне кажется, зависит от контекста. По приведённому отрывку кода сложно сказать, что обозначает переменная carCounter. Это количество машин в гараже, количество машин, ожидающих помещения в гараж, общее количество машин?..

                  В любом случае я вижу 2 сомнительные вещи:
                  1) если carCounter > availableSpace, то carCounter всё равно увеличивается на 1, после чего кидается исключение.
                  2) если !isGarageAvailable(), то carCounter всё равно увеличивается на 1.
                  • 0
                    Понятно, спасибо.
                  • +1
                    в том, что при проверке увеличивается счетчик количества машин в гараже, даже если проверка не пройдена.
                    • 0
                      Дело в том, что & — это битовая операция «И», а "&&" — логическое «И». Если для С++, насколько я помню, результат будет идентичен, но для Java — это не так.

                      На самом деле, я в своём примере хотел описать другую ошибку. В компиляторе java байткода, конечно же есть специальные приёмы оптимизации. Так вот если в выражении с логическом «И» первая часть будет false, то вторая часть даже не будет выполняться, следовательно, инкрементное приращение счётчика никогда не будет выполнено.

                      if (isGarageAvailable() && carCounter++ > availableSpace) {
                           throw new GarageIsFullException();
                      }
                      
                      • 0
                        И для Java, и для C результаты применения операторов & и && к логическим выражениям будут идентичны (помимо указанной особенности второго оператора не выполнять вторую часть выражения, если первая равна false).
                        • 0
                          Да вы правы, варианты на подобии null & false даже статический анализатор компилятора не пропускает.
                  • +1
                    В основном разработка проекта ведётся командой, а не одним программистом, поэтому критерий «программист определяет тривиальность по ходу дела» также является ошибочной.
                    Кстати, есть одни интересное и неформальное определение legacy кода — это «код, которвый вы написали месяц назад». Или с вами не когда не случалась ситуация, когда вы гововорите «Кто написал такую ерунду?», смотрите на аннотацию строчек, а там стоят ваши инициалы.
                    • 0
                      Какой выход Вы предлагаете? 100% покрытие?

                      Программист, обычно, существо с мозгом. И определить тривиален ли код он может хоть и не всегда, но часто. И этого вполне достаточно.

                      Ну а если уж одна функция из 100, якобы тривиальных и непокрытых, оказалась непростой и «стрельнула», то затраты на исправление частенько значительно меньшие, чем на написание тестов на все 100 функций.

                      ЗЫ
                      Особый случай написания софта для ракет запускающих в небо ГЛОНАСС не рассматриваю.
                      • +2
                        100% покрытие тестами — это из области фантастики. Но не потому, что кто-то решил что один код тривиален, а другой нет, особенно в проектах, которые не начинали свою разработку с использованием TDD. К тому же, покрытие тестами — это не цель, это всего лишь метрика. Цель покрытия тестами — это встроить качество в процесс разработки.
                        • 0
                          Ни фига подобного, 1 тестом даже без ассертов можно покрыть 100% кода. Проверено.
                          • 0
                            Что же то за тест такой без assert-ов? В таком случае вам придётся очень часто проводить кучу времени в дебагере.
                            • 0
                              Забейте болт вообще на значение процента покрытия, CRAP вот та метрика на которую нужно ориентироваться при написании тестов. И да я провожу иногда неприлично много времени в деббагере так как в комментах все профессионалы от бога, но кто же пишет тот код который я потом сижу и дебажу? Инопланетяне?
                              • 0
                                Я так понимаю метрика CRAP, это из той же области что и метрика «не пишите тесты для тревиального кода».
                                Извините, но я всё равно не понимаю, как можно писать тесты без assert-ов, что же вы тогда проверяете своими тестами? Зачем они тогда нужны?
                                • 0
                                  Да из той же, боунс в том что думать нужно меньше. Тесты без ассертов это вариант как добиться 100% покрытия тестов если цель процент покрытия, но да это абсурд.
                      • 0
                        Если команда — тогда у каждой команды своя планка тривиальности.

                        Впрочем, пост не об этом, а о косвенной пользе потуг поддержки покрытия кода юнит-тестами.
                        • 0
                          Точнее — о взаимосвязи юнит-тестирования и «хорошей» архитектуры
                          • 0
                            Не бывает ровных команд. Поэтому надо писать тесты думаю о других.
                        • 0
                          Резюме: пишите код так, как будто вы его на самом деле собираетесь покрывать юнит-тестами :)

                          Пишите код так, как будто читать его будет сексуальный маньяк с садистскими наклонностями, и он знает адрес где вы живёте.
                          • 0
                            об этой идее подробнее можно почитать в книге GOOS: www.growing-object-oriented-software.com/
                            • 0
                              Моё мнение, писать тесты после написания кода — мало того, что скушно, долго, так ещё и тесты будут не полностью валидными. Мазохистический саммобман.

                              А вот писать из ДО написания кода — вот где истинный путь.
                              • +3
                                да ладно.
                                а вы ДО написания кода на 100% уверены во всех его интерфейсах?

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

                                И приходим к известной дилеме «прототипирование vs проектирование». Что подчас быстрее написать прототип модуля, по ходу столкнувшись с проблемами и получив в итоге хорошие и цельные интерфейсы, и потом уже протестировать эти интерфейсы.
                                Никто при этом не говорит, конечно, что проектировать — плохо.

                                Так что я бы на вашем месте удержался от «унижения» тех, кто, с вашей точки зрения, не достиг еще дзена. Потому что у «недостигших» тоже есть свои аргументы.
                                • 0
                                  Независимо от того, что раньше родилось — тест или код — впоследствии они идут взаимосвязанной парой, изменяя одно приходится менять другое. А правило «сперва тесты, потом код» я для себя переформулировал в «сперва думать, потом делать».
                                  • +1
                                    Что ладно-то?
                                    Что значит «уверен в интерфейсах»? Обычно интерефейс диктуется уже существующим кодом.
                                    В любом случае, пишите сначала Acceptance тесты, если изменения влияют на внешнее поведение (т.е. это не рефакторинг). После пишите базовые тесты. Делаете скелет нужной структуры, минимальный, так что бы тесты фейлились, но отрабатывали. Потом пишите уже код, так, что бы тесты проходили. Если становится понятно что нужно что-то добавить, чего вы не предусматривали — пишете на него тест, делаете скелет, тесты фейлятся, пишите реализацию.

                                    Мы стараемся писать тесты так, что бы 1 тест проверял 1 аспект.
                                    На ошибку тоже пишем тест.
                                    Если есть момент взаимодействия с другими частями, не забывайте писать integration тесты.
                                    Старайтесь писать так, что бы тесты были по минимуму завязаны на детали реализации. Тут важно понимать какой уровень тестируете.
                                    Не бойтесь небольшой избыточности в тестировании — просто следите что бы каждый тест тестировал свой уровень абстракции.
                                    Пользуйтесь coverage-утилитами.

                                    У меня всё.
                                    • 0
                                      Тесты пишутся как раз для того, чтоб не думать на пять шагов вперед и на ранней стадии выявить недостатки прототипа интерфейса.
                                      • 0
                                        «а вы ДО написания кода на 100% уверены во всех его интерфейсах?»
                                        Вообще-то, это не нужно. Бек прекрасно показывает, как именно меняется интерфейс системы по мере реализации сценария — и все это продолжает происходить под юнит-тестом.
                                    • 0
                                      Диалог:
                                      1 — перед рефакторингом необходимо убедиться в покрытии кода тестами
                                      2 — а если тестов нет?
                                      1 — …
                                      1 — тесты нужно написать основываясь на спеках
                                      2 — а если спек нет?
                                      1 — …
                                      1 — нужно уточнить требования у клиента
                                      2 — а если клиент не знает?
                                      1 — …
                                      1 — рефакторинг делать нельзя
                                      2 — а нужно…
                                      • 0
                                        1 — Ой, ну ты меня переспорил, великий спорщик. Приятного тебе секса с твоим проектом.

                                        • 0
                                          ну как бы да, ваши розовые мечты разобьются о чугунную задницу реальности. мораль перекликающаяся с изначальным постом в том что проект который заточен с расчетом на тестирование легче поддерживать не только за счет тестов но и за счет того что код с такой заточкой изначально будет «лучше». И наоборот — если тесты не планировались изначально то с большой вероятностью отсутвие тестов не будет единственым минусом.
                                          • 0
                                            Верно, но ни одно сказанное Вами в этом комментарии предложение никак не просматривается в Вашем первом комментарии. Они вообще не связаны по смыслу.
                                        • +1
                                          Смешались в кучу кони, люди…
                                        • –1
                                          плохо спроектированный код почти невозможно автоматически тестировать, и наоборот — намерение тестировать код вынуждает более грамотно проектировать архитектуру

                                          Кажется, это очередной миф. Формально, тест работает с реализацией как с черным ящиком: для теста все равно — хоть там архитектурное чудо, хоть путанное-перепутанное «спагетти».
                                          • 0
                                            Да в том-то и дело, что далеко не для каждого кода можно вообще нормальные тесты создать. А если у вас самодостаточный «черный ящик» который удобно тестировать — тогда да, всё равно как оно там внутри устроено
                                            • 0
                                              Да, но некоторые вещи типа констант это палка в колеса при написании теста, в результате ты либо не используешь из изначально либо убираешь в последствии.
                                            • 0
                                              Для чужого кода есть хорошая практика: необходимо перед рефакторингом зафиксировать его поведение юнит тестами на модули верхнего уровня или тестами черного ящика. И еще есть очень простое (но не совсем очевидное) правило: модифицировать можно либо тест, либо код, но не то и другое одновременно.
                                            • 0
                                              Резюме: пишите код так, как будто вы его на самом деле собираетесь покрывать юнит-тестами :)

                                              Странное резюме. Если вы знаете TDD (об этом ссылка в топике), то зачем писать «как будто», когда можно писать сначала тесты, потом код?
                                              • 0
                                                Это совет тем, кто пренебрегает тестами
                                                • 0
                                                  Это из серии «Книгу не читал, но хочу заметить...»? xD
                                                  • 0
                                                    упс, пропустил его аргумент «мне лень писать тесты для всего» %)
                                                    Такой подход не есть хорошо по трем простым причинам:
                                                    1) «Писать код, как будто вы его тестируете» это как вообще? Думать про моки, кейсы и ассерты? Но ведь TDD не заставляет писать код правильно. Оно лишь дает обратную связь, по которой можно сделать вывод о принятом архитектурным решении. Нет тестов — нет обратной свяи — говнокод.
                                                    2) Не покрывая бОльшую часть кода тестами, изменять (в том числе рефакторить) код будет невозможно. После одного изменения, которое что-то поломает, тесты скажут всё ок, грин бар, потому, что автор посчитал излишними для тестирования эту часть кода. (это самое ужасное, когда тесты лгут что все нормально, а через некоторое время выясняется, что что-то работает совсем не так, как ожидается)
                                                    3) Унит-тесты не только проверка на то, что ничего не ломается, это еще и отличная документация как пользоваться этим кодом.

                                                    Поэтому резюме должно быть:
                                                    Читать про TDD и четко следовать его принципам.

                                                    ps: о 2-ом пункте писали уже, но для полноты пусть будет еще раз.
                                                • 0
                                                  Заметил, что авторы большинства комментов на посты модульному тестированию делятся на 2 части: те кто убеждают делать тестирование и описывают преимущества и те, кто делать их не хочет и описывает сложности и тщетность этой работы.

                                                  Сразу вспоминаетсы высказывание: «Кто хочет делать работу — найдёт средства, кто не хочет — найдёт отговорки».

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

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

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