Кипарис: аварийный пост

Практический обзорсквозного тестирования

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

Мы рассмотрим стандартные этапы E2E-тестирования: посещение, запрос, взаимодействие и утверждения, а также некоторые дополнительные сведения, которые я считаю полезными.

Чтобы упростить процесс, я создал следующее репозиторий, чтобы вы могли следить за этим постом, играя с пользовательским интерфейсом Cypress:

  1. git clone https://github.com/aleixsuau/cypress-seed
  2. cd cypress-seed && npm install
  3. npx cypress open --e2e --browser chrome
  4. Запустите тесты cypress-crash-post-examples, нажав на него.
  5. Поместите окно пользовательского интерфейса кипариса параллельно этому посту и откройте консоль браузера. Вы можете щелкнуть любую строку тестов, чтобы зарегистрировать тест в консоли.

Вот так!

Нет настройки

После установки 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.

И на этом все, надеюсь, вам было полезно. Спасибо за прочтение :)

Рекомендации и благодарности