Никто не любит писать тесты, но ИИ может исправить это

от автора

Введение

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

Я пришла на большой проект с костылями и легаси, где юнит-тестов не было. Какое-то время писала их сама, но, честно говоря, мотивация держать всё на себе быстро испарялась. Каждый Merge Request требовал корректировки тестов. Когда стало очевидно, что энтузиазма одного человека недостаточно, я поставила цель: показать команде, что тестирование — это не тяжелая обязанность, а простой рабочий навык, который можно освоить без боли и потери скорости, особенно используя современные подходы и инструменты.

Любая инициатива сопровождается вопросом — зачем?

Вопрос «зачем?» часто скрывает другие проблемы и боли разработчиков. Давайте рассмотрим, почему тесты реально не пишут, и как наш подход помог преодолеть эти барьеры.

  1. Высокий порог входа. Написание тестов воспринимается как задача, занимающая до 50% времени от разработки фичи. Страх увязнуть в сложных настройках, моках и необходимости изучать новые библиотеки отпугивает от первого шага.

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

  3. Невидимая работа без признания. Тесты — это работа «под капотом». Она требует времени, но редко приносит аплодисменты и видимый результат, в отличие от новой фичи, что снижает мотивацию.

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

Основная концепция: сместить фокус с написания на ревью.

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

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

Особенности нашего проекта.

Наша команда работает на 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/