Перевод статьи «Use cy.session() instead of login page object in Cypress».
Во многих случаях при автоматизации тестирования первым препятствием является работа с логином. Это само по себе может быть довольно сложной задачей.
Наиболее распространенный способ решения проблемы входа в систему – просто пройти через нее, как это сделал бы обычный пользователь. Давайте посмотрим, как это будет выглядеть в нашем приложении:
cy.visit('/login') cy.get('[data-cy=login-email]').type('filip@example.com') cy.get('[data-cy=login-password]').type('i<3slovakia!') cy.get('[data-cy=login-submit]').click() cy.location('pathname').should('eq', '/')
Cypress старается очищать данные браузера между тестами, что приводит к необходимости входа в систему перед тестом. Мы можем делать это с помощью хука beforeEach()
. Или же можно игнорировать удаление определенных cookies из нашего приложения с помощью cookies api.
Содержание:
- Добавление входа в систему в объект страницы
- Использование пользовательской команды
- Программный вход в систему
- Использование cy.session()
- Применение cy.session() ко всему проекту
- Создание нескольких сессий
- Зачем использовать пользовательскую команду вместо объекта страницы?
Подпишитесь на наш ТЕЛЕГРАМ КАНАЛ ПО АВТОМАТИЗАЦИИ ТЕСТИРОВАНИЯ
Добавление входа в систему в объект страницы
Имеет смысл вынести последовательность входа в систему в отдельную сущность. В качестве такого решения можно использовать объект страницы. Во многих случаях он может выглядеть примерно так:
export class LoginPage { username: string password: string log_in: string constructor() { this.username = '[data-cy=login-email]' this.password = '[data-cy=login-password]' this.log_in = '[data-cy=login-submit]' } /** * opens a login page */ load() { cy.visit('/login') } /** * fills in username, password and submits form * @param username username of user to log in * @param pass password of the user */ login(username: string, pass: string) { cy.get(this.username).type(username) cy.get(this.password).type(pass) cy.get(this.log_in).click() cy.location('pathname').should('eq', '/') } }
Таким образом мы можем легко входить в систему перед каждым тестом и готовить тест перед выполнением действий в нашем тестовом сценарии. Если вы хотите сделать это глобально, можно просто добавить глобальный хук beforeEach()
в ваш файл поддержки:
import { LoginPage } from '../support/models/LoginPage' beforeEach( () => { loginPage.load() loginPage.login('filip@example.com', 'i<3slovakia!') })
Использование пользовательской команды
Лично я для таких широко используемых функций предпочитаю использовать пользовательские команды. Большим преимуществом пользовательских команд является то, что они становятся частью вашей библиотеки Cypress. Благодаря этому их легко найти, к тому же они хорошо сочетаются с синтаксисом цепочек Cypress. Также можно создавать снимки DOM для отладки и многое другое.
Пользовательская команда для входа в систему может выглядеть следующим образом:
declare global { namespace Cypress { interface Chainable { /** * Logs in with a given user * @param email email of the user you want to log in * @param password user passwird * @example * cy.login('filip@example.com', 'i<3slovakia!') * */ login: typeof login } } } const login = (email: string, password: string) => { cy.visit('/login') cy.get('[data-cy=login-email]').type(email) cy.get('[data-cy=login-password]').type(`${password}`) cy.get('[data-cy=login-submit]').click() cy.location('pathname').should('eq', '/') }; Cypress.Commands.addAll({ login })
Строки 1 – 15 – это добавление нашей пользовательской команды в библиотеку Cypress путем расширения определений TypeScript. Строки 17 – 24 – это определение функции, содержащей последовательность входа в систему. Наконец, в строке 26 мы добавляем нашу функцию.
Аналогично примеру с объектом страницы, мы можем добавить созданную нами команду cy.login()
в глобальный хук beforeEach()
и заставить наш тест входить в систему перед каждым блоком it()
.
beforeEach( () => { cy.login('filip@example.com', 'i<3slovakia!') })
Программный вход
Однако использование пользовательского интерфейса – не самый эффективный способ. Он непременно скажется на производительности. Второй вариант – это программный вход в систему. Но, конечно, зачастую это легче сказать, чем сделать.
При программном входе в систему (да и при любом другом входе) присутствуют три составляющие:
- Сервер
- Фронтенд
- Браузер
При входе в систему сервер предоставляет данные (обычно в виде токена), а затем фронтенд сохраняет их в браузере (обычно в виде cookies). Чтобы входить в систему программно, необходимо понимать, как ваше приложение отправляет данные на сервер и как обрабатывает ответ. По сути, необходимо знать точно, что происходит, когда пользователь заполняет информацию и нажимает кнопку “Войти”.
Другими словами, вы будете заново воссоздавать этот вход в своем тесте. Это довольно быстро, если разобраться, но не просто. Особенно если вам нужно разобраться со входом через сторонние сервисы, потоками OAuth и другими более сложными методами входа. Для этого существует множество полезных руководств в документации по Cypress.
Использование функции cy.session()
Но на самом деле чтобы быть эффективным не обязательно разбираться с логином. С помощью команды cy.session()
вы можете использовать свой пользовательский интерфейс всего один раз для всего набора тестов.
Команда cy.session()
появилась в версии 8.2.0. Это был один из релизов, который, на мой взгляд, не получил должного внимания. Это одна из самых эффективных вещей, которые вы можете сделать для входа в систему. Позвольте мне привести простой пример ее использования и показать, как она работает. Для этого мы будем использовать пользовательскую команду, показанную выше.
it('logs in the user', () => { cy.session('performLoginSequence', () => { cy.login('filip@example.com', 'i<3slovakia!') }) cy.visit('/') })
В нашем тесте мы обернули нашу команду cy.login()
в команду cy.session()
. Это дает Cypress информацию о том, что все, что выполняется внутри вызова cy.session()
(другими словами, между строками 3 и 5), должно быть запомнено как сессия. Когда мы снова запустим этот тест, вместо того, чтобы проходить процедуру входа в систему, наша сессия будет восстановлена.
Обратите внимание на следующее изображение, где показан первый запуск (вверху) и второй запуск (внизу).
Как видно, во втором запуске происходит восстановление сессии, а в первом – ее создание. Также можно заметить, что второй тест занимает лишь малую часть времени по сравнению с первым.
Работает это примерно так:
- Cypress проводит тестирование.
- Наткнувшись на
cy.session()
, он примет решение:- Если сессия с именем
performLoginSequence
не существует, выполняется код внутриcy.session()
. - Если сессия с именем
performLoginSequence
существует, сессия восстанавливается.
- Если сессия с именем
По сути, восстановление сессии означает восстановление всех куки-файлов браузера, локального хранилища и сеансового хранилища, которые были в браузере после входа пользователя в систему. Помните, мы говорили о программном входе в систему и трех частях потока входа? Так вот, cy.session()
берет на себя браузерную часть, а значит, вам не нужно беспокоиться о фронтенде и сервере.
Применение функции cy.session() ко всему проекту
Теперь мы можем перенести сессию на весь проект, вставив последовательность cy.session()
в файл поддержки:
beforeEach( () => { cy.session('loginTestingUser', () => { cy.login('filip@example.com', 'i<3slovakia!') }, { cacheAcrossSpecs: true }) })
Обратите внимание на опцию cacheAcrossSpecs
в строке 5. Это позволит выполнить вход в систему только один раз за весь тестовый прогон. Представьте, что каждая последовательность входа в систему занимает 2 секунды. Если у вас 100 тестов, которые входят в систему, то вы только что сократили время выполнения теста более чем на 3 минуты!
Создание нескольких сеансов
Допустим, у вас есть несколько пользователей, на которых вы хотите протестировать свой пользовательский интерфейс. Первый аргумент команды cy.session()
на самом деле является псевдонимом для сессии. Это означает, что мы можем создать несколько сессий и дать им разные имена. Самый простой способ добиться этого – изменить порядок написания команд. Вместо того чтобы оборачивать команду cy.login()
в команду cy.session()
, давайте сделаем cy.session()
частью нашей команды cy.login()
:
declare global { namespace Cypress { interface Chainable { /** * Logs in with a given user * @param email email of the user you want to log in * @param password user passwird * @example * cy.login('filip@example.com', 'i<3slovakia!') * */ login: typeof login } } } const login = (email: string, password: string) => { cy.session(email, () => { cy.visit('/login') cy.get('[data-cy=login-email]').type(email) cy.get('[data-cy=login-password]').type(`${password}`) cy.get('[data-cy=login-submit]').click() cy.location('pathname').should('eq', '/') }, { cacheAcrossSpecs: true }) }; Cypress.Commands.addAll({ login })
Обратите внимание, что в строке 17 мы присваиваем нашей сессии то же имя, что и email, который мы передаем в функцию cy.login()
. Это означает, что каждый пользователь, с которым мы входим в систему, будет создавать свою собственную сессию. Другими словами, сколько бы мы ни входили в систему с разными пользователями, каждый из них будет входить в систему только один раз.
Зачем использовать пользовательскую команду вместо объекта страницы?
Возможно, в прошлом вы слышали от меня противоречивые высказывания об объектной модели страницы. Я критикую ее за то, как она используется, но в то же время утверждаю, что она не является антипаттерном. Хотя абстракции имеют смысл, важно то, как их делать.
В настоящее время очень популярно использование объектной модели страниц для создания абстракций и DRY-кода. (DRY – один из принципов программирования, расшифровывается как Don’t repeat yourself – “Не повторяйся”). Я поддерживаю DRY-код, и я думаю, что как тестировщики мы должны стремиться к DRY-тестированию.
Объекты страницы часто используются для установки состояния приложения. В принципе, если вы хотите протестировать состояние “B”, то для этого нужно выполнить “A”. Если “А” – это вход в систему, то, возможно, вам придется выполнять его во всех тестах. Команда cy.session() позволяет ограничить количество раз, когда вы выполняете “А”. Это не обязательно должен быть вход в систему. Вы можете выполнить настройку, вызвав кучу конечных точек API или выполнив другие действия по настройке.
Использование пользовательских команд или пользовательских функций имеет больший смысл, чем использование объекта страницы UI. Но даже если вы решите выполнить настройку через UI, вы можете сэкономить много времени, используя cy.session()
.
Вам будет трудно использовать cy.session()
с объектом страницы. Это не невозможно, но вам, возможно, придется поместить все действие входа в систему в одну функцию, где вы посещаете, заполняете и отправляете форму входа. Такой объект страницы будет выглядеть примерно так:
export class LoginPage { username: string password: string log_in: string constructor() { this.username = '[data-cy=login-email]' this.password = '[data-cy=login-password]' this.log_in = '[data-cy=login-submit]' } /** * fills in username, password and submits form * @param username username of user to log in * @param pass password of the user */ loginAndFill(username: string, pass: string) { cy.session(username, () => { cy.visit('/login') cy.get(this.username).type(username) cy.get(this.password).type(pass) cy.get(this.log_in).click() cy.location('pathname').should('eq', '/') }, { cacheAcrossSpecs: true }) } }
В данном случае объект нашей страницы содержит всего одну функцию, и добавлять к ней другие не имеет смысла, так как это приведет к нарушению сессии. Поэтому лучше выбрать более простой паттерн, например, пользовательскую команду или отдельный функциональный блок, который импортируется в тест.
Пингбэк: Большой учебник по Cypress