Кипарис: аварийный пост
Практический обзорсквозного тестирования
Cypress упрощает тестирование вашего приложения как реального пользователя. Поработав с ним несколько недель, я хотел бы обобщить свои знания, чтобы вы (и я в будущем) были готовы к наиболее распространенным сценариям e2e в кратчайшие сроки.
Мы рассмотрим стандартные этапы E2E-тестирования: посещение, запрос, взаимодействие и утверждения, а также некоторые дополнительные сведения, которые я считаю полезными.
Чтобы упростить процесс, я создал следующее репозиторий, чтобы вы могли следить за этим постом, играя с пользовательским интерфейсом Cypress:
git clone https://github.com/aleixsuau/cypress-seed
cd cypress-seed && npm install
npx cypress open --e2e --browser chrome
- Запустите тесты cypress-crash-post-examples, нажав на него.
- Поместите окно пользовательского интерфейса кипариса параллельно этому посту и откройте консоль браузера. Вы можете щелкнуть любую строку тестов, чтобы зарегистрировать тест в консоли.
Вот так!
Нет настройки
После установки Cypress (npm install cypress --save-dev
) вам просто нужно запустить npx cypress open
, чтобы открыть пользовательский интерфейс Cypress в окне браузера и начать тестирование. Вы также можете запускать тесты без головы с помощью npx cypress run
.
Одна из самых крутых вещей в Cypress заключается в том, что вам просто нужно посетить свое приложение и начать его тестирование. Это действительно взорвало мой мозг по сравнению с модульными тестами Jasmine/Jest/Spectator, где мы должны подключить все зависимости… Для меня имеет смысл использовать Cypress для всего, кроме модульного тестирования сервисов и кросс-общих компонентов (кстати, Cypress только что выпустила и компонентное тестирование).
Посещение
Обычно тест E2E начинается с посещения нашего работающего приложения.
cy.visit(“http://localhost:4200");
Вы можете изменить baseUrl в файле cypress.config.ts, чтобы он указывал на любой URL (даже рабочий). В нашем примере репозитория я установил базовый URL-адрес своего сайта (baseUrl: 'https://aleixsuau.dev'
), поэтому нам просто нужно cy.visit('');
, чтобы посетить его.
Запрос
Как только мы окажемся на сайте, нам нужно получить целевые элементы. Запросы просты, как пирог, и работают с первой попытки… большую часть времени!
context('Querying', () => { it('should query', () => { // Query by CSS selector cy.get('#profile'); cy.get('[data-test=profileGreeting]'); // Query by text content cy.contains("I'm Aleix, frontend developer"); // Query by index cy.get('[data-test="profileLink"]').first(); // LinkedIn logo cy.get('[data-test="profileLink"]').eq(2); // Github logo // Query + filter cy.get('a').filter('[data-test="profileLink"]').should('have.length', 5); // Query children cy.get('[data-test="contactForm"]').find('input'); cy.get('[data-test="contactForm"]').contains('Email'); // Query multiple elements and iterate through all the results // ($el and $list are JQuery elements (read more below)) cy.get('ul>li').each(($el, index, $list) => {...}) cy.get('[data-test="contactForm"]').within(($form) => { // The following inputs are form’s children cy.get('[data-test="emailInput"]'); cy.get('[data-test="messageInput"]'); }); }); });
Нажмите на любую тестовую строку, чтобы увидеть элемент в предварительном просмотре и журнал тестирования в консоли:
Взаимодействие
После того, как мы нашли целевой элемент, пришло время выполнить над ним некоторые действия.
context('Interacting', () => { it('should trigger events on DOM elements', () => { // Cypress provides some commands like: cy.get('[data-test="menuToggle"]').dblclick(); cy.get('[data-test="menuToggle"]').click(); cy.get('[data-test="menu"]').rightclick(); cy.get('[data-test="emailInput"]').type('[email protected]'); cy.get('[data-test="emailInput"]').clear(); // We can also trigger events cy.get('[data-test="menu"]').trigger('mousedown'); // In order to trigger events in non-actionable elements (non-visible, disabled, animated…) we need to force it cy.get('[data-test=submitButton]').click({ force: true }); }); });
Утверждая
Утверждение довольно близко к другим тестовым библиотекам и к простому английскому языку:
context('Asserting', () => { it('should assert things', () => { // Assert length cy.get('[data-test="menuItem"]').should('have.length', 4); // Assert class cy.get('[data-test="emailInput"]').should('not.have.class', 'disabled'); // Assert value cy.get('[data-test="messageInput"]').should('have.value', ''); // Assert text cy.get('[data-test="menuItem"]').eq(3).should('include.text', 'CONTACT'); // Assert existence cy.get('[data-test="idontExist"]').should('not.exist'); // Assert visibility cy.get('[data-test="menu"]').should('not.be.visible'); // Assert state cy.get('[data-test=submitButton]').should('be.disabled'); // Assert CSS cy.get('[data-test="menu"]').should('have.css', 'visibility', 'hidden'); // Assert multiple concerns cy.get('[data-test="menuItem"]') .first() .should('have.class', 'active') .and('have.attr', 'href') .and('include', '#home'); cy.get('[data-test="menuItem"].active') .contains('hi', { matchCase: false }) .should('exist'); }); });
Советы
Только
Запустите только одну спецификацию или комплект с только:
describe.only(‘Course‘, () => { … }); it.only('should complete a practice ', () => { … });
Атрибуты E2E
Рекомендуется использовать атрибуты data-test вместо классов CSS или идентификаторов для целевых элементов, атрибуты data-test менее подвержены изменениям.
<div data-test=”titleContainer”></div>
Псевдоним
Псевдонимы позволяют разделять цели между тестами, помогая свести к минимуму дублирование:
context('Aliasing', () => { beforeEach(() => { cy.get('[data-test="menuToggle"]').as('menuToggle'); cy.fixture('/posts.json').as('postsData'); }); it('should work with alias', () => { cy.get('@menuToggle').click(); cy.get('@postsData').should('have.length', 7); }); });
Псевдонимы сбрасываются перед каждым тестом, поэтому их необходимо установить в блоках beforeEach, чтобы они работали между несколькими тестами.
HTTP
HTTP-запросы можно перехватывать, имитировать, изменять, использовать псевдонимы, ожидать и утверждать, чтобы можно было настроить и протестировать все отношения с серверной частью.
context('HTTP', () => { it('should assert API responses', () => { // Alias an HTTP request cy.intercept('GET', '/config.json').as('getConfig'); // We need to visit the site in order the requests are triggered cy.visit(''); // We need to wait the request in order to assert cy.wait('@getConfig').its('response.body').should('have.property', 'menu'); // We can also trigger API requests manually so we don't need to wait cy.request('GET', 'https://portfolio-aleix.firebaseio.com/config.json').then( (response) => expect(response.body).to.have.property('menu') ); }); it('should mock API responses', () => { const mockedAPIResponse = { statusCode: 404, body: '404 Not Found!', headers: { 'x-not-found': 'true', }, }; cy.intercept('GET', '/works.json', mockedAPIResponse).as('getWorks'); cy.visit(''); cy.wait('@getWorks').its('response.statusCode').should('equal', 404); }); it('should modify API responses', () => { cy.intercept('GET', '/works.json', (request) => { request.reply((response) => { response.body.length = 0; return response; }); }).as('getWorks'); cy.visit(''); cy.wait('@getWorks').its('response.body').should('have.length', 0); }); });
Ожидание/повторная попытка
Иногда запросы терпят неудачу… в основном из-за асинхронной природы внешнего интерфейса. Может быть, вы хотите запросить элемент, который появляется только после ответа API или после клика… Нет никаких гарантий, когда это произойдет, поэтому нам нужно дождаться этого.
Чтобы упростить ожидание, Cypress повторяет запросы до тех пор, пока утверждение не станет истинным или пока не истечет время ожидания. DefaultCommandTimeout составляет 4 секунды, но его можно настроить для разных областей действия:
// Cypress / Project (`cypress.config.ts` or the old `cypress.json`) { 'defaultCommandTimeout': 5000 } // Suite describe('suite example', { defaultCommandTimeout: 5000 }, () => { … }); // Spec it('spec example', { defaultCommandTimeout: 5000 }, () => {}); // Command cy.get('.example', { timeout: 10000 }); cy.location('pathname', { timeout: 10000 }); // Waiting rougly cy.wait(10000); // wait 10 second and continue // Waiting for HTTP requests (more on this in the next section) it('should wait for HTTP alias', () => { cy.intercept(GET, '/users').as('getUsers'); cy.intercept(GET, '/users').as('getConfig'); cy.wait(['@getUsers', '@getConfig']); // Users and config are ready here }); // Wait for a ‘should’ callback cy.get('.list').should( (items) => { // retries until this callback passes or times out });
Внимание! Запросы HTTP могут быть сложными. Cypress ожидает HTTP-запросов по порядку, это означает, что если ваше приложение отправляет 3 запроса @getUsers, а вас интересует только последний, вам нужно cy.wait('@getUsers' ) 3 раза. Кроме того, если один из этих запросов не будет выполнен, тест завершится ошибкой. Это позволяет легко сойти с ума, считая запросы…
К счастью, есть отличная библиотека, позволяющая условно придерживаться последнего запроса и многое другое: https://github.com/bahmutov/cypress-wait-if-happens
cy.waitIfHappens({ alias: '@getUsers', timeout: 100, lastCall: true, yieldResponseBody: true, });
Cypress Chainable Objects, JQuery и Wrap:
Запросы Cypress возвращают объекты с цепочкой (Chainable‹JQuery‹E››), которые предлагают множество команд, которые мы можем связать.
Мы можем получить доступ к объекту JQuery из Cypress Chainable Object и преобразовать его обратно в CCO с помощью обернуть:
context('Cypress Chainable Objects', () => { it('should chain objects', () => { cy.get('[data-test="profileLink"]').eq(2).focus(); }); it('should give access to the JQuery object and wrap it back to Cypress chainable object', () => { cy.get('[data-test="menu"]').then(($menu) => { // $menu is a JQuery object const classList = $menu.attr('class'); if (!classList.includes('mat-drawer-opened')) { cy.get('[data-test="menuToggle"]').click(); } // cy.wrap($menu) converts it back into a Cypress chainable object cy.wrap($menu).should('have.class', 'mat-drawer-opened'); }); }); });
Команды
Мы можем сделать задачи доступными глобально с помощью команд cypress/support/commands.ts. Я считаю, что это имеет смысл в основном для задач, связанных с приложением, таких как вход в систему или очистка.
Cypress.Commands.add('login', () => { cy.get('[data-cy=email-input]').type('[email protected]'); cy.get('[data-cy=password-input]').type('testpassword'); cy.get('[type=submit]').click(); }); // Now you can use it in any test cy.login();
Объекты страницы
Объекты страницы — отличная абстракция, которая делает тест более декларативным и содержательным, уменьшая при этом дублирование кода.
Например, взгляните на следующую спецификацию:
it('should complete a practice', () => { const answers = [0, 1, 2, 4, 5]; cy.contains('.mat-expanded button', 'Check your knowledge').click({ force: true }); answers.forEach((answerIndex) => { cy.get('.activity-select-right__item').eq(answerIndex).click(); }); cy.get('.activity-select-right__submit').click(); });
Теперь взгляните на это:
it('should complete a practice', () => { CourseDetailPageObject.takePractice([0, 1, 2, 4, 5]); });
Много ясного, не так ли? Инкапсуляция бизнес-логики в осмысленные методы класса/страницы превращает тесты в отличный инструмент документирования функциональности кода.
Конфигурации
Мы можем глобально настроить такие параметры, как время ожидания, повторные попытки или размер области просмотра в cypress.config.ts (старый cypress.json). Кроме того, как и в примере с тайм-аутом ожидания, некоторые из этих конфигураций также могут быть перезаписаны в наборе, спецификации и области действия команды путем передачи объекта конфигурации или вызова Cypress.config(configOptions). . "Читать далее"
Чтобы просмотреть дополнительные примеры тестов, нажмите site.e2e.cy.js в списке тестов пользовательского интерфейса Cypress.
И на этом все, надеюсь, вам было полезно. Спасибо за прочтение :)
Рекомендации и благодарности
- Документы и курсы Cypress великолепны https://learn.cypress.io
- Глеб Бахмутов, автор книги кипарис-подожди-если-будет, также опубликовал много ресурсов о кипарисе на https://cypress.tips.
- Амбассадор Cypress Filip Hric, который указал мне на решение waitIfHappnes, имеет отличные ресурсы на своей странице: https://filiphric.com
- https://testing-angular.com