- Введение
- А что было до
- Первая интеграция и первые результаты
- Разочарование
- Доработка напильником
- Финишная прямая
- Где ручные кейсы, Карл?
- Dashboards and reports
- Заключение и планы на будущее
Приветствую тебя, мой виртуальный друг! Если ты читаешь эту статью, скорее всего тебе интересен Allure, или ты хочешь разобраться с тем, что это за зверь и как он интегрируется в тестирование без многонедельных плясок с бубном.
В этой статье хочу рассказать о том, как мы внедряли один из инструментов, который помогает (или пытается) сделать жизнь инженерам по обеспечению качества продукта немножечко легче: постараюсь рассказать в деталях с чего все начиналось, как шло внедрение, через что прошла команда, что в итоге получили и куда планируем двигаться дальше.
А виновником появления этой статьи, как нетрудно догадаться, стал тот самый инструмент — бывший Allure EE, Allure Server и нынешний Allure Testops.
Несколько лет назад я присоединился к одному очень интересному и амбициозному проекту. В команде было шесть разработчиков и два QA-инженера, включая меня. На тот момент тестовым стеком, который уже был внедрен и использовался в компании, оказался:
- php7.4 и framework Codeception,
- Framework Selenoid для управления браузерами
- В качестве CI/CD системы мы использовали Gitlab.
- Отдельной системы генерации отчетов как такой не было: для авто тестов мы использовали встроенный в codeception простой генератор отчетов (включается при передаче флага --html при запуске самих тестов), а для ручных тестов, мы использовали TMS Zephyr .
Соответственно приходилось смотреть два разных отчета и делать сопоставление между ручными и автотестами, что, сами понимаете, не очень-то удобно. Кроме неудобства в репортинге, постоянно возникали вопросы вроде “А как же скрестить ежа с попугаем?” Ладно, шучу! “А как же получить отчет в котором будет наглядный список автоматических и ручных тестов?” или “как же получить отчет в котором будет четко показано, что проверяют уже написанные автотесты?” Ответы на эти вопросы очень затягивались и в итоге сам стек для автотестов на тот период времени было принято не менять по причинам плотной интеграции и существующей базы автотестов, но вот касаемо отчетов и полноценной report-системы решили, что пришла пора переменам.
Главным преимуществом системы, которая изначально была у нас в стеке, напомню, это был Zephyr, была глубокая интеграция с JIRA. Интеграция позволяла вести всю тестовую документацию прямо не выходя из нее, настраивать дэшборды, используя JQL, писать тест кейсы прямо в таске и интегрировать их в “борду”.
Впрочем, были и минусы. Во-первых, очень сложно и неудобно писать/редактировать тесты в маленьком окошке (да-да, оно умеет настраиваться по размеру, но все равно неудобно, особенно при большом объеме документации). Во-вторых, мы не смогли найти ответ, как слинковать автотесты c ручными и настроить сквозные отчеты для нашей тестовой инфраструктуры. Эти недостатки и отправили нас в путешествие в надежде найти тихую гавань, в которой можно заниматься тестированием и качеством продукта, а не постоянной рутиной по сборке и допиливанию отчетов. Мы отправились искать другой инструмент.
Вторым заходом в бухту стал всем известный TestRail. При первом знакомстве с ним я был в восторге! В одном месте можно удобно хранить тесты, тест планы, делать прогоны ваших тестов и показывать красивые отчеты: тут и история создания тест-кейсов есть, и удобное редактируемое поле для шагов в самом тест-кейсе. Но и без ложки дегтя не обошлось — как с автотестами подружить отчеты и сделать единые прогоны? Да, у TestRail есть API-документация, которую мы использовали и даже реализовали свою версию (за что отдельное спасибо команде разработчиков, которая нам помогала в этом) интеграции с запуском и проставлением статусов у каждого теста. Просто в определенный момент пришло осознание, что мы уделяем интеграции и ее настройке уйму времени, а конечного результата на горизонте все еще нет. Тихой гавани не было видно, поэтому переезд номер три был не загорами.
В этот раз у нас не было права на ошибку и мы занялись серьезной подготовкой к выбору следующего инструмента:
долгий поиск в интернете, советы с коллегами, жаркие дискуссии и чтение книг, — все это привело нашу команду на одну
из конференций по тестированию, на которой выступал Артем Ерошенко. Доклад был
посвящен тому самому Allure EE, что это за инструмент, что он умеет и как это готовить. Впечатлившись выступлением, я
пошел разбираться, будет ли все это безобразие работать со стеком на нашем проекте.
Здесь стоит сделать отступление, что наша команда уже запланировала переезд на другой тестовый стек и
мы плотно над этим работаем, но об этом в другой раз.
Как выяснилось при внедрении — это оказалось одной из самых больших проблем, с которыми пришлось столкнуться. На момент нашего знакомства с Allure EE была реализована только часть функций, а адаптер для PHP был сыроват. Несмотря на эти недочеты, уже реализованная функциональность почти полностью покрывала наши требования, и, по сравнению с другими инструментами, которые мы пробовали, это был глоток свежего воздуха. Снимаемся с якоря, первые попытки интеграции не заставят долго ждать.
Пятница, поздний вечер… Всей командой сидим на работе? Какого ***?
Ответ прост — мы получили триальный ключик Allure TestOps! Развернув само приложение на отдельном сервере через образы docker-compose, мы наконец-то увидели форму авторизации, залогинившись в которой, получили пустой проект и полное осознание того, что впереди еще долгий путь.
Всю тестовую инфраструктуру надо поднимать и настраивать почти что с нуля, а потом еще и показать коллегам, как это будет работать. Понеслась!
Создали проект в UI, добавили в composer новую зависимость, создали ветку, пушнули, иии... магии нет :) Что же, придется почитать документацию и выяснить неочевидный факт — отчет надо загружать через allurectl.
С подключением зависимости в composer и описанием yaml-файла проблем не возникло, в репозитории есть пример, а вот как отчет грузить, это прям вопрос? Как я писал выше, у нас используется gilLab в качестве CI/CD системы и определенная пачка тестов бегает в docker контейнерах, а часть тестов — на тестовых стендах. Хорошо... Пытаемся интегрировать allurectl в docker и подружить его с gitlab-ом.
Создаем dockerfile, указываем путь к репозиторию allurectl и делаем этот файл исполняемым.
- Пример запуска скрипта allurectl:
FROM docker/compose:alpine-1.25.5
RUN apk add \
bash \
git \
gettext
ADD https://github.com/allure-framework/allurectl/releases/latest/download/allurectl_linux_386 /bin/allurectl
RUN chmod +x /bin/allurectl
Уже лучше, дальше описываем логику работы с пайплайном и запускаем скрипт (allurectl) после прохождения тестов на CI.
Для того чтобы в Allure Testops узнал про наши тесты, нужно описать работу двух джоб:
allure-start
и allure-stop
.
- Пример реализации джобы
allure-start
:
allure-start:
stage: build
image: ваш образ
interruptible: true
variables:
GIT_STRATEGY: none
tags:
- ваши теги
script:
- allurectl job-run start --launch-name "${CI_PROJECT_NAME} / ${CI_COMMIT_BRANCH} / ${CI_COMMIT_SHORT_SHA} / ${CI_PIPELINE_ID}" || true
rules:
- if: если нужны какие правила для запуска и ниже условия
when: never
- when: always
needs: []
- А allure-stop — так:
allure-stop:
stage: test-success-notification
image: ваш образ
interruptible: true
variables:
GIT_STRATEGY: none
tags:
- ваши теги
script:
- ALLURE_JOB_RUN_ID=$(allurectl launch list -p "${ALLURE_PROJECT_ID}" --no-header | grep "${CI_PIPELINE_ID}" | cut -d' ' -f1 || true)
- echo "ALLURE_JOB_RUN_ID=${ALLURE_JOB_RUN_ID}"
- allurectl job-run stop --project-id ${ALLURE_PROJECT_ID} ${ALLURE_JOB_RUN_ID} || true
needs:
- job: allure-start
artifacts: false
- job: acceptance
artifacts: false
rules:
- if: $CI_COMMIT_REF_NAME == "master"
when: never
Тут стоит добавить, что Allure TestOps создает папку allure-results в папке {имя вашего проекта}/tests/_output
которую формирует сам framework (в нашем случае codeception). И все что нужно сделать — загрузить артефакты,
сформированные после прогона тестов в Allure через консольную команду allurectl
.
.after_script: &after_script
- echo -e "\e[41mShow Artifacts:\e[0m"
- ls -1 ${CI_PROJECT_DIR}/docker/output/
- allurectl upload ${CI_PROJECT_DIR}/docker/output/allure-results || true
- Для нотификаций в Slack:
Чтобы отправлять нотификации к примеру об упавших тестах, достаточно передать launch id. Grep-нуть его можно примерно так:
if [[ ${exit_code} -ne 0 ]]; then
# Get Allure launch id for message link
ALLURE_JOB_RUN_ID=$(allurectl launch list -p "${ALLURE_PROJECT_ID}" --no-header | grep "${CI_PIPELINE_ID}" | cut -d' ' -f1 || true)
export ALLURE_JOB_RUN_ID
Вот так это может выглядеть у вас в Slack:
В итоге получаем результат — тесты начинают бегать в docker, используя локальное окружение. При этом запускается джоба allure-start. Как только allure-start пробежала в самом Allure, создается заглушка, в которую будут загружены тесты после их исполнения. Вот так может выглядеть пайплайн:
- Пример
acceptance.yaml
файла.
Тут достаточно подключить allure-ee как extensions и указать настройки конфигурации. Кстати, если у вас есть тесты c аннотацией @skip — добавьте эту строчку в конфиг, в противном случае будет ошибка.
extensions:
enabled:
- Yandex\Allure\Codeception\AllureCodeception
config:
Yandex\Allure\Codeception\AllureCodeception:
deletePreviousResults: false
outputDirectory: allure-results
ignoredAnnotations:
- env
- dataprovider
- skip
Итак, первый шаг интеграции готов. Смотрим первые результаты и пытаемся понять, что в итоге загрузилось в Allure? Есть тесты с шагами и бублик с разными статусами. Значит, запуск тестов на каждой ветке и конкретном sha-ref (коммите) работает, а еще на каждый коммит и создание ветки запускается набор тестов, который по итогам выполнения отправляет результаты в Allure. Allure в свою очередь рисует бублик со статусами выполненных тестов. Успех и маленькая победа!
На этом вечерние посиделки в пятницу были завершены, и мы с радостными мыслями удалились в бар по домам :).
Итак, тесты успешно бегают на бранчах и для каждого sha-ref (коммита) — восторгу нет предела. Впрочем, как известно, эйфория проходит быстро. Именно так и случилось. Мы полезли смотреть на дерево автотестов (к тому моменты они уже успешно бегали в Gitlab) и обнаружили совершенно нечитаемую историю, по которой никак нельзя понять, что в ней проверяется. В этот момент пришло горькое осознание, что мы сделали шаг вперед и два назад.
-
Примерно так выглядел результат после успешной загрузки xml-отчета в Allure: Само дерево строится по принципу
название тестового метода + data set #1 (если он есть в тесте)
. -
Шаги в тестах выглядят примерно так: Из этих шагов, естественно, ничего не понятно: что за
get web driver
, что заget helper
; зачем мы перезагружаем страницу и т.д., — если такие отчеты увидит кто-то из руководства, вопросов будет больше, чем шагов в тесте.
И сейчас вы наверняка думаете, что я расскажу, как в два клика мы победили эту "неувязочку" ? Нет!
Тут начинается боль… Боль инженера по обеспечению качества. Ребята из Qameta предлагают решить эту проблему при помощи аннотаций к тестам: определить, где у нас features, где story, а где component и для каждого теста выставить аннотации над методом. Такая разметка позволяет динамически строить красивое дерево.
А что насчет шагов? Аннотировать каждый? Для этого тоже есть решение, которое заключается в создании обертки для каждого метода. Эта обертка будет служить красивым и понятным именем для метода и шага. В целом предложения и подход здравые, но когда на проекте n^8-тестов, этот процесс может затянуться. По сути, разметка тестов для Allure требует практически полного рефакторинга всего, что есть в проекте! Очень много времени.
Также к минусам можно отнести отсутствие валидации этих аннотаций. Если вполне естественным образом пропустить опечатку в аннотациях, забыть закрыть кавычки или неправильно закрыть фигурные скобки, тест просто упадет со всеми вытекающими последствиями. Особенно болезненно будет тем, кто в проекте недавно и не очень хорошо представляет себе структуру тестового набора, и тем, кто занимается ручным тестированием и сильно далек от мира автоматизации.
P/s. В качестве решения валидации аннотаций, можно посоветовать установить плагин для phpStoprm PHP Annotations, который поможет найти подобные ошибки, но конечно же не все.
Ну что, дорогие читатели, напильник в руки и начинаем полировать заготовку! Добавляем аннотации к автотестам, по пути рефакторим код тестов, а так же согласуем с аналитиками и ВСЕМИ участниками вашей команды будущую структуру вашего дерева. Давайте посмотрим на то как это может быть выглядеть в коде на примере одного автотеста:
/**
* @Title("Название вашего теста - короткое")
* @Description("Пишем подробное описание")
* @Features({"А что за фича собственно?"})
* @Stories({"Ну про сторю не забываем!"})
* @Issue({А где требования Карл?})
* @Labels({
* @Label(name = "Jira", values = "SA-1111"),
* @Label(name = "component", values = "Наш компонет"),
* @Label(name = "layer", values = "the layer of testing architecture - вспоминаем мат часть"),
* })
То, что вы видите на скриншоте, это адаптированный стандарт документирования Javadoc для PHP. Теперь посмотрим, как это выглядит в Allure Testops:
На скриншоте видно, что тесты выстроены в структуре, а именно: Component -> Feature -> Story. Это предварительно настраивается в самом Allure TestOps.
В любой момент структуру можно отредактировать и дерево пересоберется в соответствии с запросом. Теперь пора навести
порядок с шагами в автотестах. Можно использовать
метод executeStep
как надстройку и красивое описание для нашего метода. В нашем примере в сигнатуру метода первым параметром передаем
текст, который будет отображаться как human readable step
в UI Allure для метода addInvite
(пример реализации):
public function createInviteCode(User $user, string $scheme): Invite
{
return $this->executeStep(
'Create invite code for ' . $user->username . ' with ' . $scheme,
function () use ($user, $scheme) {
return $this->getHelper()->addInvite(['user' => $user, 'schemeType' => $scheme]);
}
);
}
Таким образом, вся внутренняя реализация скрывается под капотом, а шаги в тестах "естественным" образом получают красивые и понятные названия. В итоге тестовый сценарий в Allure выглядит примерно вот так:
Важно отметить, что такой рефакторинг занял у команды примерно месяц интенсивной и упорной работы: приходилось даже засиживаться допоздна в надежде, что еще чуть-чуть и финишная прямая...
Подведем некий итог, что же в итоге получилось? Напомню, что изначальной задачей было приведение всего набора автотестов в порядок, чтобы их можно было спокойно читать и использовать, не забыв при этом про ручные тесты. В нашем случае, на создание разработчиком pull-request в Gitlab создается ветка с названием и sha-ref коммита и заводится пайплайн, в котором запускаются тесты. Так вот, мы хотели понимать, какие тесты запускались каждой из веток или каждом из коммитов.
Настроив интеграцию, мы получили отдельную страницу со всеми запусками (Launches) в Allure TestOps. Страница Launches показывает эту информацию в структурированном виде, позволяет отфильтровать ее и получить нужный срез данных (запущенные тесты, процент успешно выполненных и упавших тестов и другую схожую информацию) по конкретной ветке.
Если провалиться в конкретный Launch, можно посмотреть на структуру нашего проекта: какие есть компоненты, какие фичи, сколько было перезапусков и какие именно тесты перезапускались, информацию о дереве тест-кейсов, график, который показывает сколько времени бежал самый долгий тест,
график с длительностью тестов, — и в конце концов экспортировать этот прогон в JIRA-тикет.
Отдельно хочется упомянуть про политики для Live documentation, которая в автоматическом режиме обновляет дерево тест-кейсов по правилам и триггерам, например, только из ветки мастер.
Работает это так: у нас есть автотест, в котором изменили пару шагов и поправили аннотации. Этот тест запускается и выполняется с набором тестов на мастере (на пулл-реквест в мастер), после чего можно либо вручную закрыть launch, либо дождаться, когда launch будет закрыт по cron’у, который настраивается в интерфейсе Allure. При закрытии запуска старый автотест обновляется — получаем актуальный тест-кейс. Это позволяет в принципе забыть про актуализацию тестовой документации в интерфейсе.
Итак, стало понятно, что и где тестируется, какие тесты выполняются, как часто перезапускаются и какие дают результаты по итогам прогонов. Кажется, все! или нет? Ах да! Мы забыли про ручные кейсы.
Давайте чуть разберемся в структуре тест-кейсов, которую нам предлагает следовать Allure TestOps — она предполагает, что тест представляет из себя законченный сценарий, а expected result является необязательным элементом теста. А там, где это необходимо, можно описывать “expected result” в виде вложенных шагов. Написав пару-тройку тест-кейсов в таком стиле, становится ясно, что это удобно и практично. Пример такого теста может выглядеть так:
Если такой стиль написания кейсов не устраивает по какой-либо причине, всегда можно включить другую
структуру с
предусловиями.
Далее мы импортировали ручные тест кейсы из TestRail в Allure TestOps. Механизм импорта тест-кейсов позволяет гибко настроить, что именно импортировать, а что нет, поэтому проблемы были только с самими тестами — много ручных тестов снова пришлось переработать под новую структуру. Думаете, все? Как бы не так!
В конечном итоге мы получили базу с ручными и автоматическими тестами в едином месте и были готовы презентовать результаты работы коллегам, но что-то пошло не так и обнаружилась еще одна неприятная история. Перед той самой презентацией мы решили настроить все отчеты, фильтры, показать, какие у нас есть ручные тест-кейсы, какие автоматические, как все это прекрасно работает, но, немного перемудрив с настройками Allure TestOps, мы окончательно запутались в нашем дереве и с трудом стали различать, где ручной, а где автоматический тест. Почему так произошло?
Потому что мы попытались интегрировать ручные тесты в структуру автоматических тестов. Впрочем, проблема оказалась не
фундаментальной, а технической: поскольку структура для автоматических тестов была написана с помощью аннотаций в коде,
а ручные тесты создавались/редактировались прямо в интерфейсе Allure, после закрытия каждого прогона с мастер
ветки начиналась путаница в статусах всех тест-кейсов.
Выглядело это примерно так:
Осознав, где свернули не туда, мы решили удалить текущее дерево и все настроить заново. С автоматическими тестами проблем не возникло, они очень быстро обновили документацию и выстроились в новое дерево на основе аннотаций. Но один важный вопрос остался открытым: что же делать с ручными тестами? Если пойти тем же путем, что и в прошлый раз, получим инструмент который решает задачи связанные только с автоматизацией, оставим TestRail для ручных тестов и продолжим связывать вручную два разрозненных отчета, как в старые добрые времена.
Решение нашлось неожиданно: как-то вечером перед Гейзенбагом, одной из самых больших конференций по тестированию, я просматривал программу прошлых докладов и обнаружил доклад на тему Тест-кейсы как код. Да, практика не новая, и про нее многие слышали. Ее суть заключается в том, что даже ручные тесты следует писать как метод под будущую автоматизацию: создали тестовый метод, в нем описали сценарий, сделали ветку, сделали merge request, прислали коллегам на ревью. Если коллегам что-то не понравилось в mr, они оставляют комментарий, вы правите, после чего ветка успешно мержится в основную. Что характерно, все это можно делать прямо в вашей CI (напомню, что в нашем случае это gitlab).
Отличная практика, но как ее подружить c Allure и получить вменяемые отчеты? На помощь снова приходят аннотации. Пишем ручной тест в нужном месте в репозитории, размечаем его с помощью аннотаций, созданных для автотестов, после чего указываем, что тест — ручной. Примерно так:
* @Title("Короткое описание")
* @Description("Чуть понятнее пытаемся расписать")
* @Features({"Наша фича к которой относится тест кейс"})
* @Stories({"Если нужна"})
* @Issues({"Ссылка на требования, к примеру conf"})
* @Labels({
* @Label(name = "Jira", values = "Номер jira тикета"),
* @Label(name = "component", values = "Компонент"),
* @Label(name = "createdBy", values = "автор тест кейса"),
* @Label(name = "ALLURE_MANUAL", values = "true"),
* })
Шаги теста могут выглядеть таким образом:
Метод step в данном случае так же как и в случае с автотестами является просто оберткой над встроенным методом Allure executeStep. Единственное отличие в том, что для такого теста добавляется Label = ALLURE_MANUAL. А так это выглядит в Allure:
Стоит отметить, что сами тесты после запуска прогона на ветке получают статус: in_progress, что достаточно удобно, когда в отчете есть ручные и автоматические тесты. Это означает, что их нужно пройти вручную. Вообще, Allure умеет назначать тесты на тестировщиков, если вы пропишете правила в настройках запуска. Так же для более удобной работы с такими тестами, есть плагин Allure TestOps Support, который позволяет удобно выгружать результаты прогонов как ручных так и авто тестов в Allure Testops прямо не выходя из IDE.
Таким образом, мы получаем ручные тест-кейсы, которые хранятся в коде, имеют версионированность, подробную информацию об авторе тест-кейса, ссылки на все имеющиеся требования и ручной статус в Allure. Кроме того, такой тест автоматически становится готов к дальнейшей автоматизации. А самое главное, что теперь можно не бояться что-то потерять или удалить, так как все тесты могут очень быстро воссоздать прежнюю структуру после прогона.
Да, при этом редактировать и убираться в репозитории приходится чаще, а подходить к созданию тест-кейсов нужно более ответственно. Лично мне почему-то такой подход ближе. Возможно потому, что он избавляет нас от бездумных тест-кейсов в стиле "проверяю то, сам не знаю что", но это мое личное мнение. Теперь о глобальных минусах. Если нужно что-то прикрепить (картинку или файл) к тест-кейсу, сделать это будет довольно сложно, так как в Allure TestOps эта возможность реализована не для всех языков. Для PHP, это пока что в стадии доработки.
Еще один минус заключается в том, что такие ручные тесты постоянно запускаются с автотестами и потребляют драгоценное время при прогоне всего набора тестов. Конечно время выполнения такого теста очень маленькое, но это может оттолкнуть от идеи использовать такой подход.
Одно могу точно сказать: ребята из Allure EE сейчас активно работают в этом направлении и я думаю, что со временем предложат более элегантное решение этого вопроса. Как несложно догадаться, наша команда получила то, чего желала, и пока что остается на этом пути.
Dashboards and reports Что ж, с ручными тестами вроде разобрались, перенесли часть тестов в код, часть удалили, часть по пути автоматизировали, а некоторые тесты вообще расписали как документацию в Confluence, получив внутреннюю базу знаний. Осталось разобраться с отчетами. В Allure TestOps для этого есть отдельная страница, которая так и называется — dashboards. Она представляет из себя набор маленьких дэшбордов и виджетов, на которых можно найти информацию обо всех тестах, их количестве, запусках (launches) и статистике исполнения.
На виджете “test cases” видно общее количество наших тестов, как ручных, так и автоматических. На виджете “launches” видны запуски и количество тестов, которые были запущены в каждом из них. Если этой информации покажется недостаточно, можно настроить свои виджеты с помощью встроенного query language (аналог языка в JIRA) и сделать практически любой отчет. К примеру мне очень понравилась идея с отчетом, который называется “Test Case Tree Map Chart”, показывающий матрицу покрытия.
Представьте себе единый график, на котором можно посмотреть, что для одной фичи у нас 3 ручных и 1 автотест, а для другой — только 5 ручных тестов. Такой график легко позволяет понять, где хромает автоматизация или тестовое покрытие в принципе.
Интеграция на этом этапе была завершена и то, что было реализовано в проекте, на текущий момент времени удовлетворяет наши потребности. В ближайших планах более гибкая настройка запуска тестов из Allure TestOps, т.е подготовка набора шаблонов с тестами, организация их в тест-планы и запуск их по отдельности из Allure TestOps на разных устройствах/браузерах/операционных системах. Еще планируем доработать напильником процесс работы с ручными тест-кейсами и более тщательно продумать наши процессы и, возможно, найти больше подводных камней и поделиться с вами.
Сам процесс рефакторинга, как я писал ранее, занял у команды долгий и тяжелый месяц, зато мы поняли, что на проекте автоматизировано, а что нет. Заодно привели в порядок ручные тесты. Стоило ли это того? Для нас — точно да. В итоге все стало работать понятнее и стабильнее.
В качестве заключения хочется сказать, что Allure TestOps, конечно, не является серебрянной пулей или волшебным инструментом, который в одночасье решит все проблемы на проекте. Но однозначно могу сказать, что этот инструмент очень облегчает жизнь инженеров по обеспечению качества, прививая и как бы невзначай навязывая правильные подходы, а также позволяя ощутить атмосферу безмятежной гавани, в которой можно заниматься качеством и приносить пользу, а не постоянно тушить пожары. :)
Всем спасибо, что уделили внимание этой статье, буду рад любым вопросам и постараюсь на них ответить в комментариях.