Як провести сучасне e2e-тестування з Cypress JS

18 хв. читання

Навіщо тестувати?

Одвічне питання для розробників ПЗ. Мабуть, ви вже не раз чули, що тестування робить ваш застосунок надійнішим, а отже покращується досвід користування ним. А ви як розробник можете бути більш впевненим у фічах, які випускаєте в продакшен.

Авторові матеріалу довелося працювати в команді, яка постійно супроводжувала код різноманітними тестами. Це дозволило щомісяця випускати нові фічі та вдосконалення і бути впевненими, що вони працюють. Автоматизоване тестування особливо корисне для великих команд, де йде активний процес розробки та релізів. Саме завдяки налагодженому процесу тестування можна впевнено контролювати код, який потрапляє в кінцевий продукт.

Типи тестів

Тести бувають різні. У галузі розробки ПЗ виділяють статичні тести, юніт-тести, інтеграційні тести, тести end-to-end, A/B-тести, стрес-тести, смоук-тести, тести прийняття тощо.

Початківця такий перелік може збити з пантелику. Не легше і від того, що розробники часто називають кожен вид тесту по-своєму.

Тож проллємо світло на тему тестування і поговоримо про статичні, модульні та інтеграційні тести детальніше. А тоді перейдемо до теми матеріалу, тестів end-to-end.

Статичне тестування

Одразу почнемо з прикладу. Наведемо фрагмент коду калькулятора:

 const add = (...operands) => {
  let sum = 0;

  operands.forEach((operand) => {
    sum = sum + operand;
  });

  return sums;
};

Тут можна легко побачити помилку: функція повинна повертати sum, а не sums. Так, цей приклад дуже простий, однак на практиці не завжди все так очевидно. Поширеними помилками, які перевіряються статичним тестуванням, є доступ до властивості об'єкта, якої немає, коректність змінних тощо. Такі помилки можна знайти під час написання коду за допомогою лінтерів (наприклад, ESLint), TypeScript або Flow.

Щойно ми познайомились зі статичними тестами, перейдемо до модульних.

Модульне тестування

Приклад модульного тесту:

import { add } from './add.js';

test('Adding function to return correct sum of numbers', () => {
  expect(add(1, 1)).toEqual(2);
  expect(add(2, 3)).toEqual(5);
});

Ви, мабуть, помітили, що код дуже нагадує реальну мову. Вам навіть не потрібно знати синтаксис.

У тесті відбувається перевірка функції додавання. Ми викликаємо функцію з певними аргументами і очікуємо, що вона поверне коректне значення суми.

Ви запитаєте: чому ми не тестуємо також функції ділення, віднімання, адже вони також є частиною застосунку? Ми навмисне цього не робили. Вся суть модульного тестування в тому, аби перевірити окрему одиницю програми (клас, модуль, або навіть об'єкт) в ізоляції.

Але якщо треба перевірити, як різні модулі взаємодіють в програмі? Тоді переходьте до розділу про інтеграційне тестування.

Інтеграційне тестування

Такі тести потрібні, аби перевірити, як взаємодіють різні частини системи. На відміну від модульних, інтеграційні тести враховують всі аспекти роботи застосунку. Інтеграційні тести — це баланс між часом, витраченим на імплементацію, та обсягом застосунку, який вони охоплюють.

Розглянемо один з таких тестів:

describe('Add page of the app', () => {
  it('', () => {
    // перш ніж надсилати запити, замокаємо його
    cy.server();
    cy.route({
      method: 'POST',
      url: '**/add/?operand1=23&operand2=10',
      response: {
        sum: '33',
      },
    }).as('getSumOfNumbersAPI');

    cy.visit('/add-page');

    // # Визначаємо значення першого операнда
    cy.findByLabelText('Number 1').type('23');

    // # Визначаємо значення другого операнда
    cy.findByLabelText('Number 2').type('10');

    // # Клікаємо на кнопку
    cy.findByText('Calculate').click();

    // # Очікуємо відповіді від API
    cy.wait('@getSumOfNumbersAPI').should((xhr) => {
      expect(xhr.status).to.equal(200);
      const { sum } = xhr.response.body;

      // * Перевіряємо, чи результат коректний
      cy.findByLabelText('Sum of number').should('have.value', sum);
    });
  });
});

Вище ми припускаємо, що в нас є API, який обчислює суму двох чисел і повертає її клієнту. Нам не потрібні деталі його реалізації, лише фінальний результат, який бачить кінцевий користувач.

Ми знаходимо поля для вводу чисел, заповнюємо їх значеннями та натискаємо на кнопку «Calculate». Потім мокаємо API-запити. Коли запит дійсно надсилається, фреймворк перехоплює його і повертає замоканий результат. В кінці ми просто звіряємо очікуваний результат з тим, що отримали.

Тестування end-to-end

На відміну від усіх перелічених у статті видів, тестування end-to-end передбачає запуск всього застосунку.

Тоді ви з високою точністю можете протестувати те, як поводиться з вашим застосунком справжній користувач. Однак не все так ідеально. Тести end-to-end потребують багато ресурсів для налаштування, а тому достатньо повільні.

Розглянемо один з таких тестів:

describe('Add page of the app', () => {
  it('', () => {
    cy.visit('/add-page');

    // # Визначаємо значення першого операнда
    cy.findByLabelText('Number 1').type('23');

    // # Визначаємо значення другого операнда
    cy.findByLabelText('Number 2').type('10');

    // # Натискаємо на кнопку для розрахування
    cy.findByText('Calculate').click();

    // # Чекаємо, поки API поверне результат
    cy.wait('1000');

    // Звіряємо його з правильним результатом
    cy.findByLabelText('Sum of number').should('have.value', '33');
  });
});

Тут у вас може виникнути логічне питання: скільки саме таких тестів треба написати?

На це питання, звичайно ж, немає однозначної відповіді. Також немає точного співвідношення кількості різних видів тестів для певного проєкту. Тож шукайте той баланс між тестами, який буде оптимальним за витраченими силами та охопленням для вашого застосунку.

(Чим краще ваші тести імітують реальну взаємодію із застосунком, тим вони надійніші.)

Почнемо тестувати

Зараз є багато інструментів для e2e-тестування застосунків. В матеріалі статті ми розглянемо один з них — Cypress, який допомагає швидко створювати прості та надійні тести. З коробки він має набір функцій для тестування.

Аби додати Cypress до свого проєкту, виконайте:

npm install cypress --save-dev

Після встановлення пакету додайте такий скрипт в package.json, аби з легкістю потім отримати доступ до дешборду Cypress:

  "scripts": {
    "cypress": "cypress open"
    ...
  },

Коли ви вперше запустите Cypress (командою вище), деякий час будуть встановлюватись додаткові залежності. Далі ви побачите дешборд з інформацією про всі тести, проведені в застосунку. Там також будуть приклади тестів, які ви можете видалити.

Як провести сучасне e2e-тестування з Cypress JS

Мабуть, ви помітили, що при запуску команди, в кореневій директорії проєкту створюється тека cypress.

|cypress
-|fixtures
-|integrations
-|plugins
-|support

Нас цікавитиме тека integrations, де ми й будемо писати наші тести. Перш ніж перейти до написання тестів, встановимо ще одну бібліотеку, яка допоможе нам створювати тести, максимально наближені до взаємодії користувача із застосунком:

npm install @testing-library/cypress --save-dev

Тепер все готово, аби почати писати тести end-to-end.

Щоб не витрачати час на створення власного проєкту, скористаємось готовим застосунком від Mattermost. Для налаштування середовища розробки зверніться до документації. Якщо виникнуть якісь проблеми, вам допоможе чат спільноти.

Mattermost активно використовує Cypress для автоматизованого тестування. Детальніше про те, як команда організовує цей процес, за посиланням.

Пишемо перший тест

Тепер, коли ми все підготували до написання тестів, відкриймо mattermost-webapp у будь-якому редакторі коду. Ви побачите, що кодова база проєкту дійсно велика, однак з часом ви почнете розуміти, що до чого.

Почнемо тестування зі сторінки авторизації.

Як провести сучасне e2e-тестування з Cypress JS

Тепер переходимо до директорії e2e/cypress. Там є тека integrations з декількома теками всередині. В кожній теці є тести, згруповані за фічею чи компонентом, який тестується. В теці integrations знайдемо файл login_spec.js.

describe('Login page', () => {
  let config;
  let testUser;

  before(() => {
    // Вимикаємо інші способи автентифікації
    const newSettings = {
      Office365Settings: { Enable: false },
      LdapSettings: { Enable: false },
    };
    cy.apiUpdateConfig(newSettings).then((data) => {
      ({ config } = data);
    });

    // # Створюємо нову команду та користувачів
    cy.apiInitSetup().then(({ user }) => {
      testUser = user;
			cy.apiLogout();
      cy.visit('/login');
    });
  });

  it('should render', () => {
    // *Перевіряємо, чи завантажилась секція сторінки з формою для входу
    cy.get('#login_section').should('be.visible');

    // * Перевіряємо заголовок
    cy.title().should('include', config.TeamSettings.SiteName);
  });

  it('should match elements, body', () => {
    // * Перевіряємо елементи в body
    cy.get('#login_section').should('be.visible');
    cy.get('#site_name').should('contain', config.TeamSettings.SiteName);
    cy.get('#site_description').should('contain', config.TeamSettings.CustomDescriptionText);
    cy.get('#loginId')
      .should('be.visible')
      .and(($loginTextbox) => {
        const placeholder = $loginTextbox[0].placeholder;
        expect(placeholder).to.match(/Email/);
        expect(placeholder).to.match(/Username/);
      });
    cy.get('#loginPassword')
      .should('be.visible')
      .and('have.attr', 'placeholder', 'Password');
    cy.get('#loginButton').should('be.visible').and('contain', 'Sign in');
    cy.get('#login_forgot').should('contain', 'I forgot my password');
  });

  it('should match elements, footer', () => {
    // * Перевіряємо елементи в footer
    cy.get('#footer_section').should('be.visible');
    cy.get('#company_name').should('contain', 'Mattermost');
    cy.get('#copyright')
      .should('contain', '(c) 2015-')
      .and('contain', 'Mattermost, Inc.');
    cy.get('#about_link')
      .should('contain', 'About')
      .and('have.attr', 'href', config.SupportSettings.AboutLink);
    cy.get('#privacy_link')
      .should('contain', 'Privacy')
      .and('have.attr', 'href', config.SupportSettings.PrivacyPolicyLink);
    cy.get('#terms_link')
      .should('contain', 'Terms')
      .and('have.attr', 'href', config.SupportSettings.TermsOfServiceLink);
    cy.get('#help_link')
		.should('contain', 'Help')
      .and('have.attr', 'href', config.SupportSettings.HelpLink);
  });

  it('should login then logout by test user', () => {
    // # Вводимо ім'я користувача або електронну пошту в поле вводу
    cy.get('#loginId').should('be.visible').type(testUser.username);

    // # Вводимо пароль у відповідне поле
    cy.get('#loginPassword').should('be.visible').type(testUser.password);

    // # Натискаємо кнопку "Sign in"
    cy.get('#loginButton').should('be.visible').click();

    // * Перевіряємо, що вміст кнопки змінився після натискання 
    cy.get('#loadingSpinner')
      .should('be.visible')
      .and('contain', 'Signing in...');

// * Перевіряємо, що автентифікація успішна і користувач перенаправляється на головну сторінку
    cy.get('#channel_view').should('be.visible');

    // # Натискаємо на кнопку виходу з облікового запису на головному меню 
    cy.get('#sidebarHeaderDropdownButton').click();
    cy.get('#logout').should('be.visible').click();

    // * Перевіряємо, аби вихід з облікового запису був успішним і користувача було перенаправлено на сторінку автентифікації
    cy.get('#login_section').should('be.visible');
    cy.location('pathname').should('contain', '/login');
  });
});

Ви, мабуть, одразу помітили, що в тесті використовується багато ідентифікаторів елементів. Такий підхід був в команді Mattermost, коли вони тільки-тільки починали займатись e2e-тестуванням. Гарні практики написання тестів з'явились в команді пізніше. Оскільки тест вище чомусь ще не відрефакторили, у нас з'явилась чудова можливість це зробити.

Кожен тест починається з блоку describe, який описує фічу, яку ми тестуємо. В ньому розташовані it-блоки, які є окремими тестами. Ви також могли помітити блок before, який запускається перед усіма тестами. Зараз нам потрібно знати про цей тест лише те, що він створює нового тестового користувача та перенаправляє на сторінку автентифікації.

Очистимо все, що є у блоці describe. Залишимо тільки блок before та блок it із заголовком should render all elements of the page.

describe('Login page', () => {
  let config;
  let testUser;

  before(() => {
    // Вимикаємо інші опції автентифікації
    const newSettings = {
      Office365Settings: {Enable: false},
      LdapSettings: {Enable: false},
    };
    cy.apiUpdateConfig(newSettings).then((data) => {
      ({config} = data);
    });

    // # Створюємо нову команду і користувачів
    cy.apiInitSetup().then(({user}) => {
      testUser = user;

      cy.apiLogout();
      cy.visit('/login');
    });
  });

  it('should render all elements of the page', () => {
    // будемо писати тут
  });
});

Наш перший тест має перевірити, чи рендеряться потрібні елементи на сторінці. Спочатку перевіряємо URL, що має відповідати URL сторінки автентифікації. Для отримання URL сторінки у Cypress є функція cy.url.

// * Перевіряємо URL сторінки автентифікації
cy.url().should('include', '/login');

Ви могли помітити функцію should у попередньому тесті. Це функція-припущення, яка перевіряє, чи збігається переданий результат з очікуваним. В нашому тесті функція перевіряє відповідність URL з /login. Якщо це припущення хибне, то тест також не проходить.

У схожий спосіб ми можемо перевірити заголовок документу, але вже з функцією cy.title. У блоці before ми отримуємо доступ до об'єкта-конфігурації, в якому є заголовок сторінки.

// * Перевіряємо, чи заголовок документу правильний
cy.title().should('include', config.TeamSettings.SiteName);

Тепер нам треба перевірити, чи є на сторінці потрібні елементи для автентифікації, тобто поля вводу імені користувача/паролю та кнопка відправлення даних.

// * Перевіряємо наявність полів для вводу email/username 
cy.findByPlaceholderText('Email or Username').should('exist').and('be.visible');

// * Перевіряємо наявність поля для вводу паролю
cy.findByPlaceholderText('Password').should('exist').and('be.visible');

// * Перевіримо наявність кнопки "submit"
cy.findByText('Sign in').should('exist').and('be.visible');

Ми знаходимо поля вводу за відповідними заголовками, а кнопку «Submit» за її вмістом. Фактично поводимось із застосунком так само, як і кінцевий користувач.

Аби об'єднати декілька припущень, ми можемо використати функцію and. Наприклад, вище ми перевіряємо, чи є елемент на сторінці та чи бачить його користувач.

Тепер спробуймо знайти на сторінці посилання для відновлення паролю. Перевіримо, чи посилається воно на коректну URL-адресу.

// * Перевіряємо наявність посилання для відновлення паролю
cy.findByText('I forgot my password.').should('exist').and('be.visible').
  parent().should('have.attr', 'href', '/reset_password');

Мабуть, ви помітили, що ми використали функцію parent. Аби зрозуміти її призначення, розглянемо таку HTML-структуру:

<a href="/reset_password">
  <span>I forgot my password.</span>
</a>

Функція findByText('I forgot my password.') поверне елемент span, але насправді нам потрібен елемент a. Гарною практикою вважається використовувати текст напряму всередині посилання. Але іноді доводиться йти на поступки, наприклад, якщо в застосунку є підтримка інтернаціоналізації. Функція parent() повертає батьківський компонент знайденого елемента (в нашому випадку це елемент <a>).

Повний тест буде таким:

it('should render all elements of the page', () => {
    // * Перевіряємо, що поточний URL правильний
    cy.url().should('include', '/login');

    // * Перевіряємо, аби заголовок документа був правильний
    cy.title().should('include', config.TeamSettings.SiteName);

    // * Перевіряємо, аби поля email/username були в документі
    cy.findByPlaceholderText('Email or Username').should('exist').and('be.visible');

    // * Перевіряємо наявність поля для вводу паролю
    cy.findByPlaceholderText('Password').should('exist').and('be.visible');

    // * Перевіряємо наявність кнопки sign in
    cy.findByText('Sign in').should('exist').and('be.visible');
		
	    // * Перевіряємо наявність посилання для відновлення паролю
    cy.findByText('I forgot my password.').should('exist').and('be.visible').
      parent().should('have.attr', 'href', '/reset_password');
    });

Аби запустити тест, виконайте команду open cypress в теці e2e/. Одразу як відкриється дешборд, знайдіть назву файлу з тестами та двічі клікніть на нього:

cd e2e/

npm run cypress:open

Ми впорались з написанням e2e-тесту на Cypress! Але ще точно є, що перевіряти. Ви можете самостійно продумати сценарії тестування, і написати тести для них.

Як провести сучасне e2e-тестування з Cypress JS

Закріпимо навички

Перевіримо ще один тестовий сценарій: користувач застосунку вводить хибні дані в форму логіну.

Почнемо зі створення нового тестового блоку. Назвемо його Should show error with invalid email/username and password. Створюємо випадкове ім'я користувача та пароль.

it('Should show error with invalid email/username and password', () => {
  const invalidEmail = `${Date.now()}-user`;
  const invalidPassword = `${Date.now()}-password`;
});

У нас є інформація про справжні дані користувача з блоку before. Перевіримо, чи не збігаються згенеровані дані зі справжніми:

it('Should show error with invalid email/username and password', () => {
  const invalidEmail = `${Date.now()}-user`;
  const invalidPassword = `${Date.now()}-password`;

+ // # Перевіряємо на збіг ім'я користувача
+ expect(invalidEmail).to.not.equal(testUser.username);

+ // # Перевіряємо на збіг пароль
+ expect(invalidPassword).to.not.equal(testUser.password);
});

Функція-припущення expect походить з бібліотеки ChaiJS, яка вбудована в Cypress.

Вводимо некоректні ім'я користувача та пароль у відповідні поля. В Cypress для цього є команда type. А функцією click імітуємо клік на кнопку на кнопку.

it('Should show error with invalid email/username and password', () => {
  const invalidEmail = `${Date.now()}-user`;
  const invalidPassword = `${Date.now()}-password`;

  // # Перевіряємо, чи збігаються згенероване ім'я користувача зі справжнім
  expect(invalidEmail).to.not.equal(testUser.username);

  // # Перевіряємо чи збігається згенерований пароль зі справжнім
  expect(invalidPassword).to.not.equal(testUser.password);

+ // # Вводимо некоректне ім'я користувача 
+ cy.findByPlaceholderText('Email or Username').clear().type(invalidEmail);

+ // # Вводимо некоректний пароль 
+ cy.findByPlaceholderText('Password').clear().type(invalidPassword);

+ // # Клікаємо на кнопку входу
+ cy.findByText('Sign in').click();
});

Залишилось лише перевірити правильність повідомлення про помилку.

it('Should show error with invalid email/username and password', () => {
  const invalidEmail = `${Date.now()}-user`;
  const invalidPassword = `${Date.now()}-password`;

  // # Перевіряємо, чи збігаються згенероване ім'я користувача зі справжнім
  expect(invalidEmail).to.not.equal(testUser.username);

  // # Перевіряємо, чи збігається згенерований пароль зі справжнім
  expect(invalidPassword).to.not.equal(testUser.password);

 // # Вводимо некоректне ім'я користувача 
 cy.findByPlaceholderText('Email or Username').clear().type(invalidEmail);

 // # Вводимо некоректний пароль 
 cy.findByPlaceholderText('Password').clear().type(invalidPassword);

 // # Клікаємо на кнопку входу
 cy.findByText('Sign in').click();

+ // * Перевіряємо правильність повідомлення про помилку
+ cy.findByText('Enter a valid email or username and/or password.').should('exist').and('be.visible');
});
Як провести сучасне e2e-тестування з Cypress JS

Сподіваємось, цей матеріал допоміг вам отримати базові навички e2e-тестування, які будуть корисними для ваших застосунків.

Якщо ви хочете побачити повний код тесту, перейдіть за посиланням.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 6.2K
Приєднався: 7 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація