Шість помилок тестування клієнтської частини та як їх виправити

25 хв. читання

Під час написання тестів для клієнтської частини застосунків ви натрапите на багато підводних каменів. Вони можуть призвести до поганої підтримуваності, повільного виконання коду, а у найгіршому випадку — тестуванню не можна буде довіряти. Але так не повинно бути. У цій статті ми поговоримо про часті помилки розробників, і, звичайно ж, про те, як їх уникнути.

Шість помилок тестування клієнтської частини та як їх виправити
«Це пастка!» — Адмірал Акбар (зображення: Ramona Schwering)

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

Пастки тестування фронтенду

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

  • Модульні тести (Unit tests) допомагають перевірити найдрібніші складники ваших застосунків. Ці складники можуть бути класами, інтерфейсами або методами. Тести перевіряють, чи повертають вони очікуваний результат, застосовуючи заздалегідь визначені вхідні дані, — так складники тестуються окремо та ізольовано.

  • Тести інтеграції перевіряють взаємодію окремих складників коду.

  • End-to-end (наскрізні) тести перевіряють застосунок, наче це робить фактичний користувач. Це нагадує перевірку системи тестувальниками.

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

Однак ці переваги може затьмарити те, що є безліч слабких місць і можливостей для помилок, які мають ще й різні причини. Багато з них можна вважати «пастками». Уявіть, що ви робите щось з найкращими намірами, але все завершується болем і виснаженням: це найгірший вид технічного боргу.

Чому ми повинні перейматись тестуванням пасток?

Якщо узагальнити, то існує три основних пастки тестування клієнтської частини:

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

  2. Тести, які важко підтримувати. Ця друга пастка — критичніша та важливіша причина неробочих тестів. Наприклад, ви можете повернутися до тестів через кілька місяців та не зрозуміти їхнього вмісту або що саме вони перевіряють. Або учасники команди можуть запитати, яке завдання у старого тесту, який ви писали. Взагалі, занадто багато класів або абстракцій, пояснених у простирадлах тексту або коду, можуть швидко вбити мотивацію розробника і призвести до звичайнісінького хаосу. Пастки тут можуть бути викликані слідуванням за найкращими методиками, які не підходять для тестів.

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

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

Золоте правило

Припустимо, ви працюєте на захопливій, але вимогливій роботі. Ви цілковито на ній зосереджені. Ваш мозок наповнений готовим кодом і не залишається місця для додаткової складності — голова завантажена і відсікає думки про тестування. Це найгірше — коли команди відмовляються від тестів, адже ті здаються складними.

Йоні Ґолдберґ (Yoni Goldberg) у своїх настановах «JavaScript Testing Best Practices» виводить золоте правило, що не дає сприймати тести як тягар: ставтесь до тестів як до друзів, що допомагають вам і ніколи не здаються перешкодою.

Безумовно, це найважливіше у тестуванні. Але як досягти цього? Легкий спойлер: більшість наших прикладів ілюструватиме це. Ключем успіху є принцип KISS — роби коротше і простіше (keep it simple, stupid, або інший варіант — keep it short and simple). Будь-який тест, незалежно від типу, повинен бути зрозумілим і простим.

Але який цей зрозумілий і простий тест? Як дізнатись, що тест достатньо простий? Надзвичайно важливо не ускладнювати тести. Йоні Ґолдберґ ідеально підсумував головну мету:

З першого погляду на тест ви маєте розуміти, для чого він створений.

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

А ще існує принцип щодо дублювання — DRY: не повторюйтесь (Don't repeat yourself). Взагалі уникайте дублікатів, якщо абстракція перешкоджає зрозуміти ваш тест.

Цей фрагмент коду є прикладом:

// Cypress
beforeEach(() => {
    // З першого погляду важко зрозуміти, що
    // насправді робить ця команда
    cy.setInitialState()
       .then(() => {
           return cy.login();
       })
}):

Як зробити тест зрозумілішим? Ви можете подумати, що змістовно називати команди недостатньо. Краще було б документувати команди й у коментарях, наприклад:

// Cypress
/**
* Logs in silently using API
* @memberOf Cypress.Chainable#
* @name loginViaApi
* @function
*/
Cypress.Commands.add('loginViaApi', () => {
   return cy.authenticate().then((result) => {
       return cy.window().then(() => {
           cy.setCookie('bearerAuth', result);
       }).then(() => {
           cy.log('Fixtures are created.');
       });
   });
});

Таке документування може бути необхідним у цьому випадку, оскільки у майбутньому це допоможе вам і вашій команді краще зрозуміти тест. Як бачите, інколи найкращі методики розробки коду не підходять для коду тестів. Просто вони не є кодом для розробки продукту, тож ніколи не варто й ставитись до нього так. Звичайно, треба з кодом тестів треба бути так само обережними, як і зі звичайним кодом. Однак деякі методики можуть суперечити зрозумілості. У таких випадках пам'ятайте про золоте правило та покладайтесь насамперед на свій розробницький досвід.

Пастки проєктування тестів

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

Правило трьох

Почнемо з прикладу. Звертайте увагу на назву. Зміст тесту є другорядним.

// Jest
describe('deprecated.plugin', () => {
    it('should throw error',() => {
       // Власне тест, скорочений для перекидання помилок
\t\t\t // компонентів
        const component = createComponent();

        expect(global.console.error).toBeCalled();
    });
});

Чи можете ви з першого погляду зрозуміти, для чого задумано цей тест? Особливо дивлячись на заголовок у результатах тестування (наприклад, ви можете переглянути записи журналу у ваших пайплайнах безперервної інтеграції). Очевидно, що він повинен повернути помилку. Але яку? За яких обставин вона повинна бути повернена? Як бачите, нелегко з першого погляду зрозуміти, для чого цей тест, оскільки його назва багатозначна.

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

Це правило, введене Роєм Ошеровим (Roy Osherove), допоможе вам уточнити призначення тесту. Це відома методика модульного тестування, але вона також буде корисною в end-to-end-тестуванні. Відповідно до правила, назва тесту повинна складатися з трьох частин:

  1. Що тестуватиметься?
  2. За яких обставин це буде тестуватися?
  3. Який очікуваний результат?

То як виглядав би наш тест, якби ми дотримувались цього правила? Подивимось:

// Jest
describe('deprecated.plugin', () => {
it('Property: Should throw an error if the deprecated 
         prop is used', () => {
       // Власне тест, скорочений для перекидання помилок
\t\t\t // компонентів
        const component = createComponent();

        expect(global.console.error).toBeCalled();
   });
});

Так, заголовок довгий, але в ньому ви знайдете всі три частини:

  1. Що тестуватиметься? У нашому випадку це власність.
  2. За яких обставин? Ми хочемо випробувати застарілу властивість.
  3. Який очікуваний результат? Застосунок має повернути помилку.

Дотримуючись цього правила, ми зможемо побачити результат тесту з першого погляду — і не треба читати журнали. Отже, тут ми можемо дотриматися нашого золотого правила.

«Впорядковуйте, дійте, затверджуйте» VS «Дано, коли, тоді»

Інша пастка, інший приклад коду. Чи розумієте ви цей тест після першого читання?

// Jest
describe('Context menu', () => {
   it('should open the context menu on click', async () => {
        const contextButtonSelector = 'sw-context-button';
        const contextButton =
              wrapper.find(contextButtonSelector);
        await contextButton.trigger('click');
        const contextMenuSelector = '.sw-context-menu';
        let contextMenu = wrapper.find(contextMenuSelector);
        expect(contextMenu.isVisible()).toBe(false);
        contextMenu = wrapper.find(contextMenuSelector);
        expect(contextMenu.isVisible()).toBe(true);  
   });
});

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

Існує один патерн, що може стати в пригоді: AAA. Це скорочення від «arrange, act, assert», яке визначає, що робити, аби чітко структурувати тест. Розділіть тест на три значущі частини. Цей патерн придатний для порівняно коротких тестів і здебільшого трапляється у модульному тестуванні. Якщо коротко, то є три частини:

  • Arrange (впорядковуйте) Тут ви налаштовуєте систему, яка тестується, щоб створити сценарій, який повинен імітувати тест. Тут може бути що завгодно, від налаштування змінних до роботи з макетами та заглушками.

  • Act (дійте) У цій частині ви перевірятимете потрібну частину коду. Тобто виконаєте всі дії, щоб отримати результат тесту.

  • Assert (затверджуйте) Ця частина є відносно зрозумілою. Ви просто перевіряєте і затверджуєте, чи все працює як слід.

Ось ще один спосіб розробки досить зрозумілого тесту. Пам'ятаючи про правило, ми можемо змінити свій погано написаний тест на такий:

// Jest
describe('Context menu', () => {
    it('should open the context menu on click', () => {
        // Arrange
        const contextButtonSelector = 'sw-context-button';
        const contextMenuSelector = '.sw-context-menu';

        // Assert state before test
        let contextMenu = wrapper.find(contextMenuSelector);
        expect(contextMenu.isVisible()).toBe(false);

        // Act
        const contextButton =
             wrapper.find(contextButtonSelector);
        await contextButton.trigger('click');

        // Assert
        contextMenu = wrapper.find(contextMenuSelector);
        expect(contextMenu.isVisible()).toBe(true);  
    });
});

Але навіщо тоді крок з діями перед затвердженням? І поки ми перебуваємо на ньому, ви не думаєте, що у цьому тесті забагато контексту, як на модульний тест? Правильно. Тут ми маємо справу з інтеграційними тестами. Якщо ми перевіряємо DOM, як у цьому прикладі, нам доведеться перевірити його стан до та після зміни. Тож, хоч методика AAA добре підходить для перевірки окремих функцій та тестування API, тут потрібно щось інше.

Розгляньмо методику AAA з іншої перспективи. Як зазначає Клаудіо Лассала (Claudio Lassala) в одному зі своїх дописів, замість думати, я краще щось зроблю…

  • «…впорядкую свій тест і думаю, що мені дано». Це сценарій з усіма передумовами тесту.

  • «...запускаю тест і думаю, коли саме щось відбувається». Тут ми бачимо дії тесту.

  • «... затверджую результати, думаю, якщо щось трапляється, тоді це те, що я очікую, як результат». Тут ми знаходимо те, що ми хочемо затвердити результатом тесту.

Виділені слова в останньому пункті натякають на іншу методику — керовану поведінкою розробку (BDD). Це методика дано-коли-тоді, розроблена Даніелем Тергост-Нортом (Daniel Terhorst-North) і Крісом Маттсом (Chris Matts). Ви можете бути знайомі з нею, якщо коли писали тести мовою Gherkin:

Feature: Context menu
  Scenario: 
    Given I have a selector for the context menu
       And I have a selector for the context button

    When the context menu can be found
       And this menu is visible
       And this context button can be found
       And is clicked
     
   Then I should be able to find the contextMenu in the DOM
      And this context menu is visible

Однак ви можете застосовувати її у всіх видах тестів, наприклад, структуруванням блоків. Скориставшись ідеєю з нашого списку, переписати приклад тесту досить легко:

// Jest
describe('Context menu', () => {
    it('should open the context menu on click', () => {
        // Дано
        const contextButtonSelector = 'sw-context-button';
        const contextMenuSelector = '.sw-context-menu';

        // Коли
        let contextMenu = wrapper.find(contextMenuSelector);
        expect(contextMenu.isVisible()).toBe(false);
        const contextButton =
             wrapper.find(contextButtonSelector);
        await contextButton.trigger('click');

        // Тоді
        contextMenu = wrapper.find(contextMenuSelector);
        expect(contextMenu.isVisible()).toBe(true);  
    });
});

Дані, якими ми раніше ділилися

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

Шість помилок тестування клієнтської частини та як їх виправити
Дані тесту, якими ми ділимось (автор зображення: Ramona Schwering)

Однак вони можуть бути цим розчаровані. Уявімо, що ці дві людини — це тести, а папір — тестові дані. Назвемо ці два тести, тест А та тест Б. Річ у тім, що тест A та тест B діляться однаковими тестовими даними або, що гірше, покладаються на попередній тест.

Це проблема, яка призводить до нестабільності тестів. Наприклад, якщо попередній тест не виконався або спільні дані пошкоджені, ці тести працюватимуть неправильно. Інший сценарій — тести виконуються у довільному порядку. Коли це трапляється, ви не можете передбачити, чи виконається попередній тест у такому ж порядку, як і перед цим, чи буде завершений після інших, і тоді тести A і Б втратять свою основу. Це також не обмежується end-to-end-тестами; типовим випадком модульне тестування є два тести, що видозмінюють ті самі сумнівні дані.

Гаразд, розглянемо приклад коду з end-to-end-тесту, який перевіряє функціональність входу в інтернет-магазин.

// Cypress
describe('Customer login', () => {

    // Запускається перед кожним тестом
    beforeEach(() => {
        // Крок 1: Налаштовуємо очищення стану застосунку
        cy.setInitialState()
           .then(() => {
             // Крок 2: Створюємо дані тестування
             return cy.setFixture('customer');
           })
            // … застосовуємо запит cy., щоб створити замовника
    }):

    // … далі починається тест
})

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

Друге — створюємо всі дані, необхідні для запуску тесту. У нашому прикладі потрібно зробити клієнта, який зможе увійти в наш магазин. Ми створимо всі дані, які потрібні тесту, спеціально для самого тесту. Так він буде незалежним, а порядок виконання може бути випадковим. Обидва етапи важливі для ізоляції тестів від будь-якого іншого тесту або стороннього впливу, це дасть нам стабільність.

Впровадження пасток

Гаразд, ми поговорили про хороше проєктування тестів. Але говорити про це недостатньо, оскільки диявол ховається в деталях. Отже, перевірмо усе та випробуємо реалізацію нашого тесту.

Що таке Foo Bar?

Для цієї першої пастки у реалізації тесту ми маємо гостя! Це BB-8, і він знайшов щось в одному з наших тестів:

Шість помилок тестування клієнтської частини та як їх виправити
Що в холєри таке той «Foo Bar» (автор зображення: Ramona Schwering)

Він знайшов назву, яка може бути нам знайома, але не йому: Foo Bar. Звичайно, ми, розробники, знаємо, що так інколи називають заповнювач (placeholder). Але якщо ви побачите це в тесті, чи одразу ж здогадаєтесь, про що йдеться? Знову ж таки, тест буває складно зрозуміти за одну мить.

На щастя, цю пастку легко виправити. Розгляньмо тест на Cypress. Це end-to-end-тест, але наша порада стосується й інших різновидів.

// Cypress
it('should create and read product', () => {
    // Відкриваємо модуль додавання продукту
    cy.get('a[href="#/sw/product/create"]').click();

    // Додаємо основні дані про продукт
    cy.get('.sw-field—product-name').type('T-Shirt Ackbar');
    cy.get('.sw-select-product__select_manufacturer')
        .type('Space Company');

    // … Тест триває …
});

Цей тест повинен перевірити, чи можна створювати та читати дані про продукт. У цьому тесті ми просто застосовуємо назви та заповнювачі, пов'язані зі справжнім продуктом:

  • Для назви продукту «футболка» вжито «T-Shirt Akbar».

  • Для назви виробника — «Space Company».

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

Обов'язково подивіться на селектори

Пастка нова, тест той самий. Подивіться ще раз, щось помітили?

// Cypress
it('should create and read product', () => {
    // Відкриваємо модуль додавання продукту
    cy.get('a[href="#/sw/product/create"]').click();

    // Додаємо основні дані про продукт
    cy.get('.sw-field—product-name').type('T-Shirt Ackbar');
    cy.get('.sw-select-product__select_manufacturer')
        .type('Space Company');

    // … Тест триває …
});

Ви помітили ці селектори? Це селектори CSS. Ви можете запитати себе: «Що з ними не так? Вони унікальні, ними легко користуватися і я можу застосовувати їх безпомилково!». Однак ви впевнені, що це завжди так?

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

Шість помилок тестування клієнтської частини та як їх виправити
«Подивіться на селектори. Ви повинні це зробити» (автор зображення: Ramona Schwering)

У цьому випадку пастка стосується переважно end-to-end-тестування. За інших обставин це може діяти і модульного тестування, наприклад, якщо ви користуєтесь селекторами під час тестування компонентів. Як зазначає Кент К. Додс у своїй статті на цю тему:

«Ви не повинні тестувати деталі впровадження»

Можливо, є кращі альтернативи, ніж застосування деталей реалізації для тестування. Натомість тестуйте те, що користувач помітить. А ще краще, вибирайте селектори, які навряд змінюватимуться. Улюбленим типом селектора багатьох є атрибут data. Розробник рідше змінює атрибути data під час рефакторингу, що робить їх ідеальними для пошуку елементів у тестах. Радимо називати їх змістовно, щоб чітко передати їх мету будь-яким розробникам, які працюють над початковим кодом. Це може виглядати так:

// Cypress
cy.get('[data-test=sw-field—product-name]')
  .type('T-Shirt Ackbar');
cy.get('[data-test=sw-select-product__select_manufacturer]')
  .type('Space Company');

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

Примітка: Ця тема величезна, тому краще б її розглянути в іншій статті, тож пізніше ви можете ознайомитись зі статтею Додса «Testing Implementation Details», щоб дізнатися більше про цю тему.

Не поспішайте зі змінами!

І останнє, але не менш важливе. Багато людей потрапляють в таку пастку — це фіксований час очікування, про який йдеться у статті про ненадійні тести. Гляньте на цей тест:

// Cypress
Cypress.Commands.add('typeSingleSelect', {
        prevSubject: 'element',
    },
    (subject, value, selector) => {
    cy.wrap(subject).should('be.visible');
    cy.wrap(subject).click();

    cy.wait(500);            
    cy.get(`${selector} input`)
      .type(value);
});

Маленький рядок з cy.wait(500) — це фіксований час очікування, який зупиняє виконання тесту на пів секунди. Якщо зробити цю помилку суттєвішою, ви знайдете її в спеціальній команді, тож тест застосовуватиме це очікування кілька разів. Кількість секунд буде додаватися з кожним застосуванням цієї команди.

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

Розгляньмо два способи, які пропонують динамічне очікування:

  • Очікування змін в інтерфейсі. Перший спосіб — чекати змін в інтерфейсі застосунку, які може помітити справжній користувач або навіть зреагувати на них. Приклади можуть включати зміну в інтерфейсі (наприклад, зникнення анімації завантаження), очікування анімації, щоб зупинити якийсь процес, тощо. Якщо ви користуєтеся Cypress, це може виглядати так:
// Cypress
cy.get('data-cy="submit"').should('be.visible');

Майже кожен фреймворк тестування надає такі можливості очікування.

  • Очікування запитів API. Ще один спосіб — це очікування на запити API та їхні відповіді відповідно. Cypress пропонує для цього чудову функціональність. Спочатку ви визначите маршрут, на який Cypress повинен очікувати:
// Cypress
cy.intercept({
    url: '/widgets/checkout/info',
    method: 'GET'
}).as('checkoutAvailable');

Згодом ви можете затвердити це у своєму тесті, наприклад:

// Cypress
cy.wait('@request').its('response.statusCode')
  .should('equal', 200);

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

Підсумуємо

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

Найголовніше, що слід пам'ятати, — це золоте правило, про яке ми говорили раніше. Більшість наших прикладів наслідують його. Всі проблеми виникають через нехтування ним. Тест повинен бути доброзичливим помічником, а не перешкодою! Це найважливіше, про що не варто забувати. Тест має сприйматись як звичне завдання, а не як складна математична формула. Робіть усе можливе, щоб цього досягти.

Шість помилок тестування клієнтської частини та як їх виправити
Бачите, тут R2-D2 з легкістю ловить «жуків». Бажаємо, щоб і ви, і ваша команда робила це так само легко, тож зробімо тестування простим і веселим! (автор зображення: Ramona Schwering)

Сподіваємося, що ми змогли допомогти вам і дали кілька ідей про те, як подолати найпоширеніші підводні камені. Однак існує значно більше пасток, які можна знайти та на яких можна вчитися. Якщо маєте що додати, діліться з нами цим у коментарях. Побачимось!

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

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

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

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