Языки5 мин чтения2026-03-06

Тестирование, контроль и оптимизация кода Java: пошаговый гайд для тех, кто устал от багов в проде

Java — язык с кучей инструментов для качества кода. Проблема в том, что их слишком много. JUnit, Mockito, SpotBugs, PMD, Checkstyle, SonarQube... Голова кругом.

Java — язык с кучей инструментов для качества кода. Проблема в том, что их слишком много. JUnit, Mockito, SpotBugs, PMD, Checkstyle, SonarQube... Голова кругом. Давайте разберёмся, что реально стоит использовать и как это настроить за один вечер, а не за неделю.

Шаг 1: Юнит-тесты — база, которую все делают неправильно

Начнём с тестирования. По моему опыту, около 60% команд пишут тесты «для галочки» — есть тест, и хорошо. Но тест без проверок граничных случаев хуже, чем отсутствие теста. Он создаёт ложное чувство безопасности.

Базовая структура теста на JUnit 5:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;

class PaymentServiceTest {
    
    private PaymentService service;
    
    @BeforeEach
    void setUp() {
        service = new PaymentService();
    }
    
    @Test
    void processPayment_withValidAmount_returnsSuccess() {
        PaymentResult result = service.processPayment(100.0);
        
        assertTrue(result.isSuccessful());
        assertEquals(100.0, result.getAmount(), 0.01);
    }
    
    @ParameterizedTest
    @ValueSource(doubles = {-100.0, 0.0, -0.01})
    void processPayment_withInvalidAmount_throwsException(double amount) {
        assertThrows(IllegalArgumentException.class, 
            () -> service.processPayment(amount));
    }
}

Видите @ParameterizedTest? Это вещь. Вместо того чтобы копипастить пять одинаковых тестов с разными значениями, вы пишете один параметризованный. На одном проекте мы сократили количество тест-кодов в три раза, просто переписав их на параметризованные.

Теперь моки. Mockito — стандарт де-факто, но многие используют его неправильно:

import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.api.extension.ExtendWith;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    
    @Mock
    private PaymentGateway gateway;
    
    @Mock
    private NotificationService notifier;
    
    @InjectMocks
    private OrderService orderService;
    
    @Test
    void placeOrder_whenPaymentFails_notifiesCustomer() {
        when(gateway.charge(any())).thenReturn(PaymentResult.failed());
        
        orderService.placeOrder(new Order(100.0));
        
        verify(notifier).sendFailureNotification(any());
        verify(gateway, never()).refund(any());
    }
}

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

Шаг 2: Статический анализ — ловим баги до запуска

Тесты проверяют поведение. Статический анализ проверяет структуру. Это разные уровни защиты.

SpotBugs (ранее FindBugs) находит реальные баги. Не «плохой стиль», а места, где код упадёт или поведёт себя неправильно:

<!-- pom.xml -->
<plugin>
    <groupId>com.github.spotbugs</groupId>
    <artifactId>spotbugs-maven-plugin</artifactId>
    <version>4.8.3.1</version>
    <configuration>
        <effort>Max</effort>
        <threshold>Low</threshold>
        <failOnError>true</failOnError>
    </configuration>
    <executions>
        <execution>
            <phase>verify</phase>
            <goals>
                <goal>check</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Что типичного находит SpotBugs в Java-коде? Null pointer dereference — когда вы проверяете переменную на null после того, как уже использовали её. Ресурсы, которые не закрываются. Равенство объектов через == вместо equals(). Dead code — недостижимый код.

Checkstyle — другое дело. Он следит за стилем. И вот тут важно не переборщить. Я видел конфигурации Checkstyle на 200 правил. Команда тратит больше времени на борьбу с линтером, чем на написание кода.

Рекомендую начать с минимума:

<!-- checkstyle.xml -->
<!DOCTYPE module PUBLIC
    "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
    "https://checkstyle.org/dtds/configuration_1_3.dtd">

<module name="Checker">
    <module name="TreeWalker">
        <module name="AvoidStarImport"/>
        <module name="EmptyBlock"/>
        <module name="EqualsHashCode"/>
        <module name="MissingSwitchDefault"/>
        <module name="NullAssignment"/>
    </module>
</module>

Пять правил. Этого достаточно для старта. Добавите больше, когда команда привыкнет.

PMD — третий инструмент из «большой тройки». Он находит неочевидные проблемы: дублирование кода, неэффективные конструкции, потенциальные проблемы с производительностью. Но у PMD высокий уровень false positives. Приходится тюнить.

Шаг 3: Интеграция всего этого в CI/CD

По-хорошему, анализ должен запускаться автоматически при каждом коммите. Никаких «запущу позже вручную». Позже не будет.

GitHub Actions пример:

name: Java CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: maven
      
      - name: Run tests
        run: mvn test
      
      - name: SpotBugs check
        run: mvn spotbugs:check
      
      - name: Checkstyle check
        run: mvn checkstyle:check

Для GitLab CI:

# .gitlab-ci.yml
stages:
  - test
  - analyze

test:
  stage: test
  image: maven:3.9-eclipse-temurin-17
  script:
    - mvn test
  artifacts:
    reports:
      junit: target/surefire-reports/*.xml

spotbugs:
  stage: analyze
  image: maven:3.9-eclipse-temurin-17
  script:
    - mvn spotbugs:check
  allow_failure: false

checkstyle:
  stage: analyze
  image: maven:3.9-eclipse-temurin-17
  script:
    - mvn checkstyle:check
  allow_failure: true

Обратите внимание: Checkstyle я разрешил падать (allow_failure: true). На первых порах это спасёт от желания всё выключить. Плавно приучите команду к чистому коду.

Шаг 4: Оптимизация производительности — где прячутся тормоза

Java-код может выглядеть идеально, но работать медленно. Типичные проблемы?

Строки. Конкатенация в цикле:

// Плохо — создаёт кучу промежуточных строк
String result = "";
for (Item item : items) {
    result += item.getName();
}

// Хорошо
StringBuilder sb = new StringBuilder();
for (Item item : items) {
    sb.append(item.getName());
}
String result = sb.toString();

// Ещё лучше — Java 8+
String result = items.stream()
    .map(Item::getName)
    .collect(Collectors.joining());

Неправильное использование коллекций. ArrayList для частых вставок в начало. LinkedList для произвольного доступа. HashMap без указания capacity, когда размер известен заранее:

// Плохо — будет реаллокация и перехэширование
Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < 10000; i++) {
    map.put(i, "value" + i);
}

// Хорошо — сразу выделяем нужный размер
Map<Integer, String> map = new HashMap<>(16384); // с запасом на load factor

Для выявления проблем с производительностью в Java есть отличные инструменты. JProfiler, YourKit, Java Mission Control. Но самый доступный — Java VisualVM, идёт в составе JDK.

Запускаете приложение, открываете VisualVM, подключаетесь к процессу. Смотрите на вкладку Sampler. Через 30 секунд увидите, где программа проводит время. Обычно это не то, что вы думали.

Шаг 5: Code review — последний рубеж

Автоматика ловит явные проблемы. Но архитектурные решения, неочевидные баги, соответствие бизнес-логике — это уже работа людей. Code review.

Проблема в том, что ревью часто делают на бегу. «Выглядет нормально, одобряю». Знакомо?

Хороший чеклист для ревью Java-кода:

На одном проекте мы внедрили правило: ревьюер должен найти хотя бы один комментарий. Даже мелкий. Это заставляло людей реально читать код, а не просто кликать «Approve». Количество пропущенных багов упало на 40%.

Честно? Ручное ревью — это дорого. Сеньор тратит 2-3 часа в день на ревью. Если добавить автоматическую проверку через AI-бота, который оставляет инлайн-комментарии с замечаниями, нагрузка снижается. Ревьюер фокусируется на важном, а не ищет пропущенные null-проверки.

В Distiq как раз сделали такого бота. Он интегрируется в GitLab, GitHub или GitVerse, анализирует каждый MR и пишет конкретные замечания прямо в код. Находит баги, уязвимости, проблемы с производительностью. Побробовали на паре проектов — экономит прилично времени на ревью, плюс закрывает типичные ошибки, которые люди пропускают.

Попробуйте Distiq для автоматического code review

AI-бот анализирует каждый MR/PR и оставляет комментарии с замечаниями. Интеграция за 2 минуты.

Попробовать бесплатно

Похожие статьи