Ключевые тезисы

  • Код всегда можно протестировать, если выявить антипаттерны и исправить их.

  • Архитектура и тестируемость кода влияют на возможность автоматизировать тесты.

  • Решения по архитектуре кода принимаются разработчиками, а тестировщики, в свою очередь, могут влиять на эти решения в целях улучшения тестируемости.

  • Практика чистого кода и тестируемость взаимосвязаны, что выгодно обеим сторонам, и разработчикам, и тестировщикам.

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

  • Тимлиды и менеджеры должны способствовать совместным обсуждениям как неотъемлемой составляющей улучшения процессов.

При написании автоматизированных тестов всегда есть вероятность, что мы столкнемся с какими-либо трудностями. Например, тесты не проходят, или их реализация потребует неоправданно много наших сил. “Этот код не тестируем”, — сказали бы мы в такой ситуации. Но в большинстве случаев это не так. Код можно протестировать всегда, но цена этого может быть очень высокой, а работа над этим изматывающей.

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

Могут ли тестировщики повлиять на то, как будет написан код?

Это во многом зависит от отношений между тестировщиками и разработчиками. Сплоченной agile-команде свойственна открытость. Но зачастую тестировщики получают уже “готовый к тестированию” код от разработчиков спустя недели или даже месяцы после завершения работы над ним. На этом этапе перспектива просить разработчиков “вернуться назад во времени”, бросить их текущую работу, и вносить правки в “уже работающий (по их мнению) код”, особого энтузиазма не вызывает.

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

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

Паттерны кода, которые улучшают тестируемость

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

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

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

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

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

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

И это досадно. Как тестировщикам, нам необходимо предоставить стейкхолдерам максимально полную информацию. Если у нас нет возможности прогнать важные сценарии, они занимают слишком много времени, или они нестабильные, мы не сможем этого сделать.

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

Самое базовое, чему я обучаю, — это когда вместо внедрения зависимости следует использовать “new”. Если вы знаете, что это “тяжелая” зависимость, вам лучше заменить ее сейчас. Да, у этого есть цена, но когда вы заметили это сразу, обычно она не так велика. Некоторые риски все же присутствуют. Внося изменения в код (без тестов, поскольку мы делаем это для улучшения тестируемости), мы можем что-то сломать. Анализ изменений до и после их внесения может снизить риск, а обсуждение рисков с тестировщиком положительно скажется и на других аспектах тестируемости.

В качестве последнего примера приведу код с явными проблемами. Среди таковых можно выделить “божественные методы” и “божественные классы”, по наличию которых разработчик сразу понимает, что код сложен и слишком громоздок. Существует обратная связь между сложностью и масштабом кода и его тестируемостью. Всегда. Разработчикам нужен кто-то, кто бы указал им на это. Если они пишут свои собственные тесты, они очень быстро понимают это и меняют все. К сожалению, это не всегда так.

Внесение изменений в код для обеспечения тестируемости

Между практиками чистого кода и тестируемостью есть много общего. Модульная архитектура — хорошая практика как для расширяемости (мы можем заменять компоненты без необходимости в новом релизе), так и для тестируемости (мы можем подключить мок базы данных), что хорошо и для разработчика, и для тестировщика.

Есть изменения кода (в том числе и в целях улучшения тестируемости) в рамках процесса разработки, а есть, изменения, которые происходят уже “после разработки” (но это всего лишь наше восприятие, поскольку код почти всегда находится в разработке). По мнению разработчика, независимо от того, является ли изменением “стоящим”, иногда затраты (например, удаление ключевого слова Java “final”, делающее класс расширяемым) могут не казаться такими высокими или рискованными.

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

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

Преимущества бесконечны

Главное преимущество — это диалог между разработчиками и тестировщиками. Я помню, как когда-то, будучи неопытным тимлидом, мой проект скатывался в бездну, и я не знал, что делать. Проект спасло решение: посадить тестировщика и разработчика за один компьютер. Она не знала, как программировать, он не знал, как она тестирует, но когда они заговорили, случилось волшебство.

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

Что касается положительных изменений на техническом уровне, мы говорим о перспективе более качественного тестирования системы и возможности предоставить информацию о ней таким образом, который сегодня переводится как “у нас не было времени на тестирование”. И, как я уже сказал, качество продукта неразрывно связано с качеством кода.

Что могут сделать руководители технических специалистов или архитекторы для улучшения тестируемости

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

В ретроспективе обсудите, что будет “сложно”, а что “легко” протестировать. Определите это и выясните, почему. Мы можем узнать, какие похожие вещи нам нужно будет делать в следующий раз.

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


Материал подготовлен в преддверии старта курса "QA Lead".

Комментарии (1)


  1. OlegZH
    23.10.2021 22:34
    +1

    А можно задать пару-тройку недоумённых вопросов?

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

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

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