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

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

.
Основы Cypress: Переменные
04.03.2022 00:00

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

Если вы оказались здесь через Google-поиск, то, возможно, недоумеваете, почему подобный код не работает в Cypress:

it('stores value in variable', () => {
 
     let id
 
     cy.request('/api/boards')
          .then( res => {
 
          id = res.body[0].id
     })
 
     cy.visit('/board/' + id) // "id" is undefined?!
 
})

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

Почему же ID не определен? Копаясь в документации, можно слегка запутаться. Есть статья про асинхронность команд в Cypress, а также можно прочитать про обращение с переменными и попробовать async/await, но это тоже не сработает. Что же происходит?

Добавим несколько функций console.log() к нашему тесту и посмотрим, как он себя поведет. Можете ли вы, просто смотря на код, догадаться, что будет выведено в консоли браузера?

it('stores value in variable', () => {
     console.log('>>> first log')
     let id
 
     cy.request('/api/boards')
         .then( res => {
                console.log('>>> second log')
 
                 id = res.body[0].id
          })
     console.log('>>> third log')
 
      cy.visit('/board/' + id)
})

Возможно, вы верно угадали. Но я думаю, вам любопытно, почему ответ таков:

>>> first log
>>> third log
>>> second log

Как я сказал ранее, ответ есть в документации, но там можно слегка запутаться. Я, по крайней мере, запутался. Поэтому вот как можно в этом разобраться другим путем.

Cypress-цепочка VS все остальное

Команды Cypress запускаются цепочкой. Каждое звено цепочки привязано к предыдущему и к последующему. Так Cypress убеждается, что вы не попадаете в условия гонки, и будет автоматически ждать завершения предыдущей команды. Приведу пример.

cy
     .get('li')
     .should('have.length', 5) // ожидание, пока предыдущая команда найдет элементы
     .last() // ожидание срабатывания предыдущего ассерта
     .click() // ожидание завершения предыдущей команды.

Повторюсь, команда запускается только после того, как завершена предыдущая. Если любая из них не завершается вовремя (обычно в течении четырех секунд), тест падает.

Что же происходит с кодом, который находится вне цепочки? Так как он не часть цепочки, ничто не заставляет его ждать, и он запускается мгновенно.

Посмотрим на пример свежими глазами:

it('stores value in variable', () => {
     // вне цепочки, запускается немедленно
     console.log('>>> first log')
     let id
 
     cy.request('/api/boards')
          .then( res => {
               // внутри цепочки, ожидает завершения запроса
               console.log('>>> second log')
               id = res.body[0].id
          })
 
     // вне цепочки, запускается немедленно
     console.log('>>> third log')
 
     cy.visit('/board/' + id)
})

Надеюсь, что функции console.log() теперь понятнее. Но что насчет этой переменной id? Вроде как она используется внутри цепочки. Так ли это?

Вообще-то нет. Она передается как аргумент, и технически не находится внутри цепочки команд, а передана "извне". Мы объявили эту переменную в начале теста. Внутри теста мы говорим Cypress, что хотим выполнить команду .visit() с нужными '/board/' + id.

Это начинает приобретать смысл, когда мы пристально рассматриваем наш принцип "внутри цепочки и вне цепочки". Снова взглянем на код.

it('stores value in variable', () => {
     // не ждет объявления переменной
     let id
 
     cy.request('/api/boards')
          .then( res => {
               // ждет завершения запроса в тесте, затем приписывает новое значение
                id = res.body[0].id
          })
 
     // не ждет передачи переменной
     cy.visit('/board/' + id)
})

Теперь, когда проблемы яснее, разберемся, как можно передавать значения в тесте, используя разные методы. Для этого есть множество решений, рассмотрим хотя бы некоторые из них.

Возможные решения

Решение 1: Переместите нужный код внутрь цепочки команд

Самое простое решение – убедиться, что мы все включаем в цепочку команд. Для использования нового значения нужно вызвать нашу функцию .visit() внутри цепочки команд. В этом случае id будет передан с новым значением. Конечно, несколько функций .then() могут потенциально вызвать "пирамиду смерти", поэтому это решение лучше подходит для ситуаций, когда вы хотите немедленно передать одну переменную.

it('stores value in variable', () => {
let id // создание переменной
 
cy.request('/api/boards')
     .then( res => {
           id = res.body[0].id // задать значение
          cy.visit('/board/' + id) // передача свежезаданного значения
     })
 
})

Решение 2: Разделение логики на несколько тестов

Так как Cypress запускает блоки it() один за другим, вы можете разделить логику на несколько тестов и использовать "настроечную" функцию it() для присваивания значений переменным, а затем блок выполнения it() для использования этой переменной. Однако этот подход довольно ограничен, так как вам понадобится отдельный блок для каждого изменения переменной. Это также не лучший тест-дизайн, так как не каждая функция it() теперь тест. Это также может создать странный эффект домино, когда падение теста вызвано предыдущим тестом.

let id // объявление переменной
 
it('assign new value', () => {
     cy.request('/api/boards')
          .then( res => {
               id = res.body[0].id
     })
})
 
it('use variable', () => {
     cy.visit('/board/' + id)
})

Решение 3: Использование хуков

Способ получше – разделить тесты, используя хуки before() или beforeEach(). Таким образом вы более логично разделяете тест. У вас есть фаза подготовки, которая не будет частью теста, фаза и выполнения – сам тест. Другое преимущество этого подхода в том, что при падении хука у вас будет внятная информация об этом в логе ошибок.

let id // объявление переменной
 
beforeEach( () => {
     cy.request('/api/boards')
          .then( res => {
           id = res.body[0].id
     })
})
 
it('use variable', () => {
     cy.visit('/board/' + id)
})

Решение 4: Использование алиасов

Мы можем вообще пропустить создание переменной и использовать алиасы. Они не сильно отличаются от переменных, но живут непосредственно в контексте нашего теста. Преимущество этого подхода в том, что нам не нужно сразу же использовать алиас – мы можем воспользоваться им позднее в ходе теста.

it('use alias', () => {
 
     cy.request('/api/boards')
           .as('board') // создание алиаса
 
      // еще код
     // ...
 
      cy.get('@board') // использование алиаса
           .its('body')
            .then( body => {
 
                  cy.visit('/board/' + body[0].id)
 
        })
 
})

Решение 5: Использование алиасов и хуков

Алиасы на самом деле часть Mocha – фреймворка, объединенного с Cypress и использующегося для запуска тестов. Когда вы применяете команду .as(), это создает алиас в контексте Mocha, к которому можно получить доступ по ключевому слову, как показано в примере. Это будет обычная переменная, и ими можно делиться между тестами в спеке. Однако это ключевое слово нельзя использовать в стрелочных функциях () => {} – только с традиционными выражениями функций, function() {}. См. пример.

beforeEach( () => {
     cy.request('/api/boards')
          .as('board')
})
 
// использование it('use variable', () => { ... не сработает
it('use variable', function() {
      cy.visit('/board/' + this.board.body[0].id)
})

Существуют еще примеры, помогающие хранить переменные в Cypress, я привел лишь несколько из них. В более старой статье я приводил более продвинутые примеры по взаимодействию с данными из API, ее можно посмотреть здесь.

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