Введение
Эта статья — живой опыт нашей команды, прошедшей путь от полного отсутствия тестов до их активного использования.
Я пришла на большой проект с костылями и легаси, где юнит-тестов не было. Какое-то время писала их сама, но, честно говоря, мотивация держать всё на себе быстро испарялась. Каждый Merge Request требовал корректировки тестов. Когда стало очевидно, что энтузиазма одного человека недостаточно, я поставила цель: показать команде, что тестирование — это не тяжелая обязанность, а простой рабочий навык, который можно освоить без боли и потери скорости, особенно используя современные подходы и инструменты.
Любая инициатива сопровождается вопросом — зачем?
Вопрос «зачем?» часто скрывает другие проблемы и боли разработчиков. Давайте рассмотрим, почему тесты реально не пишут, и как наш подход помог преодолеть эти барьеры.
-
Высокий порог входа. Написание тестов воспринимается как задача, занимающая до 50% времени от разработки фичи. Страх увязнуть в сложных настройках, моках и необходимости изучать новые библиотеки отпугивает от первого шага.
-
Непонятно, что тестировать и как оценить результат. Отсутствие чёткого понимания, что именно стоит покрывать тестами (вёрстку или бизнес-логику?), и как измерить их реальную пользу. Голые цифры процента покрытия не дают ощущения ценности.
-
Невидимая работа без признания. Тесты — это работа «под капотом». Она требует времени, но редко приносит аплодисменты и видимый результат, в отличие от новой фичи, что снижает мотивацию.
-
Хрупкость тестов и страх будущих изменений. Существует опасение, что написанные тесты сломаются при первом же изменении кода, и их поддержка превратится в постоянную рутину.
Основная концепция: сместить фокус с написания на ревью.
Ключевая идея — изменить парадигму, сместив фокус с создания на рецензирование. Вместо того чтобы бороться с пустым редактором, вы получаете тест, сгенерированный ИИ. Ваша роль трансформируется из автора в эксперта-ревьюера, который дорабатывает и совершенствует уже готовое решение.
Такой подход устраняет главный барьер — страх начать. Мы исходим из принципа: «Лучше работающий, но несовершенный тест сегодня, чем идеальный, но так и не написанный тест завтра». Главное — получить первую рабочую версию, которую затем можно итеративно улучшать, доводя до идеала.
Особенности нашего проекта.
Наша команда работает на Vue (3 и старых костылях от Vue 2: Options API, mixins и тд), Vite, Quasar и Vitest. Мы развиваем внутренний интерфейс, насыщенный сложными бизнес-правилами: динамические формы, фичи, включающие/выключающие части страниц, подтягивание клиентских данных из разных ресурсов и детальные проверки доступов.
Мы сознательно не тестируем верстку, фокусируясь исключительно на поведении и бизнес-логике. Важно отметить, что за время внедрения тестов наш проект прошел серьезную трансформацию — от монолита к микросервисам. И тесты не только пережили эту миграцию, но и стали залогом доверия команды к нашему новому подходу, доказав свою ценность и устойчивость.
Практика внедрения: два трека к успеху.
Наш подход состоит из двух параллельных треков: технического (для разработчика) и организационного (для тимлида). Успех достигается только тогда, когда они движутся синхронно. Технические шаги без поддержки со стороны процессов быстро заглохнут, а организационные требования без простых технических инструментов вызовут отторжение.
Трек 1: Инструкция для разработчика — от настройки до готового теста
Этот трек превращает написание тестов из сложной задачи в простой и повторяемый алгоритм. Ключевая мысль: тесты пишет ИИ, а я выступаю в роли ревьюера.
Давайте рассмотрим пошаговый алгоритм, который изменил отношение нашей команды к тестированию:
Шаг 1. Настраиваем окружение с помощью ИИ.
Первый барьер — «магия конфигов». Делегируем эту задачу ИИ. Даем ему наш package.json и просим подготовить все для тестов.
Пример package.json (Vue 3 + Vite):
{ "dependencies": { "@quasar/extras": "^1.0.0", "@quasar/quasar-app-extension-qpdfviewer": "^1.0.0-beta.9", "@vue/test-utils": "^2.3.2", "axios": "^1.12.0", "quasar": "^2.6.0", "vue": "^3.3.13", "vue-i18n": "^9.3.0-beta.19", "vue-router": "^4.0.0", "vuex": "^4.0.1", }, "devDependencies": { "@babel/preset-typescript": "^7.21.4", "@intlify/vite-plugin-vue-i18n": "^3.3.1", "@quasar/app-vite": "^1.0.0", "typescript": "5.1.6", }, "engines": { "node": "^18", "npm": ">= 6.13.4" }}
Промт для ИИ: «Вот мой package.json. Настрой, тестовое окружение на Vitest для Vue3. Подготовь список npm-зависимостей (vitest, jsdom, @vue/test-utils@1), команду для установки, конфиг для vite.config.js и скрипты для package.json (test:unit, test:coverage).»
В ответ мы получаем готовую инструкцию, которую остается только выполнить, не вникая в детали документации.
Необходимые библиотеки:
npm install -D vitest jsdom @vue/test-utils @vitest/coverage-v8
Блок в package.json:
"scripts": { "test": "vitest --environment happy-dom", "coverage": "vitest run --coverage --environment happy-dom"}
Блок в vite.config.js:
test: { globals: true, setupFiles: ['./tests/unit/setupTests.ts'], //о нем далее environment: 'happy-dom', coverage: { reporter: ['html', 'lcov'], exclude: [ '**/extras/pdf**' ] } }
Шаг 2. Генерируем первый тест с помощью ИИ.
Барьер «чистого листа» — самый сильный. Вместо того чтобы думать, с чего начать, мы даем ИИ код компонента и просим написать тест.
Промт для ИИ: «Напиши unit-тесты для этого Vue 3 компонента на Vitest, используя Vue Test Utils. Вот код UserProfile.vue. Протестируй computed-свойство greeting, метод loadUserData (замокай API) и условный рендеринг для админа. Используй структуру ААА. Вот код компонента UserProfile.vue: »
<template> <div> <h1>{{ greeting }}</h1> <p v-if="user.isAdmin">Доступ: Администратор</p> <button @click="loadUserData">Загрузить данные</button> </div></template><script>import { api } from '@/api';export default { props: { userId: { type: Number, required: true } }, data() { return { user: null }; }, computed: { greeting() { if (!this.user) return 'Привет, Гость!'; return `Привет, ${this.user.name}!`; }, }, methods: { async loadUserData() { try { this.user = await api.fetchUser(this.userId); } catch (e) { this.user = { name: 'Error', isAdmin: false }; }}, },};</script>
В результате мы получаем 90% готового файла *.spec.js, что убирает главный ступор — «Я не знаю, как использовать эту библиотеку».
Шаг 3. Запускаем и исправляем.
Вставляем сгенерированный код в проект. Скорее всего, он упадет из-за неверных путей импорта или неполных моков. Но исправить 2-3 строчки в уже готовом файле — это простая и быстрая задача. Каждая зеленая галочка в терминале дает мгновенное удовлетворение и мотивирует двигаться дальше.
import { describe, it, expect, vi } from 'vitest';import { mount } from '@vue/test-utils';import UserProfile from './UserProfile.vue'; //исправляем импортimport { api } from '@/api';// Мокаем (имитируем) модуль API, чтобы контролировать его поведение в тестах// Это позволяет нам не делать реальные сетевые запросыvi.mock('@/api', () => ({ //этот мок можно вынести в файл setupTests.ts' api: { fetchUser: vi.fn(), },}));describe('UserProfile.vue', () => { it('должен отображать приветствие для гостя, если данные пользователя не загружены', () => { // Arrange (Подготовка) const wrapper = mount(UserProfile, { props: { userId: 1 }, }); // Act (Действие) const greetingText = wrapper.find('h1').text(); // Assert (Проверка) expect(greetingText).toBe('Привет, Гость!'); expect(wrapper.find('p').exists()).toBe(false); // Сообщение для администратора не должно отображаться }); it('должен загружать данные пользователя и отображать приветствие для него', async () => { // Arrange (Подготовка) const mockUser = { name: 'Иван', isAdmin: false }; // Настраиваем мок-функцию, чтобы она возвращала тестового пользователя api.fetchUser.mockResolvedValue(mockUser); const wrapper = mount(UserProfile, { props: { userId: 2 }, }); // Act (Действие) await wrapper.find('button').trigger('click'); // Кликаем на кнопку для загрузки данных // Assert (Проверка) expect(api.fetchUser).toHaveBeenCalledWith(2); // Проверяем, что API был вызван с правильным userId expect(wrapper.find('h1').text()).toBe('Привет, Иван!'); }); it('должен отображать сообщение для администратора, если у пользователя есть права', async () => { // Arrange (Подготовка) const mockAdmin = { name: 'Анна', isAdmin: true }; api.fetchUser.mockResolvedValue(mockAdmin); const wrapper = mount(UserProfile, { props: { userId: 3 }, }); // Act (Действие) await wrapper.find('button').trigger('click'); // Assert (Проверка) const adminMessage = wrapper.find('p'); expect(adminMessage.exists()).toBe(true); // Проверяем, что элемент <p> существует expect(adminMessage.text()).toBe('Доступ: Администратор'); // Проверяем его текст }); it('не должен отображать сообщение для администратора, если у пользователя нет прав', async () => { // Arrange (Подготовка) const mockUser = { name: 'Петр', isAdmin: false }; api.fetchUser.mockResolvedValue(mockUser); const wrapper = mount(UserProfile, { props: { userId: 4 }, }); // Act (Действие) await wrapper.find('button').trigger('click'); // Assert (Проверка) expect(wrapper.find('p').exists()).toBe(false); // Элемент <p> не должен существовать });});
Шаг 4. Приводим к стандарту с помощью ИИ.
Первые тесты часто бывают хаотичными. Чтобы поддерживать порядок, мы используем ИИ для рефакторинга.
Промт для ИИ: «Отрефактори этот тест по нашим правилам: структура ААА с комментариями // Arrange, // Act, // Assert; используй beforeEach для создания чистого состояния; названия тестов должны следовать шаблону should [result] when [condition].»
Это учит новичков правильной структуре не через чтение документации, а на практике. Этот цикл «ИИ сгенерировал → вставили → запустили → отрефакторили» и есть основа всего подхода. Он снижает порог вхождения и превращает написание тестов в простую привычку.
Трек 2: Инструкция для тимлида — от инициативы до культуры.
Задача тимлида (или любого заинтересованного лица) — создать среду, в которой следование техническим шагам из первого трека становится естественным и поощряемым.
Шаг 1. Делаем работу видимой: «публичные победы».
Тесты — «невидимая работа». Ваша задача — сделать ее видимой.
Что делать: На дейли, ретро и демо просите разработчиков упоминать статус тестов: «задача готова, пишу тесты», «тесты для компонента X написаны». Это вводит написание тестов в общее информационное поле и придает ему легитимности.
Шаг 2. Интегрируем в CI/CD: «эффект светофора»
Подключите запуск тестов в пайплайн.
Что делать: На первом этапе не блокируйте Merge Request, если тесты упали. Главное — сделать результат видимым. Зеленая галочка от CI становится символом качества и стабильности, а красная — сигналом, который нельзя игнорировать.
Шаг 3. Включаем в цели и техдолг: создаем мотивацию.
Сделайте написание тестов частью рабочих целей.
Что делать: Включите метрики покрытия или просто количество написанных тестов в квартальные/годовые цели разработчиков. Задача «покрыть тестами старый модуль X» становится привлекательным способом легко закрыть часть техдолга и личных KPI.
Шаг 4. Фиксируем результат: набираем критическую массу.
Когда покрытие достигло значимого уровня (у нас это было 50%, цифра может быть любой, главное предотвратить стагнацию), закрепите успех.
Что делать: Настройте Quality Gates в SonarQube или аналогах. Установите правило: «Покрытие нового кода не должно быть ниже n%». Это предотвратит откат назад и закрепит новую норму.
Шаг 5. Внедряем политику по упавшим тестам: «не храним мертвечину»
Упавшие тесты, которые никто не чинит, демотивируют и создают шум.
Что делать: Внедрите простое правило: «Если тест упал и его сложно починить за 15-20 минут — удаляйте его». Лучше потом написать новый, актуальный тест, чем тратить часы на отладку устаревшего. Это сохраняет тестовую базу чистой и полезной.
Что стало лучше: Результаты внедрения.
Мы не просто получили красивую цифру в SonarQube. Мы добились конкретных улучшений:
-
Уверенность в рефакторинге: раньше изменение общей функции было минным полем. Сейчас, если после правок тесты зеленые, мы на 80% уверены, что ничего не сломали.
-
Подтвержденная устойчивость при архитектурных изменениях: в процессе перехода от монолита к микросервисам мы смогли перенести существующие тесты для каждого выделяемого сервиса без единого изменения. Это стало ключевым фактором, который подтвердил надежность нашего тестового покрытия и дал команде бесценную уверенность в правильности выбранного пути. Ускорение онбординга: новички быстрее понимают, как работает компонент, читая его тесты — живую документацию его поведения.
-
Упрощение код-ревью: ревьюер видит, что компонент покрыт тестами, и может сфокусироваться на архитектуре и стиле кода.
Ответ на главный вопрос: Так почему никто не любит писать тесты?
Так удалось ли нам исправить нелюбовь к тестам? Частично. Мы не заставили команду полюбить их. Любовь — чувство иррациональное. Вместо этого мы устранили ключевые барьеры, которые эту нелюбовь вызывали:
-
Страх и неизвестность («Я не знаком с этой библиотекой», «что тестировать») — убрали с помощью ИИ, который дает готовую точку старта.
-
Ощущение бесполезной траты времени («фича нужна вчера», «работа, которую не видят») — убрали, сделав процесс быстрым, а результат видимым.
-
Высокий порог входа («конфиги, моки») — снизили, начав с простейших тестов.
В итоге написание тестов превратилось из сложной творческой задачи в рутинный, почти механический навык. Команда пишет тесты не потому, что полюбила их, а потому что это стало просто, быстро и поощряемо. Мы не изменили отношение к тестам, мы изменили процесс. И это сработало.
Заключение
Самое сложное в любом деле — начать. Использование ИИ как катализатора, чтобы получить первый тест за пару минут, меняет правила игры. Маленькие публичные победы, стандартизация и включение тестов в квартальные цели превращают страх в привычку. Через цепочку этих простых шагов команда перестаёт бояться тестов и начинает видеть в них не барьер, а инструмент для уверенной и быстрой разработки.
Наша статистика и итоги:

Безусловно, успех проекта зависит не только от юнит-тестов. Значительный вклад вносят также архитектурные решения, такие как переход на микросервисы, интеграция инструментов контроля качества вроде SonarQube, а также оптимизация процессов разработки, например, усовершенствование Git-flow. Однако недооценивать очевидную пользу от тестирования было бы ошибкой.
ссылка на оригинал статьи https://habr.com/ru/articles/1024326/