Введение

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

Я пришла на большой проект с костылями и легаси, где юнит-тестов не было. Какое-то время писала их сама, но, честно говоря, мотивация держать всё на себе быстро испарялась. Каждый 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. Однако недооценивать очевидную пользу от тестирования было бы ошибкой.