Мы отказались от использования Page Object потому что:
- Требует много времени на поддержку, отвлекает от написания тестовых сценариев.
- Усложняется написание тестов и анализ ошибок из-за наличия дополнительного состояния.
- Заставляет использовать единый интерфейс, что приводит к условной логике тестов.
- Замедляется выполнение тестов из-за прохода по всему интерфейсу.
Это паттерн, позволяющий вызывать или изменять методы приложения прямо в тестах Cypress, при этом скорость выполнения метода приложения выше чем у варианта с стандартным методом.
Рассмотрим Application actions в действии, на примере приложения TodoMVC.
Приложение TodoMVC — это список дел. Дела можно добавлять, помечать как выполненные и удалять. Допустим, что мы хотим проверить создание нового дела.
Если бы писали обычный авто-тест, тогда это бы выглядело так:
/// <reference types="cypress" />
context('User on home page', () => {
describe('Goes to todo-page, fills the new todo field and clicks enter', () => {
before(() => {
cy.visit('/');
cy.findByTestId('todo-input').type('123{enter}');
});
it('sees that new todo is added', () => {
cy.findByTestId('todo-count').should('contain', '1');
});
});
});
запустим:
Как видно из кода, для добавления мы нашли input, в который написали '123' и нажали enter.
C помощью Application actions мы можем избежать выборки элементов, реального заполнения поля и выполнения событий в браузере, а сразу заполнить форму.
Выполним это же действие с помощью уже готовых методов приложения. Находим в компонентах Angular'a метод добавления и вызываем его в тесте:
/// <reference types="cypress" />
context('User on home page', () => {
describe('Goes to todo-page, fills the new todo field and clicks enter', () => {
before(() => {
cy.visit('/');
cy.get('app-todo').then((elem) => {
const el = elem[0];
const win = el.ownerDocument.defaultView;
const component = win.ng.probe(el).componentInstance;
component.create('Добавлен с помощью application action');
});
cy.get('app-root').click({ force: true });
});
it('sees that new todo is added', () => {
cy.findByTestId('todo-count').should('contain', '1');
});
});
});
Запускаем тест:
С помощью application actions можно не только вызывать методы компонента, но и изменять их поведение. Такой вариант, например, может помочь в случае перехода на сторонний ресурс который тестировать не нужно. Мы просто можем вернуть необходимый ответ, либо пропустить какое-то действие метода.
Допустим что у нас есть 1000 задач. И нам необходимо очистить список.
Опишем в нашем тесте следующее:
...
before(() => {
cy.visit('/');
cy.get('app-todo').then((elem) => {
const el = elem[0];
const win = el.ownerDocument.defaultView;
const component = win.ng.probe(el).componentInstance;
for (let index = 0; index < 1000; index++) {
component.create(`Добавлен с помощью application action ${index}`);
}
component.delete = () => {
component.todoService.todos = [];
component.todoService.save();
};
component.delete();
});
});
...
Теперь метод delete
выполняет очистку всего списка дел.
Таким образом можно менять любой компонент приложения,
если это необходимо для тестирования.
Рассмотрим выполнение выше описанных действий на примере Vue.js
.
Для изменения и вызова метода, нам понадобится:
context('User on home page', () => {
describe('Goes to todo-page, fills the new todo field and clicks enter', () => {
before(() => {
cy.visit('/');
cy.window().its('app').then((component) => {
for (let index = 0; index < 1000; index++) {
component.newTodo = `Добавлен с помощью application action ${index}`;
component.addTodo();
component.todos = [];
}
});
});
});
});
Теперь пример выше описанных изменений выполним для React
.
В первую очередь, необходимо, изменить app.jsx
/app.tsx
, добавив туда следующее:
if (window.Cypress) {
window.model = model;
}
Теперь к компонентам можно обращаться также как и в Vue.js
:
context('User on home page', () => {
describe('Goes to todo-page, fills the new todo field and clicks enter', () => {
before(() => {
cy.visit('/');
cy.window().its('model').then((component) => {
for (let index = 0; index < 1000; index++) {
component.addTodo(`Добавлен с помощью application action ${index}`);
component.todos = [];
}
});
});
});
});
Это патерн, который позволяет посылать или принимать запросы с сервера, в обход пользовательского интерфейса. Когда нам нужно обойти DOM и без UI выполнить действия для подготовки тестовой среды, мы используем api actions. Так тесты работают в большей изоляции и с большей скоростью.
Например, нам необходимо реализовать вход пользователя в систему. Для этого, можно создать команду, которая будет делать запрос к API и устанавливать полученный токен пользователя:
Cypress.Commands.add('login', () => {
cy.request({
method: 'POST',
url: 'http://localhost:3000/api/users/login',
body: {
user: {
email: '[email protected]',
password: 'joe',
}
}
})
.then((resp) => {
window.localStorage.setItem('jwt', resp.body.user.token);
});
});
Асинхронность Cypress нарушает состояния между тестами, поэтому излишне изолированные проверки замедляют работу. Рекомендуется использовать множественные проверки из нескольких элементарных в одном тесте, они будут работать быстрее.
:::tip Рекомендуется
it('firstname input should be correct', () => {
cy.findByTestId('firstname-input')
.type('Johnny')
.should('have.attr', 'data-validation', 'required')
.and('have.class', 'active')
.and('have.value', 'Johnny');
});
:::
:::danger Не рекомендуется
it('firstname input should be correct', () => {
cy.findByTestId('firstname-input').type('Johnny');
});
it('firstname input should have attribute "data-validation", "required"', () => {
cy.findByTestId('firstname-input').should('have.attr', 'data-validation', 'required')
});
it('firstname input should have value "Johnny"', () => {
cy.findByTestId('firstname-input').should('have.value', 'Johnny');
});
:::
Явное ожидание устанавливается в каждой команде через параметр в миллисекундах, например:
cy.findByTestId('genedata-item-status-uploaded', { timeout: 5000 });
Неявное ожидание устанавливается через конфигурацию Cypress.
cy.findByTestId('slider-dot')
.trigger('mouseover')
.trigger('mousedown')
.trigger('mousemove', 150, 50, { force: true })
.trigger('mouseup');
cy.findByTestId(applyButton)
.click()
.then(() => {
if (Cypress.$(popover.content).is(':visible')) {
cy.get(popover.buttons.ok).click();
cy.get(popover.content).should('not.be.visible');
}
});
cy.findByTestId(price)
.invoke('text')
.as('currentPrice')
.should('be.eq', '200');
cy.get('@currentPrice').then((price) => {
cy.findByTestId('newprice-input')
.type(price_num)
.invoke('value')
.should('be.eq', price_num);
});
Необходимо создать команду в файле commands.js
для дальнейшего переиспользования:
Cypress.Commands.add('getIframeBody', (name) => {
cy.get(`iframe[name="${name}"]`)
.its('0.contentDocument')
.should('exist')
.its('body')
.should('not.be.undefined')
.then(cy.wrap)
});
В тесте, где необходимо обратиться к iframe
, вызываем созданную команду:
cy.getIframeBody('__privateStripeFrame').find('input[name="cardnumber"]').type('4242424242424242');
Для загрузки файлов на внешний ресурс используется
плагин cypress-file-upload.
Загружаемые файлы необходимо размещать в ./cypress/fixtures
.
Добавляем в ./cypress/support/commands.js
:
import 'cypress-file-upload';
Используем метод .attachFile(fileName)
в тесте:
cy.findByTestId('file-input').attachFile('file.txt');
Если вам необходимо проверить вёрстку для разных разрешений или проверить отрисовку canvas'a можно использовать визуальное тестирование.
Для простого сравнения скриншотов можно использовать cypress-image-snapshot.
Из корня проекта запустить команду:
npm install --save-dev cypress-image-snapshot
Добавляем в .cypress/plugins/index.js
:
const {
addMatchImageSnapshotPlugin,
} = require('cypress-image-snapshot/plugin');
module.exports = (on, config) => {
addMatchImageSnapshotPlugin(on, config);
};
и в ./cypress/support/commands.js
добавим:
import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command';
addMatchImageSnapshotCommand();
// Сравнить скриншот:
.matchImageSnapshot();
.matchImageSnapshot(name);
.matchImageSnapshot(options);
.matchImageSnapshot(name, options);
В тестах:
describe('Login', () => {
it('should be publicly accessible', () => {
cy.visit('/login');
// snapshot name will be the test title
cy.matchImageSnapshot();
// snapshot name will be the name passed in
cy.matchImageSnapshot('login');
// options object passed in
cy.matchImageSnapshot(options);
// match element snapshot
cy.get('#login').matchImageSnapshot();
});
});