6 антипаттернов для UI тестирования на JavaScript, которых следует избегать
Jonathan ThompsonИзучите распространенные плохие практики и способы реализации правильных решений.
![](/file/e58a7c4d31a980acb6b51.jpg)
Популярность автоматизации тестирования растет, поскольку все больше и больше компаний стремятся предоставлять программные решения для ручного тестирования.
Что касается долларов США, то, согласно прогнозам, область автоматизации тестирования вырастет с 12,6 млрд долларов в 2019 году до 28,8 млрд долларов к 2024 году. Это рост более чем на 100% в течение пяти лет. Такой астрономический рост откроет множество возможностей.
К сожалению, новые возможности могут также привести к увеличению практики плохого тестирования. В этой статье мы рассмотрим шесть наиболее распространенных антипаттернов автоматизации тестирования в данной области, чтобы новые команды могли их избегать.
Ожидание
Современные фреймворки автоматизации, такие как Cypress и Playwright, используют неявное и явное ожидание, пока страница не загрузится, элемент не станет видимым или с ним можно будет взаимодействовать. Это гарантирует, что тесты будут менее хрупкими и подверженными сбоям, давая браузеру время для загрузки в тестируемое состояние.
Пример правильного использования ожидания:
// Cypress cy.get('#sampleHeading'); // Playwright page.waitForSelector('#sampleHeading');
Наиболее распространенным антипаттерном ожидания является ожидание явного таймаута между исполнениями кода. Выполнение останавливается, а затем возобновляется после завершения таймаута. Это приводит к задержке выполнения теста и замедляет общий запуск теста.
Пример неправильного использования ожидания:
// Cypress cy.get('#newHeadingButton').click(); cy.wait(1000); cy.get('#newHeading').should('be.visible'); // Playwright page.click('#newHeadingButton'); page.waitForTimeout(1000); const visible = page.$('#newHeading').isVisible(); expect(visible).toBeTruthy();
Вместо написания вышеприведенного кода, используйте встроенные стратегии ожидания, такие как ожидание элементов или ответов. Учтите, что нажатие кнопки “newHeadingButton”, в приведенном выше примере, отправляет запрос. Мы можем дождаться завершения запроса, прежде чем предпринять следующее действие.
// Cypress cy.intercept('**/headings').as('headingRequest'); cy.get('#newHeadingButton').click(); cy.wait('@headingRequest'); cy.get('#newHeading').should('be.visible'); // Playwright const [response] = Promise.all([ page.waitForResponse('**/headings'), page.click('#newHeadingButton'), ]); const visible = page.waitForSelector('#newHeading').isVisible(); expect(visible).toBeTruthy();
Воспринимая UI тестирование слишком буквально
Если вы или ваши инженеры настраиваете состояние для теста с помощью UI, возможно, вы участвуете в плохой практике тестирования. Создание состояния через пользовательский интерфейс является слишком трудозатратным и может способствовать созданию нестабильного теста. Вместо этого используйте существующую реализацию приложения в своих интересах, чтобы создать герметично закрытые пути пользователя.
При тестировании входа в систему для нового пользователя, не пытайтесь создать нового пользователя через UI, а затем войти в систему. Такой тест не будет герметичным, поскольку теперь он тестирует два пользовательских пути:
- Регистрация нового пользователя
- Вход в приложение
Вместо этого создайте запрос для нового пользователя, используя API вашего приложения, а затем попытайтесь войти в систему с новыми учетными данными. Это можно сделать без особых усилий, используя встроенную библиотеку запросов Cypress и алиасы.
// Cypress cy.request({ method: 'POST', url: 'https://myapplication.com/new-user', body: { username: 'test.user', password: 'Test1234!' } }).then(($response) => { cy.wrap($response).as('newUser'); }); // Test code cy.get('@newUser').then(($user) => { cy.fillLoginForm($user); });
Аналогичный паттерн можно использовать для Playwright с помощью библиотеки JavaScript HTTP, такой как Requestify.
// Playwright function createNewUser() { requestify .request('https://myapplication.com/new-user', { method: 'POST', body: { username: 'test.user', password: 'Test1234!' }, }) .then(async ($response) => await $response.getBody()); } // Test code const user = createNewUser(); loginPage.fillLoginForm(user);
Тестирование независимо от CI
Еще один распространенный антипаттерн в автоматизации тестирования - запускать тесты независимо от непрерывной интеграции. Прямым результатом является то, что неудачные тесты не имеют немедленных последствий. При запуске в CI неудачный тест может помешать мержу сборки. Кроме того, неудачный тест может стать призывом к команде разработчиков обратить внимание на фейл.
Инженеры по тестированию должны работать с инженерами по надежности сайта или по инфраструктуре, чтобы гарантировать, что тесты выполняются для каждой сборки или каждого мержа. Тесты можно даже запускать с фиксацией или предварительным запуском при использовании таких пакетов, как Pre-Commit, если группа инженеров решит принять такой инструмент.
Я лично рекомендую использовать Pre-Commit в команде и запускать линтинг / форматирование на pre-commit, а затем модульные тесты на pre-push. Поскольку тесты пользовательского интерфейса занимают больше времени, я бы запускал их для каждой сборки или для каждого мержа, используя что-то вроде Github Actions или рабочего процесса Jenkins.
Поведенческие накладные расходы
Раньше я был большим сторонником BDD, настолько, что написал стандарт лучших практик для двух компаний, в которых я работал в прошлом. Тем не менее, я отказался от использования BDD после того, как пришел к выводу, что этот процесс практически не дает никакой ценности, выступая в качестве источника боли при рефакторинге.
BDD инструменты, такие как Cucumber, прекрасно подходят для работы в командах, где все увлеклись искусством BDD. Однако слишком часто именно команда тестирования пишет на Gherkin, в то время как другие аспекты бизнеса либо игнорируют практику, либо не вносят свой вклад. В этом случае BDD теряет свою ценность как инструмент сотрудничества между отделами. Вместо того чтобы приблизить управление продуктом, разработку и контроль качества, эта практика приводит к отчуждению контроля качества.
Еще одной причиной удаления BDD является объем накладных расходов, связанных с написанием и рефакторингом теста. Рассмотрим путешествие пользователя, в котором обычный пользователь переходит на страницу входа в систему, отправляет достоверную информацию и проверяет результат.
// login.feature Feature: Application Login As a user, I would like to login to the application Scenario: Login with valid information Given we visit the login page When we submit valid credentials Then we should redirect to our profile page
Теперь нам нужен файл с шагами.
Given('we visit the login page', () => { // Cypress cy.visit('/login'); // Playwright loginPage.navigate(); }); When('we submit valid credentials', () => { // Cypress cy.fillForm(user); cy.get('#submitButton').click(); // Playwright loginPage.fillForm(user); loginPage.submitForm(); }); Then('we should redirect to our profile page, () => { // Cypress cy.get('#successMessage').should('be.visible'); // Playwright const visible = page.waitForSelector('#successMessage').isVisible(); expect(visible).toBeTruthy(); });
Что делать, если процесс входа в систему больше не перенаправляет на страницу профиля?
Вместо рефакторинга в одном месте без BDD, теперь вы должны выполнить рефакторинг в двух шагах: steps и feature. Самая очевидная проблема с BDD - это то, что на выполнение небольших рефакторингов требуется вдвое больше времени, чем если бы вы вообще не использовали BDD. Вместо того, чтобы иметь единственный источник истины для ваших тестовых путешествий, у вас есть два отдельных модуля, которые должны быть синхронизированы, чтобы функционировать должным образом.
Вместо использования BDD вам следует писать более декларативные пользовательские путешествия.
// Cypress specify('As a user, I should be able to login with valid data', () => { cy.visit(`${Cypress.config('baseUrl')}/login`); cy.get('@newUser').then(($user) => { cy.fillForm($user); }); cy.get('#submitButton').click(); cy.get('#successMessage']).should('be.visible'); }); // Playwright it('As a user, I should be able to login with valid data', () => { loginPage = Login(page); loginPage.fillForm(newUser); loginPage.submitForm(); const visible = page.waitForSelector('#successMessage'); expect(visible).toBeTruthy(); });
Описанные выше путешествия достаточно декларативны для читателя (будь то тестировщик, менеджер продукта или разработчик), что тест берет пользователя, вводит допустимые данные и успешно отправляет их. Никаких шагов, функций или сложных накладных расходов не требуется, просто обычный JavaScript.
Условия
Тестировщики должны стремиться удалить как можно больше условной логики из теста, чтобы уменьшить нестабильность. При тестировании с условиями создаются недетерминированные тесты, которые сложно устранить и выполнить с уверенностью.
Я работал в компании, которой требовалось протестировать стороннюю интеграцию. Часть этого теста заключалась в том, чтобы войти в систему через третье лицо, а затем перейти в их приложение. Из-за нестабильности тестов состояние аутентификации оказалось непредсказуемым. Они реализовали условную логику, чтобы попытаться справиться с ней на предыдущем шаге.
- Если пользователь вошел в систему, выйдите из системы, а затем войдите в систему
- Если пользователь не вошел в систему, войдите в систему
Из-за этой условной логики тесты были крайне ненадежными. Иногда они проходили, хотя в большинстве случаев полностью терпели неудачу. Я решил удалить условную логику в пользу метода выхода из системы по API. Пользователю всегда приходилось входить в систему (если выполнение теста не было прервано вручную), что делало тесты намного более надежными.
Вместо использования условий, инженеры должны использовать API-интерфейсы для построения состояния теста в максимально возможной степени, будь то запросы или имитация ответов.
Давайте рассмотрим пример приложения, которое находится в разгаре кампании A/B-тестирования. Приложение представляет собой книжный магазин, который отображает определенную книгу для группы A и другую книгу для группы B. Вместо условной логики мы могли бы реализовать макет ответа, чтобы всегда показывать книгу для группы A.
// Cypress cy.intercept('**/books', { fixture: 'book.json' }); cy.contains('a', bookTitle).should('be.visible'); // Playwright await page.route('**/books', ($route) => $route.fulfill({ path: 'book.json' }), ); visible = page.$(`a >> text=${bookTitle}`).isVisible(); expect(visible).toBeTruthy();
Теперь наш тест детерминирован и надежен, поскольку мы построили тестовое состояние с помощью API, а не пытались сделать это через пользовательский интерфейс.
Выбор неправильных селекторов
Часто использование правильных критериев выбора селектора может быть сложной задачей. Распространенным антипаттерном в автоматизации тестирования является использование очень хрупких селекторов при создании Page Object или написании тестов. Хрупкие селекторы - это те, которые могут измениться из-за рефакторинга реализации. Неправильным критерием выбора будет использование неуникальных идентификаторов и классов или Xpath.
Cypress специально облегчает эту боль, применяя методы JQuery .first() и .last(), а также методы обхода DOM, такие как .sibling() и .parent().
В большинстве случаев тестировщики должны синхронизироваться со своей командой разработчиков, чтобы гарантировать добавление уникальных селекторов в кодовую базу. К ним относятся такие селекторы, как data-id, data-test-id, data-cy или любые их варианты. Их можно использовать как любой атрибут во фреймворке автоматизации.
// Cypress cy.get('[data-id=submit-button]').click(); // Playwright page.click('[data-id=submit-button]');
Итог
Теперь мы можем идентифицировать и избегать шесть наиболее распространенных антипаттернов в автоматизации тестирования. Имейте в виду, что это не единственные антипаттерны в мире автоматизации тестирования. Продолжая работать в отрасли, вы будете обращать внимание на другие паттерны, которых следует избегать, работая над внедрением передовых методов тестирования.
Переведено командой QApedia. Подписывайтесь на наш канал.