Разделы портала

Онлайн-тренинги

.
Используйте cy.session() вместо login page object в Cypress
17.10.2023 00:00

Автор: Филип Рик (Filip Hric).
Оригинал статьи
Перевод: Ольга Алифанова

Авторизация – зачастую первое препятствие, с которым сталкиваются автоматизаторы, и преодолеть его бывает непросто.

Наиболее распространенный способ решения вопроса авторизации – просто пройти ее так, как это делает обычный пользователь. Вот так это будет выглядеть в нашем приложении:


1  cy.visit('/login')
2  cy.get('[data-cy=login-email]').type(' Этот e-mail адрес защищен от спам-ботов, для его просмотра у Вас должен быть включен Javascript ')
3  cy.get('[data-cy=login-password]').type('i<3slovakia!')
4  cy.get('[data-cy=login-submit]').click()
5  cy.location('pathname').should('eq', '/')

Cypress пытается очищать данные браузера между тестами, поэтому авторизовываться надо перед каждым тестом. Это можно сделать при помощи хука beforeEach() или используя Cookies API, чтобы игнорировать удаление определенных куки из приложения.

Абстрагирование логина в Page Object

Имеет смысл абстрагировать последовательность авторизации в отдельную сущность. Готовое решение – это Page Object. Зачастую это будет выглядеть как-то так:

1  export class LoginPage {
2
3    username: string
4    password: string
5    log_in: string
6
7    constructor() {
8      this.username = '[data-cy=login-email]'
9      this.password = '[data-cy=login-password]'
10      this.log_in = '[data-cy=login-submit]'
11    }
12
13    /**
14     * opens a login page
15     */
16    load() {
17      cy.visit('/login')
18    }
19
20    /**
21     * fills in username, password and submits form
22     * @param username username of user to log in
23     * @param pass password of the user
24     */
25    login(username: string, pass: string) {
26
27      cy.get(this.username).type(username)
28      cy.get(this.password).type(pass)
29      cy.get(this.log_in).click()
30      cy.location('pathname').should('eq', '/')
31    }
32
33  }

Таким образом мы можем легко авторизовываться перед каждым тестом и настроить его до перехода к действиям тест-сценария. Если вы хотите подойти к вопросу глобально, просто добавьте глобальный хук beforeEach() в ваш файл поддержки:

support/e2e.ts
1  import { LoginPage } from '../support/models/LoginPage'
2
3  beforeEach( () => {
4
5    loginPage.load()
6    loginPage.login(' Этот e-mail адрес защищен от спам-ботов, для его просмотра у Вас должен быть включен Javascript ', 'i<3slovakia!')
7
8  })

Использование кастомной команды

Для таких широко используемых функций, как авторизация, лично я предпочитаю использовать кастомные команды. Огромное преимущество кастомных команд в том, что они становятся частью вашей библиотеки Cypress. Следовательно, их легко найти, и они хорошо взаимодействуют с синтаксисом цепочек Cypress. Можно также создать снимки состояний DOM для дебага, и сделать многое другое. Я детальнее писал об этом в прошлой статье. Кастомная команда для логина выглядит примерно так:

1  declare global {
2    namespace Cypress {
3      interface Chainable {
4        /**
5         * Logs in with a given user
6         * @param email email of the user you want to log in
7         * @param password user passwird
8         * @example
9         * cy.login(' Этот e-mail адрес защищен от спам-ботов, для его просмотра у Вас должен быть включен Javascript ', 'i<3slovakia!')
10         *
11         */
12        login: typeof login
13      }
14    }
15  }
16
17  const login = (email: string, password: string) => {
18
19    cy.visit('/login')
20    cy.get('[data-cy=login-email]').type(email)
21    cy.get('[data-cy=login-password]').type(`${password}`)
22    cy.get('[data-cy=login-submit]').click()
23    cy.location('pathname').should('eq', '/')
24
25  };
26
27  Cypress.Commands.addAll({ login })
28

Строки 1 – 15 добавляют нашу кастомную команду в библиотеку Cypress, расширяя определения TypeScript. Строки 17-24 – определение функции, содержащей последовательность логина. И, наконец, мы добавляем нашу функцию в строке 26. Схожим с Page Object образом мы можем добавить нашу свежесозданную команду cy.login() в глобальный хук beforeEach(), и тест будет авторизовываться перед каждым блоком it().

support/e2e.ts
1  beforeEach( () => {
2    cy.login(' Этот e-mail адрес защищен от спам-ботов, для его просмотра у Вас должен быть включен Javascript ', 'i<3slovakia!')
3  })

Программируемый логин

Однако использование пользовательского интерфейса – не самый эффективный способ, и это уж точно повлияет на производительность. Второй вариант – авторизовываться программно. Конечно, проще сказать, чем сделать. В программном (как и любом другом) логине три части:

  1. Сервер.
  2. Фронтэнд.
  3. Браузер.

При авторизации сервер предоставляет данные (как правило, в форме токена), а фронтэнд затем сохраняет их в браузер (как правило, в форме куки). Чтобы иметь возможность авторизоваться программно, надо понимать, как ваше приложение передает данные на сервер, и как обращается с ответом. По сути вам нужно точно знать, что происходит, когда пользователь вводит данные и нажимает на кнопку «Войти».

Иными словами, вы будете воссоздавать этот логин в своем тесте. Как только вы разберетесь с ним, все будет выполняться очень быстро, но это нелегко, особенно если приходится иметь дело со сторонним логином, потоками OAuth и другими продвинутыми методами авторизации. В документации Cypress много полезных руководств на этот счет.

Использование cy.session()

Однако на самом деле для эффективной работы нет нужды разбираться, что там происходит при авторизации. Команда cy.session() позволяет воспользоваться пользовательским интерфейсом всего лишь раз за весь набор тестов.

Команда cy.session() вышла с версией 8.2.0. Этот релиз, на мой взгляд, получил меньше внимания, нежели заслуживал. Это один из самых эффективных способов справиться с авторизацией. Приведу простой пример использования, чтобы показать, как это работает. Мы будем использовать вышеупомянутую кастомную команду.

1  it('logs in the user', () => {
2
3    cy.session('performLoginSequence', () => {
4      cy.login(' Этот e-mail адрес защищен от спам-ботов, для его просмотра у Вас должен быть включен Javascript ', 'i<3slovakia!')
5    })
6
7    cy.visit('/')
8  })

В тесте мы обернули команду cy.login() в команду cy.session(). Это дает Cypress понять, что все, что происходит внутри обратного вызова cy.session() (иными словами, между строками 3 и 5), должно запоминаться как сессия. При повторном прогоне этого теста авторизации происходить не будет – вместо этого восстановится сессия. Обратите внимание на изображение, демонстрирующее первый прогон (первое изображение) и второй (второе изображение).


Можно видеть, что второй прогон восстановит сессию, а первый ее создает. Заметьте также, что второй тест занимает лишь долю времени, потраченного на первый.

Работает это вот как:

  • Cypress прогоняет тест.
  • Натыкаясь на cy.session(), он принимает решение:
    • Сессия с названием performLoginSequence не существует, запускаю код внутри вызова cy.session().
    • Сессия с названием performLoginSequence существует, восстанавливаю сессию.

Восстановление сессии – это извлечение всех куки браузера, локального хранилища и хранилища сессии, присутствовавших в браузере после авторизации пользователя. Помните, мы говорили о программной авторизации и трех частях авторизационного потока? cy.session() берет на себя функцию браузера, и вам не нужно волноваться за фронтэнд и сервер.

Применение cy.session() к проекту целиком

Теперь мы можем применить сессию к проекту в целом, вставив последовательность cy.session() в файл поддержки:

support/e2e.ts
1  beforeEach( () => {
2    cy.session('loginTestingUser', () => {
3      cy.login(' Этот e-mail адрес защищен от спам-ботов, для его просмотра у Вас должен быть включен Javascript ', 'i<3slovakia!')
4      }, {
5      cacheAcrossSpecs: true
6    })
7
8  })

Обратите внимание на опцию cacheAcrossSpecs в строке 5. Она заставляет авторизацию прогоняться только один раз за прогон набора тестов. Если логин занимает 2 секунды, и у вас сто тестов с авторизацией, вы только что сэкономили более трех минут!

Создание нескольких сессий

Допустим, у вас несколько пользователей, под которыми вы хотите протестировать UI. Первый аргумент команды cy.session() – это по сути синоним для сессии. Это означает, что мы можем создать несколько сессий, дав им разные имена. Проще всего сделать это, перевернув порядок наших команд. Вместо того, чтобы оборачивать команду cy.login() в cy.session(), сделаем cy.session() частью команды cy.login(), вот так:

1  declare global {
2    namespace Cypress {
3      interface Chainable {
4        /**
5         * Logs in with a given user
6         * @param email email of the user you want to log in
7         * @param password user passwird
8         * @example
9         * cy.login(' Этот e-mail адрес защищен от спам-ботов, для его просмотра у Вас должен быть включен Javascript ', 'i<3slovakia!')
10         *
11         */
12        login: typeof login
13      }
14    }
15  }
16
17  const login = (email: string, password: string) => {
18
19    cy.session(email, () => {
20      cy.visit('/login')
21      cy.get('[data-cy=login-email]').type(email)
22      cy.get('[data-cy=login-password]').type(`${password}`)
23      cy.get('[data-cy=login-submit]').click()
24      cy.location('pathname').should('eq', '/')
25    }, {
26      cacheAcrossSpecs: true
27    })
28
29  };
30
31  Cypress.Commands.addAll({ login })
32

Обратите внимание, что на строке 17 мы даем сессии имя, совпадающее с электронной почтой, которую мы передаем функции cy.login(). Это означает, что каждый авторизующийся пользователь будет создавать свою собственную сессию. Иными словами, вне зависимости от количества авторизаций под разными пользователями каждый из них авторизуется лишь один раз.

Почему кастомная команда, а не Page Object? Мое мнение

Возможно, вы уже сталкивались с моими возражениями против Page Object. Я критикую модель Page Object за то, как она применяется, но также говорю, что это не антипаттерн. Если в создании абстракций есть смысл, очень важен способ их реализации. Использование модели Page Object для создания абстракций и DRY-кода (Don’t Repeat Yourself) – это уже перебор. Я только за DRY-код, но мы, как тестировщики, должны также стремиться к DRY-выполнению тестов.

Page Object часто используются для настройки состояния приложения. По сути, желая протестировать «Б», вы сначала добираетесь туда, делая «А». Если это «А» - авторизация, она может понадобиться во всех ваших тестах. cy.session() позволяет ограничить количество повторений А. Это необязательно авторизация – можно также выполнять настройку, вызывая ряд конечных точек API и осуществляя другие подготовительные действия.

Использование кастомных команд или функций несет больше смысла, чем использование UI Page Object. Даже если вы хотите настраивать тесты через UI, то сбережете время, используя cy.session(). Использовать cy.session() с Page Object очень трудно. Это возможно, но по сути вам придется уместить всю авторизационную деятельность в одну функцию, в которой вы открываете, заполняете и отправляете форму авторизации. Такой Page Object будет выглядеть примерно так:

1  export class LoginPage {
2
3    username: string
4    password: string
5    log_in: string
6
7    constructor() {
8      this.username = '[data-cy=login-email]'
9      this.password = '[data-cy=login-password]'
10      this.log_in = '[data-cy=login-submit]'
11    }
12
13    /**
14     * fills in username, password and submits form
15     * @param username username of user to log in
16     * @param pass password of the user
17     */
18    loginAndFill(username: string, pass: string) {
19
20      cy.session(username, () => {
21        cy.visit('/login')
22        cy.get(this.username).type(username)
23        cy.get(this.password).type(pass)
24        cy.get(this.log_in).click()
25        cy.location('pathname').should('eq', '/')
26      }, {
27        cacheAcrossSpecs: true
28      })
29    }
30
31  }

В этом случае Page Object содержит одну-единственную функцию, и добавлять больше нет смысла, так как это разорвет сессию. Поэтому лучше выбирать паттерн попроще – кастомную команду или отдельный функциональный модуль, импортируемый в тест.

Обсудить в форуме