Тестирование React Native в 2026: Jest 30, Testing Library и Maestro

Настройка Jest 30, компонентные тесты с React Native Testing Library, мокирование нативных модулей, E2E-тестирование с Maestro и интеграция с EAS Workflows — с рабочими примерами кода.

Почему тестирование в React Native — это не опция, а необходимость

Признайтесь честно: сколько раз вы выкатывали обновление приложения, которое «работало на вашем устройстве», а потом ловили поток гневных отзывов? Если ни разу — вам крупно повезло. Ну а если это знакомая история — значит, пора всерьёз заняться тестированием.

В 2026 году мобильная разработка дошла до такого уровня зрелости, что ручное тестирование просто не масштабируется. React Native приложения крутятся на сотнях разных устройств с кучей версий Android и iOS, и каждый апдейт библиотеки или ОС потенциально может всё сломать. Автоматизированные тесты — по сути, единственный способ спать спокойно без того, чтобы QA-команда сходила с ума от регрессий.

Итак, в этом руководстве мы пройдём весь путь от настройки Jest 30 и первого юнит-теста до E2E-тестирования с Maestro, интеграции с EAS Workflows и лучших практик 2026 года. Все примеры кода рабочие — можете копировать и использовать в своих проектах прямо сейчас.

Стратегия тестирования: пирамида тестов для React Native

Прежде чем писать первый тест, стоит разобраться, какие виды тестов бывают и зачем каждый из них нужен. Классическая «пирамида тестов» работает и для React Native, но с некоторыми нюансами.

Юнит-тесты — фундамент пирамиды

Юнит-тесты проверяют отдельные функции, утилиты и модули в изоляции. Быстрые, стабильные, дают мгновенную обратную связь. Сюда попадает тестирование бизнес-логики, валидации данных, хелперов и чистых функций. В React Native проекте это примерно 70% всех тестов.

Компонентные (интеграционные) тесты — средний слой

Компонентные тесты проверяют React-компоненты и их взаимодействие друг с другом. С помощью React Native Testing Library вы рендерите компонент в Node.js-окружении и проверяете, что он корректно отображается, реагирует на действия пользователя и обновляет состояние. Это около 20% тестов.

E2E-тесты — вершина пирамиды

End-to-end тесты запускают настоящее приложение на эмуляторе или реальном устройстве и имитируют то, что делает живой пользователь. Они самые медленные, зато дают максимальную уверенность в том, что всё работает.

В 2026 году Maestro стал де-факто стандартом для E2E-тестирования React Native — благодаря YAML-синтаксису и устойчивости к «флакам». Оставьте на E2E 10% тестов — только критические пользовательские сценарии.

Настройка Jest 30 для React Native и Expo

Jest остаётся стандартом де-факто для тестирования JavaScript-приложений. Версия 30, выпущенная в 2025 году, принесла заметное улучшение производительности, поддержку TypeScript 5.4+ из коробки и ряд полезных оптимизаций. Если вы создали проект через React Native CLI или Expo — Jest уже предустановлен. Но давайте разберёмся с тонкостями настройки.

Настройка в Expo-проекте

Для Expo используется пресет jest-expo, который автоматически мокает нативную часть Expo SDK:

npx expo install jest-expo jest @types/jest -- --dev

Обновите package.json:

{
  "scripts": {
    "test": "jest --watchAll"
  },
  "jest": {
    "preset": "jest-expo",
    "transformIgnorePatterns": [
      "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg)"
    ]
  }
}

Параметр transformIgnorePatterns — честно говоря, одна из самых раздражающих частей настройки. Многие npm-пакеты для React Native не компилируют свой код перед публикацией, и Jest должен трансформировать их через Babel. Если ваш тест падает с ошибкой SyntaxError: Unexpected token — почти наверняка нужно добавить проблемный пакет в этот список. Я сам на это натыкался раз пять, пока не запомнил.

Настройка в bare React Native-проекте

npm install --save-dev jest @types/jest babel-jest @react-native/jest-preset

Конфигурация в package.json:

{
  "jest": {
    "preset": "@react-native/jest-preset"
  }
}

Структура тестовых файлов

Jest автоматически находит файлы с тестами по двум паттернам:

  • Файлы с суффиксом .test.ts / .test.tsx
  • Файлы внутри директории __tests__

Рекомендую размещать тесты рядом с тестируемым кодом. Так гораздо проще поддерживать связь между компонентом и его тестом, и не приходится прыгать по дереву файлов:

src/
  components/
    LoginForm/
      LoginForm.tsx
      LoginForm.test.tsx
    Button/
      Button.tsx
      Button.test.tsx
  utils/
    validation.ts
    validation.test.ts

Первый тест — проверяем, что всё работает

Создайте файл src/utils/math.ts:

export function calculateDiscount(price: number, percentage: number): number {
  if (price < 0 || percentage < 0 || percentage > 100) {
    throw new Error('Некорректные параметры');
  }
  return price - (price * percentage) / 100;
}

export function formatPrice(price: number, currency: string = '₽'): string {
  return `${price.toFixed(2)} ${currency}`;
}

И тест для него — src/utils/math.test.ts:

import { calculateDiscount, formatPrice } from './math';

describe('calculateDiscount', () => {
  it('корректно вычисляет скидку 20% от 1000', () => {
    expect(calculateDiscount(1000, 20)).toBe(800);
  });

  it('возвращает полную цену при скидке 0%', () => {
    expect(calculateDiscount(500, 0)).toBe(500);
  });

  it('возвращает 0 при скидке 100%', () => {
    expect(calculateDiscount(500, 100)).toBe(0);
  });

  it('выбрасывает ошибку при отрицательной цене', () => {
    expect(() => calculateDiscount(-100, 20)).toThrow('Некорректные параметры');
  });
});

describe('formatPrice', () => {
  it('форматирует цену с рублями по умолчанию', () => {
    expect(formatPrice(1234.5)).toBe('1234.50 ₽');
  });

  it('использует переданную валюту', () => {
    expect(formatPrice(99.9, '$')).toBe('99.90 $');
  });
});

Запустите тест:

npm test

Если все тесты зелёные — настройка работает, можно двигаться дальше.

React Native Testing Library: тестируем компоненты как пользователь

Главный принцип React Native Testing Library (RNTL) звучит так: «Чем больше ваши тесты напоминают реальное использование приложения, тем больше уверенности они дают». Идея простая — вместо того чтобы копаться во внутреннем состоянии компонента, вы тестируете именно то, что видит и делает пользователь.

Установка RNTL

npm install --save-dev @testing-library/react-native

Важно: библиотека @testing-library/react-native полностью заменяет устаревший react-test-renderer, который не поддерживает React 19. Если вы до сих пор используете react-test-renderer — самое время от него избавиться.

Основные методы поиска элементов

RNTL даёт набор методов для поиска элементов, которые имитируют поведение реального пользователя:

import { render, screen } from '@testing-library/react-native';

// Поиск по тексту — как пользователь видит текст на экране
screen.getByText('Войти');

// Поиск по placeholder — для текстовых полей
screen.getByPlaceholderText('Введите email');

// Поиск по testID — когда нет видимого текста
screen.getByTestId('submit-button');

// Поиск по роли доступности
screen.getByRole('button', { name: 'Отправить' });

// Проверка отсутствия элемента
expect(screen.queryByText('Ошибка')).toBeNull();

Всегда старайтесь искать элементы по видимому тексту или роли — это делает тесты устойчивее к рефакторингу. К testID прибегайте только когда другие способы не подходят.

Тестирование пользовательского взаимодействия

Разберём тестирование формы входа — типичный и очень полезный пример, который пригодится почти в любом проекте:

// LoginForm.tsx
import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, ActivityIndicator } from 'react-native';

interface LoginFormProps {
  onSubmit: (email: string, password: string) => Promise<void>;
}

export function LoginForm({ onSubmit }: LoginFormProps) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async () => {
    if (!email || !password) {
      setError('Заполните все поля');
      return;
    }
    setLoading(true);
    setError('');
    try {
      await onSubmit(email, password);
    } catch (e) {
      setError('Неверный email или пароль');
    } finally {
      setLoading(false);
    }
  };

  return (
    <View>
      {error ? <Text testID="error-message">{error}</Text> : null}
      <TextInput
        placeholder="Email"
        value={email}
        onChangeText={setEmail}
        autoCapitalize="none"
        keyboardType="email-address"
      />
      <TextInput
        placeholder="Пароль"
        value={password}
        onChangeText={setPassword}
        secureTextEntry
      />
      <TouchableOpacity onPress={handleSubmit} disabled={loading} testID="login-button">
        {loading ? <ActivityIndicator /> : <Text>Войти</Text>}
      </TouchableOpacity>
    </View>
  );
}

Теперь напишем тест:

// LoginForm.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
import { LoginForm } from './LoginForm';

describe('LoginForm', () => {
  const mockOnSubmit = jest.fn();

  beforeEach(() => {
    mockOnSubmit.mockClear();
  });

  it('отображает поля ввода и кнопку', () => {
    render(<LoginForm onSubmit={mockOnSubmit} />);

    expect(screen.getByPlaceholderText('Email')).toBeOnTheScreen();
    expect(screen.getByPlaceholderText('Пароль')).toBeOnTheScreen();
    expect(screen.getByText('Войти')).toBeOnTheScreen();
  });

  it('показывает ошибку при пустых полях', () => {
    render(<LoginForm onSubmit={mockOnSubmit} />);

    fireEvent.press(screen.getByText('Войти'));

    expect(screen.getByText('Заполните все поля')).toBeOnTheScreen();
    expect(mockOnSubmit).not.toHaveBeenCalled();
  });

  it('вызывает onSubmit с email и паролем', async () => {
    mockOnSubmit.mockResolvedValueOnce(undefined);
    render(<LoginForm onSubmit={mockOnSubmit} />);

    fireEvent.changeText(screen.getByPlaceholderText('Email'), '[email protected]');
    fireEvent.changeText(screen.getByPlaceholderText('Пароль'), 'password123');
    fireEvent.press(screen.getByText('Войти'));

    await waitFor(() => {
      expect(mockOnSubmit).toHaveBeenCalledWith('[email protected]', 'password123');
    });
  });

  it('показывает ошибку при неудачном входе', async () => {
    mockOnSubmit.mockRejectedValueOnce(new Error('Invalid credentials'));
    render(<LoginForm onSubmit={mockOnSubmit} />);

    fireEvent.changeText(screen.getByPlaceholderText('Email'), '[email protected]');
    fireEvent.changeText(screen.getByPlaceholderText('Пароль'), 'wrong');
    fireEvent.press(screen.getByText('Войти'));

    await waitFor(() => {
      expect(screen.getByText('Неверный email или пароль')).toBeOnTheScreen();
    });
  });
});

Тестирование кастомных хуков

Для тестирования хуков RNTL предоставляет удобную функцию renderHook:

// useCounter.ts
import { useState, useCallback } from 'react';

export function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(() => setCount(c => c + 1), []);
  const decrement = useCallback(() => setCount(c => c - 1), []);
  const reset = useCallback(() => setCount(initialValue), [initialValue]);

  return { count, increment, decrement, reset };
}
// useCounter.test.ts
import { renderHook, act } from '@testing-library/react-native';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  it('инициализируется с начальным значением', () => {
    const { result } = renderHook(() => useCounter(10));
    expect(result.current.count).toBe(10);
  });

  it('увеличивает счётчик', () => {
    const { result } = renderHook(() => useCounter(0));

    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });

  it('сбрасывает к начальному значению', () => {
    const { result } = renderHook(() => useCounter(5));

    act(() => {
      result.current.increment();
      result.current.increment();
    });

    expect(result.current.count).toBe(7);

    act(() => {
      result.current.reset();
    });

    expect(result.current.count).toBe(5);
  });
});

Мокирование нативных модулей и API-вызовов

Вот тут начинается самое интересное (и иногда самое болезненное). React Native компоненты часто зависят от нативных модулей — камера, геолокация, уведомления — которые просто не существуют в Node.js-окружении Jest. Умение правильно мокать эти зависимости — ключевой навык для продуктивного тестирования.

Мокирование нативных модулей

Создайте файл jest.setup.ts и укажите его в конфигурации Jest:

// package.json
{
  "jest": {
    "preset": "jest-expo",
    "setupFiles": ["./jest.setup.ts"]
  }
}
// jest.setup.ts
jest.mock('@react-native-async-storage/async-storage', () =>
  require('@react-native-async-storage/async-storage/jest/async-storage-mock')
);

jest.mock('expo-location', () => ({
  requestForegroundPermissionsAsync: jest.fn().mockResolvedValue({ status: 'granted' }),
  getCurrentPositionAsync: jest.fn().mockResolvedValue({
    coords: { latitude: 55.7558, longitude: 37.6173 },
  }),
}));

jest.mock('expo-notifications', () => ({
  getPermissionsAsync: jest.fn().mockResolvedValue({ status: 'granted' }),
  getExpoPushTokenAsync: jest.fn().mockResolvedValue({ data: 'ExponentPushToken[xxx]' }),
}));

Мокирование API-вызовов с MSW

Mock Service Worker (MSW) — это, пожалуй, лучшая на сегодня альтернатива ручному мокированию fetch. Вместо подмены глобальной функции вы описываете обработчики на уровне сетевого слоя, что гораздо ближе к реальности:

npm install --save-dev msw
// mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('https://api.example.com/user/profile', () => {
    return HttpResponse.json({
      id: 1,
      name: 'Алексей',
      email: '[email protected]',
    });
  }),

  http.post('https://api.example.com/auth/login', async ({ request }) => {
    const body = await request.json() as { email: string; password: string };

    if (body.email === '[email protected]' && body.password === 'password123') {
      return HttpResponse.json({ token: 'mock-jwt-token' });
    }

    return HttpResponse.json({ error: 'Invalid credentials' }, { status: 401 });
  }),
];
// mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);
// jest.setup.ts — добавьте к существующей конфигурации
import { server } from './mocks/server';

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

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

Snapshot-тесты: когда использовать и когда нет

Snapshot-тестирование — штука мощная, но коварная. Jest фиксирует «снимок» отрендеренного компонента, а при последующих запусках сравнивает с сохранённой версией. Любое изменение в рендере — и тест падает.

Правильное применение

Используйте снимки для маленьких, стабильных компонентов — иконок, кнопок, бейджей. Для сложных экранов снимки создают больше головной боли, чем пользы: каждое мелкое изменение порождает огромный diff, и разработчики начинают машинально обновлять снимки через jest --updateSnapshot, даже не глядя на изменения.

import React from 'react';
import { render } from '@testing-library/react-native';
import { Badge } from './Badge';

describe('Badge', () => {
  it('корректно отображает бейдж со статусом', () => {
    const tree = render(<Badge status="success" label="Активен" />);
    expect(tree.toJSON()).toMatchSnapshot();
  });
});

Когда НЕ стоит использовать снимки

  • Для целых экранов и страниц — слишком хрупко
  • Для компонентов с динамическими данными (даты, ID) — замучаетесь с сериализаторами
  • Вместо осмысленных проверок — снимок не объясняет, что именно должен делать компонент

E2E-тестирование с Maestro

Юнит-тесты и компонентные тесты запускаются в Node.js — они вообще не затрагивают нативный код. Если баг сидит в нативном слое или в интеграции между экранами — эти тесты его просто не поймают. Вот тут на сцену выходят E2E-тесты.

В 2026 году Maestro от mobile.dev стал предпочтительным инструментом для E2E-тестирования React Native. И честно говоря, заслуженно: декларативный YAML-синтаксис вместо императивного кода, встроенная устойчивость к задержкам UI (автоматически ждёт элементы), минимальная «флаковость» и простая интеграция с Expo и EAS.

Установка Maestro

# macOS / Linux
curl -Ls "https://get.maestro.mobile.dev" | bash

# Проверка установки
maestro --version

Для Android потребуется Java 17+ и настроенный Android SDK. Для iOS — Xcode и симулятор.

Подготовка приложения

Maestro взаимодействует с приложением через testID (в React Native) или видимый текст. Добавьте testID к ключевым элементам интерфейса:

<TextInput testID="email-input" placeholder="Email" />
<TextInput testID="password-input" placeholder="Пароль" />
<TouchableOpacity testID="login-button">
  <Text>Войти</Text>
</TouchableOpacity>

Создание E2E-тестового сценария

Создайте директорию .maestro в корне проекта и добавьте файл сценария:

# .maestro/login-flow.yml
appId: com.yourapp.id
---
- launchApp

- tapOn:
    id: "email-input"
- inputText: "[email protected]"

- tapOn:
    id: "password-input"
- inputText: "password123"

- tapOn:
    id: "login-button"

- assertVisible: "Добро пожаловать"

Запуск тестов

Сначала соберите приложение и установите на эмулятор или симулятор, затем запустите тесты:

# Для Android: соберите APK
npx expo run:android --variant release

# Для iOS: соберите приложение
npx expo run:ios --configuration Release

# Запуск одного сценария
maestro test .maestro/login-flow.yml

# Запуск всех сценариев в директории
maestro test .maestro/

Продвинутые возможности Maestro

Maestro поддерживает условную логику, работу со скроллами, ожидания и даже скриншот-тестирование:

# .maestro/product-search.yml
appId: com.yourapp.id
---
- launchApp

- tapOn:
    id: "search-input"
- inputText: "React Native"

- waitForAnimationToEnd

- assertVisible: "Результаты поиска"

- scrollUntilVisible:
    element: "Показать ещё"
    direction: DOWN
    timeout: 10000

- tapOn: "Показать ещё"

- assertVisible:
    text: ".*найдено.*"
    regex: true

- takeScreenshot: "search-results"

Интеграция тестов с EAS Workflows

Тесты приносят максимальную пользу, когда выполняются автоматически при каждом изменении кода. EAS Workflows позволяет настроить CI/CD-конвейер, который запускает тесты при каждом пулл-реквесте — и блокирует мердж, если что-то сломалось.

Запуск Jest-тестов в EAS Workflows

Создайте файл .eas/workflows/test.yml:

# .eas/workflows/test.yml
name: Run Tests
on:
  pull_request:
    branches: ['main']

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Run unit and component tests
        run: npm test -- --coverage --ci

      - name: Check coverage threshold
        run: |
          npx jest --coverage --coverageThreshold='{"global":{"branches":60,"functions":60,"lines":60,"statements":60}}'

Запуск Maestro E2E в EAS Workflows

Для E2E-тестов с Maestro потребуется сборка приложения и эмулятор:

# .eas/workflows/e2e-android.yml
name: E2E Tests (Android)
on:
  push:
    branches: ['main']

jobs:
  e2e-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Build Android APK
        uses: expo/expo-github-action@v8
        with:
          command: eas build --platform android --profile preview --local

      - name: Install Maestro
        run: curl -Ls "https://get.maestro.mobile.dev" | bash

      - name: Run E2E tests
        run: maestro test .maestro/

Важный нюанс: для Android E2E на GitHub Actions нужны раннеры с поддержкой аппаратной виртуализации. Стандартные Ubuntu-раннеры не поддерживают вложенную виртуализацию, необходимую для Android-эмулятора. Рассмотрите использование macos-latest или кастомных раннеров — это сэкономит немало нервов при отладке CI.

Лучшие практики тестирования в 2026 году

1. Тестируйте поведение, а не реализацию

Не проверяйте внутреннее состояние компонента через instance() или state(). Проверяйте то, что пользователь видит на экране. Если вы рефакторите компонент без изменения его поведения — тесты не должны падать. Если падают — значит, тесты написаны слишком хрупко.

2. Один тест — одна проверка

Если ваш тест содержит 15 expect и 10 действий — пора его разбить. Маленькие тесты проще отлаживать и поддерживать. Когда падает тест «проверяет весь экран целиком» — непонятно, что именно сломалось, и приходится тратить время на поиск.

3. Изолируйте тесты друг от друга

Каждый тест должен быть независимым. Используйте beforeEach для сброса моков, очистки AsyncStorage, навигационного стека и другого глобального состояния. Порядок запуска тестов не должен влиять на результат — если влияет, где-то утекает состояние.

4. Не гонитесь за 100% покрытием

Стопроцентное покрытие — ложная цель, и я говорю это из опыта. Фокусируйтесь на покрытии бизнес-логики, критических путей пользователя и граничных случаев. Тестирование тривиальных стилей или статических текстов не добавляет реальной ценности, зато отнимает время.

5. Используйте waitFor для асинхронных операций

React Native часто выполняет операции асинхронно. Всегда оборачивайте проверки после асинхронных действий в waitFor — иначе рискуете получить нестабильные тесты:

// Плохо — может быть нестабильным
fireEvent.press(screen.getByText('Загрузить'));
expect(screen.getByText('Данные загружены')).toBeOnTheScreen();

// Хорошо — ждёт появления элемента
fireEvent.press(screen.getByText('Загрузить'));
await waitFor(() => {
  expect(screen.getByText('Данные загружены')).toBeOnTheScreen();
});

6. Пишите тест до исправления бага

Получили баг-репорт? Сначала напишите тест, который воспроизводит проблему и падает. Потом исправьте баг — тест станет зелёным. Теперь этот баг гарантированно не вернётся. Это классический TDD-подход, и он реально работает.

Часто задаваемые вопросы (FAQ)

Какой фреймворк выбрать для тестирования React Native: Jest или Vitest?

Для React Native проектов Jest остаётся лучшим выбором. Он идёт «из коробки» с React Native CLI и Expo, имеет зрелую экосистему моков для нативных модулей и отличную интеграцию с React Native Testing Library. Vitest заточен под Vite-проекты и пока не имеет полноценной поддержки React Native. Так что если вы работаете с React Native — берите Jest 30 и не сомневайтесь.

Нужно ли тестировать навигацию между экранами в юнит-тестах?

Навигацию лучше тестировать на двух уровнях. В юнит-тестах можно проверить, что компонент вызывает navigation.navigate с правильными параметрами, замокав объект навигации. А вот реальные переходы между экранами — это уже задача E2E-тестов с Maestro, где проверяется весь пользовательский путь целиком.

Как тестировать компоненты, использующие Reanimated 4?

Reanimated предоставляет мок для Jest. Добавьте в jest.setup.ts строку require('react-native-reanimated/mock'). Это замокает все анимационные хуки. Но учтите, что юнит-тесты в принципе не проверяют плавность анимации — для этого лучше подходит ручное тестирование или визуальное регрессионное тестирование.

Чем Maestro лучше Detox для E2E-тестирования?

Maestro и Detox решают одну и ту же задачу, но подходят к ней по-разному. Maestro использует YAML-синтаксис, что снижает порог входа, и имеет встроенную устойчивость к задержкам UI. Detox написан на JavaScript и даёт больше контроля, но требует заметно больше кода и более сложной настройки. В 2026 году Maestro стал выбором большинства React Native команд благодаря простоте, однако Detox по-прежнему актуален для проектов со сложными E2E-сценариями, требующими программной логики.

Как интегрировать тесты в процесс код-ревью?

Настройте CI-конвейер (EAS Workflows или GitHub Actions), который запускает тесты при каждом пулл-реквесте и блокирует мердж при падении. Добавьте отчёт о покрытии в PR-комментарий — это мотивирует команду поддерживать тесты. И введите простое правило: новый код идёт с тестами, исправление бага — с регрессионным тестом. Со временем это станет привычкой.

Об авторе Editorial Team

Our team of expert writers and editors.