В этой статье мы обсудим, как в Cypress выполняется код, как использовать алиасы, создавать хранилища и пользовательские команды для вызова API.
Подпишитесь на наш ТЕЛЕГРАМ КАНАЛ ПО АВТОМАТИЗАЦИИ ТЕСТИРОВАНИЯ
Содержание:
- Как выполняется код в Cypress?
- Использование команды .then()
- Использование алиасов
- Объект window
- Использование окружения
- Создание хранилища
- Создание пользовательской команды для вызова API
- Добавление типов для пользовательских команд
- Добавление типов для хранилищ
- Итоги
Итак, представим, что в начале теста вы вызываете конечную точку API. Она выдаст вам ответ, который вы хотите использовать в дальнейшем в своем тесте. Что делать дальше?
Очевидный соблазн – хранить ответ в переменной, примерно так:
beforeEach( () => { cy .log('starting test') }) it('creates a new board', () => { let res cy .request('POST', '/api/boards', { name: 'new board' }) .then( ({ body }) => { res = body }) console.log(res) })
Однако это не будет работать корректно. В console.log
вернется undefined
. Основная причина этого заключается в том, что команды Cypress являются асинхронными. Но что это означает на простом языке?
Как выполняется код в Cypress?
Интуиция подсказывает, что наш код читается сверху вниз. Это отчасти верно, но не совсем. На самом деле он выполняется блоками. В нашем тесте есть три отдельных блока кода (или функции). Наш beforeEach()
блок, it()
блок и .then()
блок. Это означает, что при выполнении кода сначала будет выполняться первый блок:
beforeEach( () => { cy .log('starting test') }) it('creates a new board', () => { let res cy .request('POST', '/api/boards', { name: 'new board' }) .then( ({ body }) => { res = body }) console.log(res) })
Затем будет выполнен it()
блок (посмотрите, что происходит с res
переменной):
beforeEach( () => { cy .log('starting test') }) it('creates a new board', () => { let res cy .request('POST', '/api/boards', { name: 'new board' }) .then( ({ body }) => { res = body }) console.log(res) })
И, наконец, .then()
блок:
beforeEach( () => { cy .log('starting test') }) it('creates a new board', () => { let res cy .request('POST', '/api/boards', { name: 'new board' }) .then( ({ body }) => { res = body }) console.log(res) })
Теперь становится ясно, почему console.log()
не возвращает нужное нам значение.
Использование команды .then()
Если мы хотим работать с тем, что возвращает наша .request()
команда, то нам нужно написать этот код внутри .then()
функции. Таким образом, если мы хотим создать новый список внутри board
, нам нужно написать код следующим образом:
it('creates a new list within a board', () => { cy .request('POST', '/api/boards', { name: 'new board' }) .then((board) => { cy .request('POST', '/api/lists', { title: 'new list', boardId: board.body.id }) }) })
Допустим, мы хотим создать задачу, которая находится внутри списка (list), расположенного внутри board
. Код будет выглядеть примерно так:
it('creates a new task on a list within a board', () => { cy .request('POST', '/api/boards', { name: 'new board' }) .then((board) => { cy .request('POST', '/api/lists', { title: 'new list', boardId: board.body.id }) .then((list) => { cy .request('POST', '/api/tasks', { title: 'new task', listId: list.body.id, boardId: board.body.id }) }) }) })
Использование алиасов
Вы уже видите, как код, приведенный выше, становится все труднее читать. Одним из способов избежать ада колбеков (callback hell) в Cypress является использование алиасов Mocha. Это позволяет хранить данные и обращаться к ним во время тестирования. По сути мы переносим все на один уровень:
it('creates a new task on a list within a board', function() { cy .request('POST', '/api/boards', { name: 'new board' }) .as('board') cy .then(() => { cy .request('POST', '/api/lists', { title: 'new list', boardId: this.board.body.id }) .as('list') }) cy .then(() => { cy .request('POST', '/api/tasks', { title: 'new task', listId: this.list.body.id, boardId: this.board.body.id }) }) })
Однако обратите внимание, что в строке 1 вместо стрелочной функции мы используем синтаксис обычной функции. Это связано с тем, что в стрелочных функциях нельзя использовать ключевое слово this
.
Объект window
Другим способом передачи данных является использование объекта window браузера. Это позволяет обменивать данные между тестами:
it('creates a board', () => { cy .request('POST', '/api/boards', { name: 'new board' }) .then((board) => { window.board = board.body; }) }) it('creates a list', () => { cy .request('POST', '/api/lists', { title: 'new list', boardId: window.board.id }) });
В целом, лучше избегать этого подхода, так как он подразумевает создание зависимостей тестов друг от друга. Если первый тест не сработает, то автоматически не сработает и второй, даже если теоретически он мог бы пройти успешно. Однако использование window контекста может оказаться полезным, когда вы пытаетесь собрать данные из всей спецификации и затем использовать их в after()
хуке.
Использование окружения
Этот подход похож на то, что часто делается в Postman. В Postman для хранения данных из запросов часто используется окружение. В Cypress можно использовать Cypress.env()
для хранения любых данных, которые возвращает сервер. Вкратце, это выглядит следующим образом:
cy .request('POST', '/api/boards', { name: 'new board' }) .then(({ body }) => { Cypress.env('board', body) })
Пока это не слишком отличается от всего остального. На самом деле для использования Cypress.env()
следует сделать еще несколько вещей:
- Создайте место для хранения в
support/index.ts
файле - Создайте пользовательскую команду для вызова API
- Добавьте типы для пользовательских команд
- Добавьте типы для хранения.
Создание хранилища
Данные можно читать или извлекать, но главное здесь то, что у вас есть единое хранилище. В этом хранилище вы определяете, где должны быть размещены ваши данные. Так, все boards
хранятся в boards
массиве, lists находятся в lists массиве и т.д. Чтобы определить хранилище для вашего приложения, следует создать beforeEach()
хук в support/index.ts
файле и определить атрибуты Cypress.env()
и их начальные значения:
beforeEach(() => { Cypress.env('boards', []); Cypress.env('lists', []); });
Создание пользовательской команды для вызова API
Далее необходимо добавить запрос в качестве пользовательской команды:
Cypress.Commands.add('addBoardApi', (name) => { cy .request('POST', '/api/boards', { name }) .then(({ body }) => { Cypress.env('boards').push(body) }) })
Теперь при каждом вызове пользовательской команды ответ на запрос будет сохраняться в boards
массиве. Когда вам понадобится обратиться к этому хранилищу, вы сможете просто использовать его в своем коде следующим образом:
Cypress.env('boards')[0].id
Это позволит эффективно получить доступ к идентификатору форума. Однако это не полностью решает проблему ада колбеков, поскольку нельзя получить доступ к boards
id только таким образом:
it('creates a list', () => { cy .addBoardApi('new board') cy .request('POST', '/api/lists', { title: 'new list', boardId: Cypress.env('boards')[0].id }) });
Это приведет к ошибке, поскольку наш Cypress.env('boards')[0].id
по-прежнему будет undefined
. Но использование пользовательской команды аналогично использованию .then()
функции. Поэтому мы можем написать пользовательскую команду и для второго запроса. Поскольку у нас теперь есть хранилище, мы можем использовать его и искать в нашем хранилище нужный uuid:
Cypress.Commands.add('addListApi', ({ title, boardIndex = 0 }) => { cy .request('POST', '/api/lists', { boardId: Cypress.env('boards')[boardIndex].id, title, }).then(({ body }) => { Cypress.env('lists').push(body); }); });
Таким образом, можно ссылаться на board
, используя индекс. Мы можем создать два
в нашем тесте и добавить список непосредственно внутри второго boards
board
.
it('creates a list', () => { cy .addBoardApi('first board') .addBoardApi('second board') .addListApi({ title: 'new list', boardIndex: 1}) });
Наша пользовательская .addListApi()
команда по умолчанию устанавливает boardIndex
опцию в 0
, нам даже не нужно добавлять ее вручную, если мы создаем только один board
. По сравнению со всеми .then()
функциями, это гораздо проще для восприятия.
Добавление типов для пользовательских команд
Возможно, вы уже заметили, что в большинстве тестов используется TypeScript. Вы можете ознакомиться с документацией по TypeScript, чтобы начать работу. Одним из приятных преимуществ использования TypeScript является то, что вы можете легко добавить определение типа команды. Это позволяет использовать автозаполнение Intellisense и помогает всем, кто будет использовать ваши пользовательские команды в будущем. Чтобы добавить их, нужно создать commands.d.ts
файл:
declare namespace Cypress { interface Chainable { /** * creates a new board via API */ addBoardApi(name: string): Chainable<Element> /** * Adds new list via API */ addListApi(options: { title: string; boardIndex?: string; }): Chainable<Element> } }
Добавление типов для хранилищ
В качестве завершающего штриха можно написать код, который позволяет добавлять собственные ключи окружения, которые будут появляться всякий раз, когда вы ссылаетесь на один из элементов хранилища в Cypress.env()
. Этот код, по сути, расширяет типы для функции Cypress.env()
:
export { }; declare global { namespace Cypress { export interface Cypress { /** * Returns all environment variables set with CYPRESS_ prefix or in "env" object in "cypress.json" * * @see https://on.cypress.io/env */ env(): Partial<EnvKeys>; /** * Returns specific environment variable or undefined * @see https://on.cypress.io/env * @example * // cypress.json * { "env": { "foo": "bar" } } * Cypress.env("foo") // => bar */ env<T extends keyof EnvKeys>(key: T): EnvKeys[T]; /** * Set value for a variable. * Any value you change will be permanently changed for the remainder of your tests. * @see https://on.cypress.io/env * @example * Cypress.env("host", "http://server.dev.local") */ env<T extends keyof EnvKeys>(key: T, value: EnvKeys[T]): void; /** * Set values for multiple variables at once. Values are merged with existing values. * @see https://on.cypress.io/env * @example * Cypress.env({ host: "http://server.dev.local", foo: "foo" }) */ env(object: Partial<EnvKeys>): void; } } } interface EnvKeys { 'boards': Array<{ created: string; id: number; name: string; starred: boolean; user: number; }>; 'lists': Array<{ boardId: number title: string id: number created: string }>; }
Итоги
Этот паттерн фактически создает библиотеку тестирования, в которой все конечные точки API имеют пользовательскую команду, а ответы хранятся в Cypress.env()
хранилище. Состояние теста описывается в beforeEach()
хуке, а все остальное – в it()
блоке. Это помогает получить четкое представление о том, что происходит перед тестом, а также внутри теста. В итоге получается код, который выглядит примерно так:
beforeEach(() => { cy .addBoardApi('hello board') .addListApi({ title: 'hello list' }); }); it('create a task', () => { cy .visit(`/board/${Cypress.env('boards')[0].id}`); cy .get('.List_addTask') .click(); cy .get('.ListContainer .TextArea') .should('be.visible') .type('new task{enter}'); cy .get('.Task') .should('be.visible'); });
Перевод статьи «Working with API response data in Cypress».