Как научиться писать тестируемый и сопровождаемый код — отвечают эксперты

Аватар Никита Прияцелюк
Отредактировано

Все слышали про важность тестируемого и сопровождаемого кода, но не все понимают, о чём идёт речь. Спрашиваем у экспертов, как писать такой код.

9К открытий10К показов

В вакансиях практически всегда есть требование «писать тестируемый и сопровождаемый код», но начинающие программисты далеко не всегда понимают, что это такое. Как начинающему программисту научиться писать качественный код, который будет легко сопровождать и тестировать?

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

  1. Оформление кода, наименование переменных, классов и т. д. осуществляется в соответствии с принятыми для данного языка разработки нотациями.
  2. Код структурирован и разбит на модули. Очень полезно периодически делать рефакторинг кода, оптимизацию его структуры.
  3. Комментарии: к классам, методам, алгоритмам. Не ленитесь писать комментарии, ведь то, что кажется очевидным сейчас, может стать совершенно непонятным вам же самим спустя какое-то время или кому-то ещё, кто будет разбираться в вашем коде. Кроме того, есть очень много средств (в том числе, интегрированных в платформу разработки), позволяющих генерировать документацию на основе комментариев в коде. Обязательно используйте эти средства!
  4. Не забываем про вывод отладочной информации и «юнит-тестинг».

И ещё небольшой совет: навыки написания «правильного» кода можно очень быстро «прокачать», применяя практику парного программирования (когда один код пишут и сопровождают несколько разработчиков) или поучаствовав в каком-нибудь Open Source проекте.

Можно написать целую книгу, отвечая на этот вопрос. Всё зависит от конкретной проблемы. Тем не менее, если выбирать один совет, который подойдёт в девяти из десяти случаев, то я бы рекомендовал писать код так, чтобы каждая функция выполняла только одно действие — очевидное из названия.

Это основная проблема, с которой я сталкиваюсь, работая с начинающими программистами. Например, часто бывает так, что метод API делает всё сразу — обращается к БД, обрабатывает результаты и отгружает их дальше. Чтобы написать тест к такой функции, надо иметь копию всей экосистемы приложения исключительно для тестирования. Порой это просто невозможно. Намного лучше будет разбить такую функцию на мелкие «возьми», «распредели» и «отправь». Тогда «возьми» и «отправь» не надо unit-тестировать, потому что они не выполняют никакой логики, а «распредели» будет легко тестироваться, так как она не обращается к внешним ресурсам, и ей можно передать любые тестовые данные. Это называется «чистая функция».

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

В идеале ваш код будет выглядеть как инструкция по сборке конструктора. Это, пожалуй, самая важная характеристика легко сопровождаемого кода. Это философия, которой придерживается Apple со своими километровыми названиями — читать чужой код на их языках очень просто. Существует даже такая аналогия: программный код как анекдот — если его надо объяснять, значит он не очень хороший.

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

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

С «тестируемым кодом» всё сложнее, так как подходы к тестированию кода могут быть весьма разнообразными. Тем не менее, когда говорят о «тестируемом коде» обычно речь идёт о написании тестов для отдельных сущностей (методов, классов, модулей) программистом, возможно, не участвующим в самой разработке. Для того, чтобы он смог написать эти тесты, тестируемая сущность должна быть ему понятна и подвластна, т. е. не должна выполнять операций неявных и таких, на которые нельзя повлиять из тестов. Отсюда возникают рекомендации по написанию тестируемого кода: следить за соответствием имён сущностей их функциональности, обеспечить возможность вставить в качестве каждой сущности заглушку, не делать неявной инициализации и т. п.

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

На мой взгляд, в вакансиях на позицию junior-разработчика такое требование встречается нечасто. А если оно там есть, то указано скорее «для галочки». Дело в том, что умение писать «тестируемый и сопровождаемый код» в любом случае требует определённого опыта. Когда мы говорим о таком коде, то подразумеваем, что он написан определённым количеством людей, а проект существует уже довольно давно — разработчики поддерживают его на протяжении года-двух. Как правило, начинающий специалист не имеет таких проектов в своём портфолио, отсюда и возникает непонимание.

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

  1. Читать специализированную литературу и форумы. Крайне важно разобраться с терминологией и понять, что такое тестируемость и сопровождаемость. Также разработчику нужно изучить критерии качественного кода, ознакомиться с SOLID. Это принципы проектирования, которые объясняют, например, как сделать классы расширяемыми и как работать с зависимостями в них. Всё это имеет отношение к тестируемости кода.
  2. Найти ментора — опытного специалиста, который смог бы подсказывать, как написать код лучше. А главное — объяснить, почему стоит делать так, а не иначе: почему один код можно протестировать, а второй никуда не годится.
  3. Ознакомиться с исходниками известных open source проектов. Как правило, качество кода в них довольно высокое, и эти проекты существуют не один год.

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

Для начала надо определить, какой код мы считаем хорошим. Можно выделить три главных признака:

  1. Хороший код описывает сам себя. Чем понятнее, последовательнее и логичнее код, тем лучше. Комментарии в коде приветствуются, но названия переменных должны говорить сами за себя — код пишется и для разработчиков, которые с ним будут работать.
  2. Хороший код не переусложнен. Он максимально прост и по исполнению (затратам компьютерных ресурсов), и с визуальной точки зрения. В написании кода соблюдён баланс наиболее простых структур данных и наиболее оптимальных алгоритмов.
  3. Хороший код не избыточен и не повторяет себя. Помним о принципе «Don’t repeat yourself», чтобы избежать повторения кода с помощью абстракций и нормализации данных. И о принципе YAGNI («You ain’t gonna need it»), чтобы отказаться от избыточной функциональности без острой необходимости.

Важно понимать, что тестирование ведёт к хорошему коду, но ещё не является признаком хорошего кода. В процессе тестирования можно выявить слабые места реализации функциональности. Отсюда вопрос: как хорошо тестировать методы? Мы тестируем:

  1. Общие требования функциональности. Метод принимает такие параметры, а возвращает то, что нужно в желаемом виде.
  2. Пограничные условия. Надо следить, не выходит ли алгоритм за границы массива данных. Сюда же относится обработка null-значений.
  3. Выбросы исключений и корректность их обработки.

Для контроля качества кода есть несколько простых инструментов. Например, линтеры — инструмент для контроля стилистики кода, а также code-coverage инструменты, которые позволяют оценить доли кода, покрытого тестами.

По вёрстке — однотипная функциональность реализовывается однотипным HTML-деревом с применением читаемых атрибутов. Возьмём для примера кнопочки на сайте.

Пример #1:

  • плохо: в одном месте a , во втором — button, в третьем — div.link>span>a. И все они одинаковы визуально и физически, но в душе/DOM — разные;
  • хорошо: строго один из вариантов, с которым согласился набор кодревьюверов команды.

Пример #2:

  • плохо: <button type="submit" class="action" name="send" id="send2"><span>Sign In</span></button> — атрибуты ничего полезного не говорят. Какая-то кнопочка, которая что-то отсылает. И ещё со замечательным айдишником (привет форме авторизации Magento 2). Да, в этом случае можно завязаться на текст, но это стрёмно-узкое решение, зависящее от локали. Нужна универсальность;
  • хорошо: <button class="action" type="submit" data-qa-id="set-form-login">Sign In</button> — добавляем отдельный атрибут в тег, который никто не трогает, по которому можно понять, к чему относится соответствующий элемент. Название атрибута и стилистика написания его значений вариативны: вариант свыше можно сократить до data-qa-id="login" , но тогда в наборе локаторах для UAT будет локатор с выбивающейся из общего ряда доп конструкцией: //button[@type='submit'][@data-qa-id='login'].

Самые главные требования:

  1. Отдельный атрибут, к которому нет привязок CSS/JS со стороны приложения, вследствие чего мал шанс непреднамеренного изменения его значения;
  2. Значение атрибута предельно ясно объясняет роль/действие элемента.

Начнём с понятия сопровождаемости кода. Если кратко, сопровождаемость кода определяет, насколько легко и просто посторонний человек может разобраться в том, что творится в предоставленном ему коде. Хорошо сопровождаемый код также называют «чистым». Чем лучше и чище написан код, тем легче вникнуть в задачу новому сотруднику и тем меньше времени и усилий требуется создателю кода на объяснение, что в нём происходит. В идеале обращения к создателю быть не должно, всё должно быть понятно по самому коду и комментариям.

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

Следующий шаг — изучение правил стилистики языка, на котором пишет разработчик. Для каждого языка есть общие правила, которые закладываются его создателями, а крупные фирмы, такие как Google, Yandex, Airbnb, имеют свой свод правил касательно того или иного языка программирования. Полезно изучить какой-либо из предлагаемых наборов правил и придерживаться его. Во-первых, это тренирует дисциплину, во-вторых, вероятность, что придётся работать с кем-то, кто знает и использует такой же свод правил выше, чем что-то придуманное самим. И третий шаг — это подробное изучение стиля, используемого в команде, в которой работает разработчик. Скорее всего, в команде разработки, в которую он придет, уже будет сложившаяся стилистика написания кода. Чтобы не возникало конфликтов и другим коллегам было легко и удобно разбираться в коде, лучше следовать устоявшимся правилам. Если они описаны в документации — это отлично, если документации нет — можно проанализировать код написанный другими сотрудниками и делать так же.

Также в помощь программисту сделано много вспомогательных утилит, которые следят за качеством кода и оповещают о нарушениях, например ESLint для JavaScript, Flake8 для Python и т.д. Конфигурационные файлы для этих утилит легко редактируются и распространяются внутри команды, что позволяет легче внедрять единую стилистику.
Со временем использование этих правил и инструментов войдёт в привычку и усилия на их соблюдение сведутся к нулю.

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

  1. Код должен быть модульным. Каждый метод должен выполнять одну задачу, принимать определённый набор параметров и выдавать результат определённого типа. Таким образом мы можем легко написать тест под указанное поведение.
  2. Не нужно заставлять метод выискивать значения в переданных аргументах. В метод нужно передавать только то, что он будет использовать. Пример: передача объекта с множеством ключей, в котором метод использует только одно, глубоко зарытое: object.key.anotherKey. Это затрудняет как сопровождение кода, так и написание тестов под него.
  3. Не нужно использовать глобальные переменные. Глобальные переменные трудно отслеживать и изменение её в одной части кода может пагубно сказаться на другой. Также сами тесты могут перезаписывать их, тем самым влияя на последующие тесты. Исключение составляют только константы, которые не изменяются в процессе работы.
  4. Конструктор должен заниматься только инициализацией. Добавление в конструктор каких-либо расчётов может замедлить инициализацию и тем самым тестирование. Выносить часть функциональности конструктора в отдельный метод тоже плохая идея, т.к. это затрудняет понимание кода и приходится добавлять тесты для вынесенного куска.
  5. И напоследок, нужно помнить, что тесты должны быть быстрыми, иначе из полезного инструмента они превращаются в источник непрерывной боли, поэтому важно рефакторить код с учётом того, что под него пишутся тесты. Желательно как можно больше сторонних библиотек, которые тестировать не нужно, заменять моками, имитациями этих библиотек, которые без обработки информации отдают заранее подготовленный набор данных. Это помогает существенно ускорить работу тестов, избавиться от зависимости от стороннего кода и тестировать только то, что написано разработчиком.

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

Есть множество интересных методик для написания хорошего кода. Я перечислю ряд практических приемов, которые могут вам в этом помочь.

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

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

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

  1. Последующий рефакторинг не должен ломать прохождения тестов или требовать минимальных изменений в коде тестов.
  2. Зачастую процесс добавления новой функциональности должен приводить к падению части тестов, если этого не происходит, стоит насторожиться.
  3. При использовании внешних ресурсов, будь то базы данных или сторонние сервисы, весь такой код должен быть вынесен в отдельные клиенты с возможностью замены реализации при проведении тестирования.

Одна из самых сложных задач — разделение кода по сущностям и зонам ответственности. Зачастую преждевременная оптимизация и неправильное выделение сущностей приводит к появлению несопровождаемого кода. При добавлении кода в существующий продукт или использовании готового фреймворка сначала необходимо провести поиск схожих решений задачи или найти возможность переиспользовать текущий код. Придерживаясь общей стилистики проекта, вы сильно облегчите работу вашим коллегам.

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

  1. Реализация задачи в виде последовательно выполняемого кода без использования сложных архитектурных решений. Зачастую это будет набор функций, код в которых связан общими задачами. Здесь не стоит мельчить. На этом этапе тесты уже должны быть написаны.
  2. Выделение связанного кода в блоки, отдельные методы. На данном этапе самое время обнаружить зависимости, которые были изначально упущены из виду.
  3. Устранение всего дублирования кода. Поиск блоков, реализующих одни и те же или схожие действия.
  4. Выделение сущностей, слоёв. Как минимум логическое разделение функций по зонам ответственности.
  5. Выбор наиболее подходящих паттернов и подходов к структурированию кода.
  6. Проведение оптимизации, но не ранее чем будет проведено исследование производительности и анализа полученных результатов.

Выбор момента остановки процесса рефакторинга — одна из самых сложных задач. Процесс может привести к излишнему усложнению, что в свою очередь затруднит восприятие другими людьми. Зачастую стоит остановиться на 4-ом шаге и попросить совета у более опытных коллег. В определённый момент вы можете осознать, что допустили ошибку на одном из шагов. Это нормально, вернитесь назад и начните процесс заново.

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

Давайте разберёмся в терминологии:

Тестируемый код — это код, который покрыт модульными тестами. Модульные тесты — это тесты, которые позволяют проверить функции, методы, классы, компоненты. Выполняют ли они ту функциональность, которая от них ожидается при указанных вводных параметрах.

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

Есть несколько групп людей, которые могут оценить ваш результат:

  • владелец продукта;
  • аналитик (оценивают вашу работу по критериям приёмки);
  • коллеги-программисты (для них важен единый стиль вашего кода с остальным кодом, а также отсутствие ошибок с точки зрения программирования);
  • тестировщики (следят, чтобы код не ломал ранее реализованный потенциал).

Как видите, критерии оценки вашей работы у всех 3 групп разные, однако объединяет всех одно — абсолютно всем нужен тестируемый и сопровождаемый код.

Переходим к тому, как же его написать. Для этого необходимо соблюдать простые правила. Их вы найдете в Code Style проекта, также можно воспользоваться наработками других компаний. Многие правила универсальны, некоторые применимы к определённым языкам, но все они помогают делать код читабельнее, проще для восприятия другими разработчиками и избегать багов.

Что поможет вам писать сопровождаемый код:

  • изучение Code Style проекта — на GitHub много примеров для каждого языка программирования (Airbnb, Google и другие),
  • обязательное прохождение код ревью,
  • знакомство с кодом проекта, написанным более опытными коллегами,
  • изучение паттернов проектирования.

Для того чтобы ваш код было удобнее покрывать тестами, придерживайтесь следующих рекомендаций: используйте чистые функции, выносите общий код в утилиты, не передавайте в методы лишнюю информацию, отделяйте логику и визуализацию от модели, старайтесь не использовать singleton и неявные зависимости.

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

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

Итак, как писать сопровождаемый и тестируемый код?

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

  • читайте специализированную литературу и форумы;
  • найдите ментора — опытного специалиста, который подскажет, как писать код лучше и сможет объяснить, почему нужно делать именно так;
  • посмотрите на код известных open-source проектов, в особенности тех, у которых есть правила по внесению изменений вроде предпочитаемого стиля кода;
  • оформляйте код согласно принятым в языке правилам, руководствам по стилю. Некоторые компании применяют свой набор правил, придерживайтесь их в таком случае;
  • с оформлением кода также могут помочь различные инструменты вроде ESLint для JavaScript или Flake8 для Python;
  • не пихайте всё в один файл — структурируйте код и разбивайте его на модули;
  • избавьтесь от дублируемого кода;
  • передавайте в методы только ту информацию, которая им нужна;
  • пишите комментарии к вашему коду. Это поможет как людям, которые будут читать/изменять код после вас, так и вам самим через неделю после его написания. А ещё из правильно оформленных комментариев можно автоматически генерировать документацию;
  • пишите тесты;
  • пишите быстрые тесты. Иначе из верного друга они превратятся в злейшего врага;
  • избегайте использования глобальных переменных. Их сложно отслеживать, а изменение одной части кода с ними может негативно сказаться на остальном коде. Также такие переменные могут перезаписываться тестами, создавая ненужную путаницу;
  • не забывайте про практику. Простое чтение теории не сделает вас экспертом в написании тестируемого и сопровождаемого кода.

Напоминаем, что вы можете задать свой вопрос экспертам, а мы соберём на него ответы, если он окажется интересным. Вопросы, которые уже задавались, можно найти в списке выпусков рубрики. Если вы хотите присоединиться к числу экспертов и прислать ответ от вашей компании или лично от вас, то пишите на experts@tproger.ru, мы расскажем, как это сделать.

Следите за новыми постами
Следите за новыми постами по любимым темам
9К открытий10К показов