Топ-10 ошибок в автотестах на Java которые я вижу каждую неделю
Tech Lead из Ozon и VK разбирает 10 самых частых ошибок в Java-автотестах: отсутствие архитектуры, кривые локаторы, слабое знание языка, устаревший стек, игнорирование JUnit Extensions и нейросетей. С примерами кода как делать правильно.
Топ-10 ошибок в автотестах на Java которые я вижу каждую неделю
Я провожу code review автотестов каждую неделю — в рамках менторства, на мок-собеседованиях и в командах где работаю. За несколько лет сложился устойчивый список ошибок которые встречаются снова и снова, независимо от компании и уровня специалиста. Некоторые из них делают даже Middle-инженеры с 2–3 годами опыта.
Это не теория из учебника. Это реальные проблемы из реальных проектов. Разберём каждую с примером как делают и как надо.
Ошибка #1: Нет архитектуры — нельзя переиспользовать компоненты
Самая распространённая и самая дорогостоящая ошибка. Тесты написаны как набор независимых скриптов: каждый тест сам открывает браузер, сам находит элементы, сам делает всё с нуля. Когда меняется один элемент на странице — нужно обновлять 40 тестов вместо одного.
Как делают неправильно:
1// Плохо: логика дублируется в каждом тесте
2@Test
3void testCheckout() {
4 open("https://shop.example.com/login");
5 $("#email").setValue("user@test.com");
6 $("#password").setValue("pass123");
7 $("button[type=submit]").click();
8 // ... 30 строк логики покупки
9}
10
11@Test
12void testOrderHistory() {
13 open("https://shop.example.com/login");
14 $("#email").setValue("user@test.com"); // дублирование
15 $("#password").setValue("pass123"); // дублирование
16 $("button[type=submit]").click(); // дублирование
17}Как надо — Page Object Model + степы:
1// Хорошо: логика в одном месте, тесты читаемые
2public class LoginPage {
3 private final SelenideElement email = $("#email");
4 private final SelenideElement password = $("#password");
5 private final SelenideElement submit = $("button[type=submit]");
6
7 public MainPage loginAs(String email, String password) {
8 this.email.setValue(email);
9 this.password.setValue(password);
10 submit.click();
11 return new MainPage();
12 }
13}
14
15@Test
16void testCheckout() {
17 new LoginPage().loginAs("user@test.com", "pass123")
18 .goToCart()
19 .checkout();
20}Правило простое: если одно и то же действие встречается в двух тестах — оно должно быть в Page Object или степе. Не в тесте.
Ошибка #2: Кривые локаторы
Хрупкие локаторы — вторая по частоте причина нестабильных тестов. Тест падает не потому что сломалась функциональность, а потому что разработчик переименовал CSS-класс или изменил структуру DOM.
1// Плохо: хрупкие локаторы
2$("div.sc-bdXxxt.fKBQlc > span:nth-child(2)").click(); // XPath по структуре
3$(".btn-primary-large-rounded-shadow").click(); // CSS по визуальному классу
4$("//div[3]/button[1]").click(); // XPath по позиции
5
6// Хорошо: стабильные локаторы
7$("[data-testid='submit-button']").click(); // атрибут для тестов
8$("[data-qa='login-form']").click(); // явный QA-атрибут
9$(byText("Оформить заказ")).click(); // по тексту (если текст стабилен)
10$("#checkout-submit").click(); // по IDИерархия надёжности локаторов (от лучшего к худшему): data-testid / data-qa → ID → aria-label → текст → CSS по семантическому классу → XPath по структуре. Если в проекте нет data-testid — договоритесь с разработчиками добавить их. Это занимает 10 минут и экономит часы отладки.
Ошибка #3: Слабое знание Java
QA-инженеры часто учат Java ровно настолько чтобы написать тест. Это работает до первого code review. Потом выясняется что человек не знает generics, не понимает разницу между equals() и ==, не умеет работать со Stream API и пишет циклы там где нужна одна строка.
1// Плохо: не знает Stream API
2List<String> activeUsers = new ArrayList<>();
3for (User user : users) {
4 if (user.isActive()) {
5 activeUsers.add(user.getEmail());
6 }
7}
8
9// Хорошо: Stream API
10List<String> activeUsers = users.stream()
11 .filter(User::isActive)
12 .map(User::getEmail)
13 .toList();
14
15// Плохо: сравнение строк через ==
16if (status == "ACTIVE") { ... }
17
18// Хорошо: null-safe сравнение
19if (Objects.equals(status, "ACTIVE")) { ... }Минимум который должен знать Java QA Automation инженер: ООП (наследование, интерфейсы, абстрактные классы), generics, коллекции (List, Map, Set), Stream API, Optional, лямбды, исключения. Без этого код будет работать, но его будет стыдно показывать на собеседовании.
Ошибка #4: Устаревший стек
В 2026 году я всё ещё вижу проекты на JUnit 4, Selenium 3, TestNG без причины. Это не просто эстетическая проблема — старый стек медленнее, менее функционален и создаёт проблемы при найме.
| Устарело | Актуально в 2026 | Почему менять |
|---|---|---|
| JUnit 4 | JUnit 5 | Extensions, параметризация, вложенные тесты |
| Selenium 3 + явные ожидания | Selenide 7 / Playwright | Автоожидания, читаемость, скорость |
| TestNG (без причины) | JUnit 5 | Лучшая экосистема, активная разработка |
| RestTemplate в тестах | REST Assured 5+ | Специализированный инструмент для API |
| Allure 2.x | Allure 3 / Allure TestOps | Интеграция с CI, история запусков |
| Jenkins (legacy) | GitLab CI / GitHub Actions | Декларативные пайплайны, проще поддержка |
Переход с JUnit 4 на JUnit 5 занимает день. Переход с Selenium на Selenide — неделю. Это инвестиция которая окупается за месяц.
Ошибка #5: Повторяющиеся тесты без параметризации
Классика: три теста которые делают одно и то же с разными данными. Вместо одного параметризованного теста — три копии с разными значениями в коде.
1// Плохо: три одинаковых теста
2@Test void loginWithGmail() { loginPage.loginAs("user@gmail.com", "pass"); }
3@Test void loginWithCorporate() { loginPage.loginAs("user@company.ru", "pass"); }
4@Test void loginWithSubdomain() { loginPage.loginAs("u@mail.example.com","pass"); }
5
6// Хорошо: @ParameterizedTest
7@ParameterizedTest
8@ValueSource(strings = {"user@gmail.com", "user@company.ru", "u@mail.example.com"})
9void loginWithValidEmail(String email) {
10 loginPage.loginAs(email, "pass");
11 assertThat(mainPage.isLoaded()).isTrue();
12}
13
14// Для сложных данных — @MethodSource
15@ParameterizedTest
16@MethodSource("validCredentials")
17void loginWithValidCredentials(String email, String password, String expectedRole) {
18 loginPage.loginAs(email, password);
19 assertThat(userProfile.getRole()).isEqualTo(expectedRole);
20}
21
22static Stream<Arguments> validCredentials() {
23 return Stream.of(
24 Arguments.of("admin@test.com", "admin123", "ADMIN"),
25 Arguments.of("user@test.com", "user123", "USER"),
26 Arguments.of("mentor@test.com", "mentor123", "MENTOR")
27 );
28}Ошибка #6: Не используют JUnit 5 Extensions
JUnit 5 Extensions — один из самых мощных инструментов фреймворка. Они позволяют вынести повторяющуюся логику (setup/teardown, логирование, скриншоты, управление браузером) в одно место. Большинство QA-инженеров знают только @BeforeEach и @AfterEach и не подозревают о существовании Extensions.
1// Хорошо: Extension для автоматических скриншотов при падении
2public class ScreenshotExtension implements TestWatcher {
3 @Override
4 public void testFailed(ExtensionContext context, Throwable cause) {
5 String name = context.getDisplayName();
6 Selenide.screenshot("screenshots/" + name);
7 Allure.addAttachment("Screenshot", new FileInputStream("screenshots/" + name + ".png"));
8 }
9}
10
11// Хорошо: Extension для управления браузером
12public class BrowserExtension implements BeforeEachCallback, AfterEachCallback {
13 @Override
14 public void beforeEach(ExtensionContext ctx) {
15 Configuration.browser = "chrome";
16 Configuration.headless = true;
17 Configuration.baseUrl = System.getProperty("base.url", "https://app.example.com");
18 }
19 @Override
20 public void afterEach(ExtensionContext ctx) { closeWebDriver(); }
21}
22
23// Использование — чистые тесты без setup/teardown
24@ExtendWith({BrowserExtension.class, ScreenshotExtension.class})
25class LoginTests {
26 @Test
27 void loginTest() { /* только бизнес-логика */ }
28}Другие полезные Extension: ParameterResolver для инжекции зависимостей в тесты, ExecutionCondition для условного запуска, @RegisterExtension для динамической регистрации.
Ошибка #7: Thread.sleep() вместо ожиданий
Thread.sleep() — признак того что инженер не понимает как работает асинхронность в браузере. Тест либо ждёт слишком долго (медленный), либо недостаточно (нестабильный).
1// Плохо
2$("#submit").click();
3Thread.sleep(3000); // ждём 3 секунды вслепую
4$("#success").shouldBe(visible);
5
6// Хорошо: Selenide ждёт автоматически (до 4 сек по умолчанию)
7$("#submit").click();
8$("#success").shouldBe(visible);
9
10// Хорошо: кастомное время ожидания если нужно
11$("#heavy-report").shouldBe(visible, Duration.ofSeconds(30));
12
13// Хорошо: для чистого Selenium — WebDriverWait
14new WebDriverWait(driver, Duration.ofSeconds(10))
15 .until(ExpectedConditions.visibilityOfElementLocated(By.id("success")));Ошибка #8: Игнорирование нейросетей в работе
В 2026 году QA-инженер который не использует AI-инструменты работает в 2–3 раза медленнее конкурентов. Нейросети не заменяют инженера — они убирают рутину.
- ▸Генерация тестовых данных: попросите ChatGPT/Claude сгенерировать 50 вариантов невалидных email-адресов для негативных тестов — 10 секунд вместо 10 минут
- ▸Boilerplate-код: Page Object для новой страницы, POJO-классы для API-ответов, конфигурационные классы — генерируется за секунды
- ▸Анализ упавших тестов: вставьте стектрейс в Claude и получите объяснение причины и варианты исправления
- ▸Рефакторинг: попросите улучшить читаемость метода или предложить более идиоматичный Java-код
- ▸Граничные случаи: опишите функциональность и получите список edge cases которые вы могли пропустить
Главное правило: AI генерирует черновик, инженер проверяет и дорабатывает. Слепо копировать код из ChatGPT — такая же ошибка как не использовать его вообще.
Ошибка #9: Тесты зависят друг от друга
Тест B падает потому что тест A не создал нужные данные. Или тест C проходит только если запускать после теста B. Это делает тест-сьют хрупким: нельзя запустить один тест, нельзя запустить в параллель, нельзя изменить порядок.
1// Плохо: тест зависит от состояния после предыдущего
2@Test @Order(1)
3void createUser() { /* создаёт пользователя в БД */ }
4
5@Test @Order(2)
6void updateUser() { /* упадёт если createUser не запускался */ }
7
8// Хорошо: каждый тест самодостаточен
9@Test
10void updateUser() {
11 // создаём данные прямо здесь через API
12 User user = userApi.create(UserFactory.defaultUser());
13 userApi.update(user.getId(), UpdateRequest.builder().name("New Name").build());
14 assertThat(userApi.get(user.getId()).getName()).isEqualTo("New Name");
15}@Order в JUnit 5 существует для специфичных сценариев — не для компенсации зависимостей между тестами. Каждый тест должен сам создавать нужные данные и сам убирать за собой.
Ошибка #10: Нет разделения на слои (UI / API / DB)
Зрелый тест-фреймворк использует разные слои для разных задач. Когда всё делается через UI — тесты медленные и хрупкие.
1// Плохо: всё через UI, включая подготовку данных (5 минут на тест)
2@Test
3void testOrderHistory() {
4 loginPage.loginAs("user", "pass");
5 catalogPage.open().addToCart("Product A");
6 cartPage.checkout().fillAddress(address).pay();
7 orderHistoryPage.open().assertOrderExists("Product A");
8}
9
10// Хорошо: подготовка через API, UI только для проверки отображения (30 секунд)
11@Test
12void testOrderHistory() {
13 String token = authApi.login("user", "pass");
14 Order order = orderApi.create(token, OrderRequest.of("Product A"));
15
16 open("/orders");
17 $("[data-testid='order-" + order.getId() + "']").shouldBe(visible);
18}Правило: используй самый быстрый и надёжный способ для каждой задачи. Создание данных — API или DB. Проверка бизнес-логики — API. Проверка отображения — UI. Это пирамида тестирования, и она работает.
Итого: чеклист перед code review
- Есть Page Object Model
логика не дублируется в тестах
- Локаторы используют data-testid / data-qa / ID
не XPath по структуре
- Код использует Stream API, Optional, лямбды
не for-циклы везде
- Стек актуальный: JUnit 5, Selenide 7+, REST Assured 5+
- Повторяющиеся тесты заменены на @ParameterizedTest
- Общая логика вынесена в JUnit 5 Extensions
- Нет Thread.sleep()
только автоожидания Selenide или WebDriverWait
- AI использовался для генерации boilerplate и тестовых данных
- Каждый тест самодостаточен
не зависит от других тестов
- Подготовка данных через API/DB
UI только для проверки отображения
Хочешь писать автотесты без этих ошибок с первого дня?
На курсе Java QA Automation от ThreadQA все эти практики заложены в основу с первого урока. Page Object Model, JUnit 5 Extensions, REST Assured, Selenide, параметризованные тесты, работа с AI — не как отдельные темы, а как единый подход к написанию качественного кода. 90 уроков, 40 часов, практика на реальном проекте. Первые уроки бесплатно.