Це посібник із надійності JavaScript і Node.js від А до Я. Він узагальнює та курує для вас десятки найкращих публікацій у блогах, книг та інструментів, які може запропонувати ринок
Вирушайте в подорож, яка виходить далеко за межі основ і переходить до складних тем, як тестування у виробництві, мутаційне тестування, тестування на основі властивостей та багато інших стратегічних і професійних інструментів. Якщо ви прочитаєте кожне слово в цьому посібнику, ваші навички тестування, швидше за все, будуть набагато вище середнього
Почніть із розуміння практик тестування, які є основою для будь-якого рівня програми. Потім заглибтеся в обрану область: frontend
- JavaScript & Node.js консультант
- 📗 Тестування Node.js і JavaScript від А до Я - Мій комплексний онлайн-курс із більш ніж 7 годинами відео, 14 типів тестів і більше 40 кращих практик
- Слідкуйте за мною в Twitter
- 🇨🇳Chinese - Завдяки Yves yao
- 🇰🇷Korean - Завдяки Rain Byun
- 🇵🇱Polish - Завдяки Michal Biesiada
- 🇪🇸Spanish - Завдяки Miguel G. Sanguino
- 🇧🇷Portuguese-BR - Завдяки Iago Angelim Costa Cavalcante , Douglas Mariano Valero and koooge
- 🇫🇷French - Завдяки Mathilde El Mouktafi
- 🇯🇵Japanese (draft) - Завдяки Yuichi Yogo and ryo
- 🇹🇼Traditional Chinese - Завдяки Yubin Hsu
- 🇺🇦Ukrainian - Завдяки Serhii Shramko
- Хочете перекласти на свою рідну мову? будь ласка, відкрийте issue 💜
Одна порада, яка надихає всіх інших (1 спеціальний пункт)
Основа - структурування чистих тестів (12 пункт)
Ефективне написання backend-тестів і тестів мікросервісів (13 пунктів)
Написання тестів для веб-інтерфейсу, включаючи тести компонентів і E2E (11 пунктів)
Спостереження за сторожем - вимірювання якості тесту (4 пункти)
Рекомендації щодо CI у світі JS (9 пунктів)
✅ Роби: Тестовий код – це не робочий код. Розробіть його таким, щоб він був коротким, надзвичайно простим, зрозумілим і приємним для роботи. Треба подивитися на тест і миттєво зрозуміти намір.
Бачиш, наші уми вже зайняті основною роботою – production-кодом. Немає «простору» для додаткової складності. Якщо ми спробуємо втиснути в наш бідолашний мозок ще одну систему sus, це сповільнить роботу команди, яка працює проти того, чому ми проводимо тестування. Практично тут, багато команд просто відмовляються від тестування.
Випробування — це можливість для чогось іншого — доброзичливого помічника, другого пілота, який дає велику цінність за невеликі інвестиції. Наука говорить нам, що у нас є дві системи мозку: система 1 використовується для легкої діяльності, як-от водіння автомобіля по порожній дорозі, і система 2, яка призначена для складних і усвідомлених операцій, таких як розвʼязування математичного рівняння. Розробіть свій тест для системи 1. Дивлячись на тестовий код, це повинно відчуватися таким же легким, як зміна HTML-документа, а не розвʼязування 2X(17 × 24).
Цього можна досягти шляхом вибіркового вибору методів, інструментів і тестових цілей, які є економічно ефективними та забезпечують високу рентабельність інвестицій. Тестуйте лише стільки, скільки потрібно, намагайтеся підтримувати його спритність, інколи навіть варто відмовитися від деяких тестів і поміняти надійність на швидкість і простоту.
Більшість наведених нижче порад є похідними від цього принципу.
✅ Роби: У звіті про тестування має бути вказано, чи задовольняє поточна версія програми вимоги до людей, які не обов’язково знайомі з кодом: тестувальника, інженера DevOps, який розгортає, і майбутнього вас через два роки. Цього можна досягти найкраще, якщо тести складатимуться на рівні вимог і включатимуть 3 частини:
(1) Що перевіряється? Наприклад, метод ProductsService.addNewProduct
(2) За яких обставин і сценарію? Наприклад, у метод не передається ціна
(3) Який результат очікується? Наприклад, новий продукт не затверджено
❌ Інакше: Розгортання щойно не вдалось, тест із назвою «Add product» не вдався. Чи це говорить вам про те, що саме несправно?
👇 Примітка: Кожен пункт має приклади коду, а інколи також зображення. Натисніть, щоб розгорнути
✏ Приклади коду
//1. блок тестування
describe('Products Service', function() {
describe('Add new product', function() {
//2. сценарій і 3. очікування
it('When no price is specified, then the product status is pending approval', ()=> {
const newProduct = new ProductService().add(...);
expect(newProduct.status).to.equal('pendingApproval');
});
});
});
© Credits & read-more
1. Roy Osherove - Naming standards for unit tests✅ Роби: Структуруйте свої тести за допомогою 3 добре відокремлених розділів «Упорядкуйте, дійте та затверджуйте» (AAA). Дотримання цієї структури гарантує, що читач не витрачатиме мозок-CPU на розуміння плану тестування:
1st A - Arrange(Упорядкувати): Весь код налаштування, щоб привести систему до сценарію, який має імітувати тест. Це може включати створення екземпляра тестового конструктора блоку, додавання записів БД, mocking/stubbing обʼєктів та будь-який інший код підготовки
2nd A - Act(Дія): Виконайте тестовий блок. Зазвичай 1 рядок коду
3rd A - Assert(Стверджування): Переконайтеся, що отримане значення відповідає очікуванням. Зазвичай 1 рядок коду
❌ Інакше: Ви не лише витрачаєте години на розуміння основного коду, але те, що мало бути найпростішою частиною дня (тестування), напружує ваш мозок
✏ Приклади коду
describe("Customer classifier", () => {
test("When customer spent more than 500$, should be classified as premium", () => {
//Arrange
const customerToClassify = { spent: 505, joined: new Date(), id: 1 };
const DBStub = sinon.stub(dataAccess, "getCustomer").reply({ id: 1, classification: "regular" });
//Act
const receivedClassification = customerClassifier.classifyCustomer(customerToClassify);
//Assert
expect(receivedClassification).toMatch("premium");
});
});
test("Should be classified as premium", () => {
const customerToClassify = { spent: 505, joined: new Date(), id: 1 };
const DBStub = sinon.stub(dataAccess, "getCustomer").reply({ id: 1, classification: "regular" });
const receivedClassification = customerClassifier.classifyCustomer(customerToClassify);
expect(receivedClassification).toMatch("premium");
});
✅ Роби: Написання ваших тестів у декларативному стилі дає змогу читачеві миттєво отримати переваги, не витрачаючи на це зусилля мозку. Коли ви пишете імперативний код, наповнений умовною логікою, читач змушений докладати більше зусиль мозку. У такому випадку закодуйте очікування мовою, схожою на людську, у декларативному стилі BDD, використовуючи expect
або should
і не використовуючи спеціальний код. Якщо Chai & Jest не містить потрібного твердження, і воно дуже повторюване, подумайте над extending Jest matcher (Jest) або створіть custom Chai plugin
❌ Інакше: Команда буде писати менше тестів і прикрашати набридливі .skip()
✏ Приклади коду
test("When asking for an admin, ensure only ordered admins in results", () => {
// припускаючи, що ми додали сюди двох адміністраторів «admin1», «admin2» і юзера «user1»
const allAdmins = getUsers({ adminOnly: true });
let admin1Found,
adming2Found = false;
allAdmins.forEach(aSingleUser => {
if (aSingleUser === "user1") {
assert.notEqual(aSingleUser, "user1", "A user was found and not admin");
}
if (aSingleUser === "admin1") {
admin1Found = true;
}
if (aSingleUser === "admin2") {
admin2Found = true;
}
});
if (!admin1Found || !admin2Found) {
throw new Error("Not all admins were returned");
}
});
it("When asking for an admin, ensure only ordered admins in results", () => {
// припускаючи, що ми додали сюди двох адміністраторів «admin1», «admin2» і юзера «user1»
const allAdmins = getUsers({ adminOnly: true });
expect(allAdmins)
.to.include.ordered.members(["admin1", "admin2"])
.but.not.include.ordered.members(["user1"]);
});
✅ Роби: Тестування внутрішніх компонентів приносить величезні накладні витрати майже за безцінь. Якщо ваш код/API дає належні результати, чи варто вам справді витратити наступні 3 години на те, щоб перевірити, ЯК він працює всередині, а потім підтримувати ці крихкі тести? Кожного разу, коли перевіряється загальнодоступна поведінка, приватна реалізація також неявно перевіряється, і ваші тести не працюватимуть, лише якщо є певна проблема (наприклад, неправильний вихід). Цей підхід також називають «поведінковим тестуванням». З іншого боку, якщо ви тестуєте внутрішні елементи (підхід білого ящика) — ваш фокус переходить від планування результату компонента до дрібних деталей, і ваш тест може бути невдалим через незначні переписування коду, хоча результати хороші — це значно збільшує технічне обслуговування
❌ Інакше: Ваші тести поводяться як хлопчик, який кричав вовк: вигуки хибнопозитивних криків (наприклад, Тест провалився через те, що ім’я приватної змінної було змінено). Не дивно, що незабаром люди почнуть ігнорувати сповіщення CI, поки одного дня не проігнорують справжню помилку...
✏ Приклади коду
class ProductService {
// цей метод використовується тільки внутрішньо
// зміна цієї назви призведе до невдачі тестів
calculateVATAdd(priceWithoutVAT) {
return { finalPrice: priceWithoutVAT * 1.2 };
// Зміна формату результату або назви ключа вище призведе до невдачі тестів
}
// публічний метод
getPrice(productId) {
const desiredProduct = DB.getProduct(productId);
finalPrice = this.calculateVATAdd(desiredProduct.price).finalPrice;
return finalPrice;
}
}
it("White-box test: When the internal methods get 0 vat, it return 0 response", async () => {
// Немає вимоги дозволяти користувачам розраховувати ПДВ, відображати лише остаточну ціну. Тим не менш, ми хибно наполягаємо тут на перевірці внутрішніх елементів класу
expect(new ProductService().calculateVATAdd(0).finalPrice).to.equal(0);
});
✅ Роби: Подвійні тести є необхідним злом, тому що вони пов'язані з внутрішніми елементами програми, але деякі з них мають величезну цінність (Прочитайте тут нагадування: mocks vs stubs vs spies).
Перш ніж використовувати подвійні тести, поставте дуже просте запитання: чи використовую я його для тестування функціональності, яка відображається або може з’явитися в документі з вимогами? Якщо ні, це запах тестування білої коробки.
Наприклад, якщо ви хочете перевірити, чи ваша програма поводиться належним чином, коли платіжна служба не працює, ви можете заблокувати платіжну службу та запустити деякий повернення «Немає відповіді», щоб переконатися, що одиниця, що тестується, повертає правильне значення. Це перевіряє поведінку/відповідь/результат нашої програми за певних сценаріїв. Ви також можете використовувати шпигуна, щоб підтвердити, що електронний лист було надіслано, коли ця служба не працює — це знову перевірка поведінки, яка, ймовірно, з’явиться в документі вимог («Надіслати електронний лист, якщо платіж не вдалося зберегти»). З іншого боку, якщо ви знущаєтеся над платіжною службою та переконаєтесь, що її викликали з правильними типами JavaScript — тоді ваш тест зосереджений на внутрішніх речах, які не мають нічого спільного з функціями програми та, ймовірно, часто змінюватимуться.
❌ Інакше: Будь-яке переписування коду вимагає пошуку всіх макетів у коді та відповідного оновлення. Тести стають тягарем, а не корисним другом
✏ Приклади коду
it("When a valid product is about to be deleted, ensure data access DAL was called once, with the right product and right config", async () => {
// Припустимо, ми вже додали продукт
const dataAccessMock = sinon.mock(DAL);
// ПОГАНО: тестування внутрощів насправді є нашою головною метою, а не лише побічним ефектом
dataAccessMock
.expects("deleteProduct")
.once()
.withArgs(DBConfig, theProductWeJustAdded, true, false);
new ProductService().deletePrice(theProductWeJustAdded);
dataAccessMock.verify();
});
👏Роби це правильно. Приклад: (spy) шпигуни зосереджені на перевірці вимог, але як побічний ефект неминуче торкаються внутрішніх органів
it("When a valid product is about to be deleted, ensure an email is sent", async () => {
// Припустимо, ми вже додали продукт
const spy = sinon.spy(Emailer.prototype, "sendEmail");
new ProductService().deletePrice(theProductWeJustAdded);
// добре: ми маємо справу з внутрішніми кодом? Так, але як побічний ефект тестування вимог (надсилання електронного листа)
expect(spy.calledOnce).to.be.true;
});
Відвідайте мій онлайн-курс Testing Node.js & JavaScript From A To Z
✅ Роби: Часто production помилки виявляються під час дуже специфічних і несподіваних вхідних даних — ««чим реалістичнішими є тестові вхідні дані, тим більші шанси виявити помилки на ранній стадії. Використовуйте спеціальні бібліотеки, як-от Chance або Faker, щоб генерувати псевдореальні дані. Наприклад, такі бібліотеки можуть створювати реалістичні номери телефонів, імена користувачів, кредитні картки, назви компаній і навіть текст «lorem ipsum». Ви також можете створити деякі тести (на додаток до модульних тестів, а не як заміну), які рандомізують дані фальсифікаторів, щоб розширити тестований блок або навіть імпортувати реальні дані з вашого робочого середовища. Хочете перейти на новий рівень? Дивіться наступний пункт (тестування на основі властивостей).
❌ Інакше: Усе ваше тестування розробки хибно показуватиме зелений колір, якщо ви використовуєте синтетичні вхідні дані, як-от «Foo», але тоді production може стати червоним, коли хакер передасть неприємний рядок, як-от «@3e2ddsf». ##’ 1 fdsfds . fds432 AAAA”
✏ Приклади коду
const addProduct = (name, price) => {
const productNameRegexNoSpace = /^\S*$/; // white-space не допускаються
if (!productNameRegexNoSpace.test(name)) return false; // цей шлях не досягнуто через рінній вихід
// Логіка тут
return true;
};
test("Wrong: When adding new product with valid properties, get successful confirmation", async () => {
// Рядок "Foo", який використовується в усіх тестах, ніколи не викликає хибний результат
const addProductResult = addProduct("Foo", 5);
expect(addProductResult).toBe(true);
// Позитивний-хибний: операція вдалася, тому що ми ніколи не намагалися використати
// назву продукту з пробілами
});
it("Better: When adding new valid product, get successful confirmation", async () => {
const addProductResult = addProduct(faker.commerce.productName(), faker.random.number());
// Згенеровані випадкові данні: {'Sleek Cotton Computer', 85481}
expect(addProductResult).to.be.true;
// Тест провалився, випадковий вхід ініціював певний шлях, який ми не планували.
// Ми рано виявили помилку!
});
✅ Роби: Зазвичай ми обираємо кілька вхідних зразків для кожного тесту. Навіть якщо формат введення нагадує реальні дані (дивись ‘Don’t foo’), ми розглядаємо лише кілька комбінацій вводу (method(‘’, true, 1), method(“string” , false , 0)), Однак production API, який викликається з 5 параметрами, може бути викликаний із тисячами різних перестановок, одна з яких може призвести до зупинки нашого процесу (дивись Fuzz тестування). Що, якби ви могли написати один тест, який автоматично надсилає 1000 перестановок різних вхідних даних і виявляє, для якого вхідного сигналу наш код не повертає правильну відповідь? Тестування на основі властивостей — це техніка, яка робить саме це: надсилаючи всі можливі комбінації вхідних даних до вашого тестованого пристрою, це збільшує ймовірність виявлення помилки. Наприклад, задано метод — addNewProduct(id, name, isDiscount) — бібліотеки, що підтримують, викличуть цей метод із багатьма комбінаціями (число, рядок, логічний вираз), наприклад (1, «iPhone», false), (2, «Galaxy» », правда). Ви можете запустити тестування на основі властивостей за допомогою улюбленого засобу виконання тестів (Mocha, Jest тощо), використовуючи такі бібліотеки, як js-verify чи testcheck (значно краща документація). Оновлення: Nicolas Dubien пропонує в коментарях нижче checkout fast-check який, здається, пропонує деякі додаткові функції та також активно підтримується
❌ Інакше: Несвідомо ви обираєте тестові вхідні дані, які охоплюють лише шляхи коду, які добре працюють. На жаль, це знижує ефективність тестування як засобу виявлення помилок
✏ Приклади коду
import fc from "fast-check";
describe("Product service", () => {
describe("Adding new", () => {
// буде виконано 100 разів з різними випадковими властивостями
it("Add new product with random yet valid properties, always successful", () =>
fc.assert(
fc.property(fc.integer(), fc.string(), (id, name) => {
expect(addNewProduct(id, name).status).toEqual("approved");
})
));
});
});
✅ Роби: Коли є потреба в снепшот тестуванні, використовуйте лише короткі та цілеспрямовані снепшоти (3-7 рядків) які входять до складу тесту (Inline Snapshot) а не в зовнішніх файлах. Дотримуючись цієї вказівки, ваші тести залишатимуться зрозумілими та менш крихкими.
З іншого боку, навчальні посібники та інструменти «класичних знімків» заохочують зберігати великі файли (наприклад, розмітку візуалізації компонентів, результат JSON API) на зовнішньому носії та забезпечувати порівняння отриманого результату зі збереженою версією під час кожного тестового запуску. Це, наприклад, може неявно поєднати наш тест із 1000 рядками з 3000 значеннями даних, які автор тесту ніколи не читав і не міркував. Чому це неправильно? Таким чином, є 1000 причин невдачі вашого тесту - достатньо змінити один рядок, щоб знімок став недійсним, і це, ймовірно, станеться часто. Як часто? для кожного пробілу, коментаря або незначної зміни CSS/HTML. Крім того, назва тесту не дасть підказки про помилку, оскільки вона лише перевіряє, чи не змінилися 1000 рядків, а також заохочує автора тесту прийняти за бажаний істинний довгий документ, який він не міг перевірити та перевірити. Усе це симптоми незрозумілого та жадібного випробування, яке не є зосередженим і спрямоване на досягнення занадто багато
Варто зазначити, що є кілька випадків, коли довгі та зовнішні снепшоти є прийнятними - коли стверджується на схемі, а не на даних (вилучення значень і фокусування на полях) або коли отриманий документ рідко змінюється
❌ Інакше: UI тести впали. Код виглядає правильним, екран відображає ідеально все, що сталося? Ваше тестування снепшотів щойно виявило різницю між вихідним документом і поточним отриманим документом – до розмітки додано один пробіл...
✏ Приклади коду
it("TestJavaScript.com is renderd correctly", () => {
//Arrange
//Act
const receivedPage = renderer
.create(<DisplayPage page="http://www.testjavascript.com"> Test JavaScript </DisplayPage>)
.toJSON();
//Assert
expect(receivedPage).toMatchSnapshot();
//Тепер ми неявно підтримуємо документ довжиною 2000 рядків
//кожен додатковий розрив рядка або коментар - порушить цей тест
});
it("When visiting TestJavaScript.com home page, a menu is displayed", () => {
//Arrange
//Act
const receivedPage = renderer
.create(<DisplayPage page="http://www.testjavascript.com"> Test JavaScript </DisplayPage>)
.toJSON();
//Assert
const menu = receivedPage.content.menu;
expect(menu).toMatchInlineSnapshot(`
<ul>
<li>Home</li>
<li> About </li>
<li> Contact </li>
</ul>
`);
});
✅ Роби: Включіть усі необхідні деталі, які впливають на результат тесту, але не більше того. Як приклад, розглянемо тест, який має розраховувати 100 рядків вхідного JSON - Вставляти це в кожен тест утомливо. Якщо витягти його за межі transferFactory.getJSON(), тест залишиться невизначеним - Без даних важко співвіднести результат тесту з причиною («чому він має повертати статус 400?»). Класичні книжкові шаблони x-unit назвали цей шаблон «таємничим гостем» - Щось невидиме вплинуло на наші результати тестування, ми не знаємо, що саме. Ми можемо досягти кращих результатів, витягнувши повторювані довгі частини назовні І чітко зазначивши, які конкретні деталі мають значення для тесту. Переходячи до наведеного вище прикладу, тест може передавати параметри, які підкреслюють важливість: transferFactory.getJSON({sender: undefined}). У цьому прикладі читач повинен негайно зробити висновок, що порожнє поле відправника є причиною, чому тест повинен очікувати помилку перевірки або будь-який інший подібний адекватний результат.
❌ Інакше: Копіювання 500 рядків JSON зробить ваші тести непридатними для обслуговування та читання. Перенесення всього назовні закінчиться нечіткими тестами, які важко зрозуміти
✏ Приклади коду
👎 Приклад антишаблону: Помилка тесту незрозуміла, оскільки вся причина зовнішня і ховається у величезному JSON
test("When no credit, then the transfer is declined", async() => {
// Arrange
const transferRequest = testHelpers.factorMoneyTransfer() // повертає 200 рядків JSON;
const transferServiceUnderTest = new TransferService();
// Act
const transferResponse = await transferServiceUnderTest.transfer(transferRequest);
// Assert
expect(transferResponse.status).toBe(409);// Але чому ми очікуємо невдачі: у тесті все здається абсолютно дійсним 🤔
});
test("When no credit, then the transfer is declined ", async() => {
// Arrange
const transferRequest = testHelpers.factorMoneyTransfer({userCredit:100, transferAmount:200}) // очевидно, брак кредитів
const transferServiceUnderTest = new TransferService({disallowOvercharge:true});
// Act
const transferResponse = await transferServiceUnderTest.transfer(transferRequest);
// Assert
expect(transferResponse.status).toBe(409); // Очевидно, що якщо у користувача немає кредиту, тест впаде
});
✅ Роби: Під час спроби стверджувати, що якийсь вхід викликає помилку, може виглядати правильним використання try-catch-finally і підтверджує, що було введено пропозицію catch. Результатом є незручний і багатослівний тестовий приклад (приклад нижче), який приховує простий намір тесту та очікування результату
Більш елегантною альтернативою є використання однорядкового виділеного твердження Chai: expect(method).to.throw (або Jest: expect(method).toThrow()). Абсолютно обовʼязково також переконатися, що виняток містить властивість, яка повідомляє тип помилки, інакше, враховуючи лише загальну помилку і покаже користувачеві невтішне повідомлення
❌ Інакше: Буде складно зробити висновок зі звітів про випробування (наприклад, звітів CI), що пішло не так
✏ Приклади коду
👎 Приклад антишаблону: Довгий тестовий приклад, який намагається підтвердити існування помилки за допомогою try-catch
it("When no product name, it throws error 400", async () => {
let errorWeExceptFor = null;
try {
const result = await addNewProduct({});
} catch (error) {
expect(error.code).to.equal("InvalidInput");
errorWeExceptFor = error;
}
expect(errorWeExceptFor).not.to.be.null;
// якщо це твердження не виконується, відображатимуться лише результати тестів/звіти
// якщо якесь значення дорівнює нулю, не буде жодного слова про відсутній виняток
});
👏 Приклад правильного виконання: Очікування, зрозумілі людині, які можуть бути легко зрозумілі, можливо, навіть спеціалістам QA або PM
it("When no product name, it throws error 400", async () => {
await expect(addNewProduct({}))
.to.eventually.throw(AppError)
.with.property("code", "InvalidInput");
});
✅ Роби: Різні тести повинні виконуватися за різними сценаріями: quick smoke, без IO, тести мають запускатися, коли розробник зберігає або фіксує файл, повні наскрізні тести (e2e) зазвичай запускаються, коли надсилається новий запит на отримання тощо. Цього можна досягти позначаючи тести ключовими словами, як-от #cold #api #sanity, щоб ви могли працювати зі своїм пакетом тестування та викликати потрібну підмножину. Наприклад, ось як ви можете викликати лише групу перевірки розумності за допомогою Mocha: mocha — grep ‘sanity’
❌ Інакше: Виконання всіх тестів, у тому числі тестів, які виконують десятки запитів до БД, щоразу, коли розробник вносить невеликі зміни, може бути надзвичайно повільним і утримувати розробників від виконання тестівВиконання всіх тестів, у тому числі тестів, які виконують десятки запитів до БД, щоразу, коли розробник вносить невеликі зміни, може бути надзвичайно повільним і утримувати розробників від виконання тестів
✏ Приклади коду
👏 Приклад правильного виконання: Позначення тестів як «#cold-test» дозволяє тестувальнику виконувати лише швидкі тести (Cold===швидкі тести, які не виконують введення-виведення (IO) та можуть виконуватися часто, навіть коли розробник вводить текст)
// цей тест швидкий (без БД), і ми позначаємо його відповідним чином
// тепер користувач/CI може запускати його часто
describe("Order service", function() {
describe("Add new order #cold-test #sanity", function() {
test("Scenario - no currency was supplied. Expectation - Use the default currency #sanity", function() {
// Логіка тут
});
});
});
✅ Роби: Застосуйте певну структуру до свого набору тестів, щоб випадковий відвідувач міг легко зрозуміти вимоги (тести — найкраща документація) і різні сценарії, які тестуються. Звичайним методом для цього є розміщення принаймні 2 блоків «describe» над вашими тестами: 1-й призначений для назви тестованого блоку, а 2-й для додаткового рівня категоризації, як-от сценарій або спеціальні категорії (дивись Приклади коду). Це також значно покращить звіти про тести: читач легко визначить категорії тестів, заглибиться в потрібний розділ і порівнює невдалі тести. Крім того, розробнику стане набагато легше орієнтуватися в коді набору з багатьма тестами. Існує кілька альтернативних структур для набору тестів, які ви можете розглянути, наприклад given-when-then і RITE
❌ Інакше: Дивлячись на звіт із плоским і довгим списком тестів, читач повинен побіжно прочитати довгі тексти, щоб зробити висновок про основні сценарії та співвіднести загальність невдалих тестів. Розглянемо наступний випадок: коли 7/100 тестів не вдаються, перегляд плоского списку потребує прочитання тексту невдалих тестів, щоб побачити, як вони пов’язані один з одним. Однак в ієрархічному звіті всі вони можуть входити до одного потоку або категорії, і читач швидко зрозуміє, що або принаймні де є основна причина збою
✏ Приклади коду
👏 Приклад правильного виконання: Структурування набору з назвою блоку, що тестується, і сценаріями призведе до зручного звіту, який показано нижче
// Тестуєма одиниця
describe("Transfer service", () => {
// Сценарій
describe("When no credit", () => {
// Очікування
test("Then the response status should decline", () => {});
// Очікування
test("Then it should send email to admin", () => {});
});
});
👎 Приклад антишаблону: Плоский список тестів ускладнить читачеві ідентифікацію історій користувачів і співвіднесення невдалих тестів
test("Then the response status should decline", () => {});
test("Then it should send email", () => {});
test("Then there should not be a new transfer record", () => {});
✅ Роби: Ця публікація зосереджена на порадах щодо тестування, які пов’язані з Node JS або, принаймні, можуть бути представлені на прикладі Node JS. Однак у цьому розділі згруповано кілька добре відомих порад, не повʼязаних з Node
Навчайтеся і практикуйтеся TDD принципи — вони надзвичайно цінні для багатьох, але не лякайтеся, якщо вони не відповідають вашому стилю, ви не єдині. Розгляньте можливість написання тестів перед кодом у red-green-refactor style, переконайтеся, що кожен тест перевіряє саме одну річ, якщо ви знайдете помилку — перед виправленням напишіть тест, який виявить цю помилку в майбутньому, дайте кожному тесту принаймні один раз помилитися, перш ніж стати зеленим, запустіть модуль, написавши швидкий і спрощений код, який задовольняє тест - потім поступово рефакторинг і переведіть його до рівня виробництва, уникайте будь-якої залежності від середовища (шляхи, ОС тощо)
❌ Інакше: Ви пропустите перлини мудрості, які збиралися десятиліттями
✅ Роби: Піраміда тестування, Хоча їй понад 10 років, це чудова та релевантна модель, яка пропонує три типи тестування та впливає на стратегію тестування більшості розробників. Водночас з’явилося більше кількох блискучих нових методів тестування, які ховаються в тіні піраміди тестування. Враховуючи всі драматичні зміни, які ми спостерігаємо за останні 10 років (мікросервіси, хмарні сервіси, безсерверний доступ), чи можливо, що одна досить стара модель підійде для всіх типів програм? Чи не варто світу тестування розглянути можливість вітати нові методи тестування?
Не зрозумійте мене неправильно, у 2019 році піраміда тестування, TDD і модульні тести все ще залишаються потужною технікою та, мабуть, найкраще підходять для багатьох програм. Тільки, як і будь-яка інша модель, попри її корисність, інколи вона може помилятися. Наприклад, розглянемо IoT-додаток, який приймає багато подій у шину повідомлень, як-от Kafka/RabbitMQ, які потім надходять у якесь сховище даних і в кінцевому підсумку запитуються деяким інтерфейсом користувача аналітики. Чи справді ми повинні витрачати 50% нашого бюджету на тестування на написання модульних тестів для програми, яка орієнтована на інтеграцію та майже не містить логіки? Зі збільшенням різноманітності типів додатків (ботів, крипто, навичок Alexa) зростає ймовірність знайти сценарії, де піраміда тестування не найкраще підходить.
Настав час збагатити ваше портфоліо тестування та ознайомитися з більшою кількістю типів тестування (наступні пункти пропонують кілька ідей), моделями розуму, як-от піраміда тестування, а також зіставити типи тестування з реальними проблемами, з якими ви стикаєтесь («Гей, наш API зламано, напишімо тестування контрактів, орієнтоване на споживача!'), диверсифікуйте свої тести, як інвестор, який створює портфель на основі аналізу ризиків — оцініть, де можуть виникнути проблеми, і підберіть деякі запобіжні заходи для помʼякшення цих потенційних ризиків
Застереження: аргумент TDD у світі програмного забезпечення приймає типове обличчя хибної дихотомії, деякі проповідують використовувати його всюди, інші вважають, що це диявол. Кожен, хто говорить в абсолюті, помиляється :]
❌ Інакше: Ви пропустите деякі інструменти з неймовірною рентабельністю інвестицій, деякі, як-от Fuzz, lint і mutation, можуть надати цінність за 10 хвилин
✏ Приклади коду
👏 Приклад правильного виконання: Cindy Sridharan пропонує багате портфоліо тестування у своїй дивовижній публікації «Тестування мікросервісів — таким же чином»
✅ Роби: Кожен модульний тест охоплює невелику частину програми, і це дорого, щоб охопити цілу, тоді як наскрізне(end-to-end) тестування легко охоплює велику частину, але є нестабільним і повільним, чому б не застосувати збалансований підхід і написати тести, більші за модульні тести, але менші, ніж наскрізне тестування? Тестування компонентів – це неоспівана пісня світу тестування — вони забезпечують найкраще з обох світів: прийнятну продуктивність і можливість застосовувати шаблони TDD + реалістичне та велике покриття.
Тести компонентів зосереджуються на «блоку» мікросервісу, вони працюють проти API, не мокають(mock) нічого, що належить самому мікросервісу (наприклад, справжню БД або принаймні версію цієї БД у пам’яті), але заглушують усе, що є зовнішнім як виклики до інших мікросервісів. Роблячи це, ми перевіряємо те, що розгортаємо, підходимо до програми ззовні всередину та отримуємо велику впевненість за прийнятний проміжок часу.
У нас є повний посібник, присвячений виключно правильному написанню компонентних тестів
❌ Інакше: Ви можете витратити довгі дні на написання модульних тестів, щоб дізнатися, що ви отримали лише 20% покриття системи
✏ Приклади коду
✅ Роби: Отже, ваш мікросервіс має кілька клієнтів, і ви запускаєте кілька версій сервісу з міркувань сумісності (щоб усі були задоволені). Потім ви змінюєте якесь поле і «бум!», якийсь важливий клієнт, який покладається на це поле, сердиться. Це 22-й підступ світу інтеграції: серверній стороні дуже складно врахувати всі численні очікування клієнтів — З іншого боку, клієнти не можуть проводити жодного тестування, оскільки сервер контролює дати випуску. Існує цілий спектр методів, які можуть помʼякшити проблему контракту, деякі з них прості, інші є багатшими на функції та вимагають крутішої кривої навчання. У простому та рекомендованому підході постачальник API публікує пакет npm із типом API (наприклад, JSDoc, TypeScript). Тоді споживачі зможуть отримати цю бібліотеку та отримати вигоду від аналізу часу кодування та перевірки. Вигадливіший підхід — використання PACT, створеного для формалізації цього процесу за допомогою дуже руйнівного підходу —«не сервер сам визначає план тестування, а клієнт визначає тести. PACT може записувати очікування клієнта та розміщувати в спільному місці, «посереднику», щоб сервер міг отримати очікування та запускати кожну збірку за допомогою бібліотеки PACT для виявлення порушених контрактів — очікування клієнта, яке не відповідають. Завдяки цьому всі невідповідності API сервера та клієнта виявляються на ранній стадії збирання/CI, що може позбавити вас від розчарування
❌ Інакше: Альтернативами є виснажливе ручне тестування або страх розгортання
✅ Роби: Багато хто уникає мідлвари, оскільки воно представляє невелику частину системи та потребує активного сервера Express. Обидві причини неправильні — Проміжні програми невеликі, але впливають на всі або більшість запитів і можуть бути легко перевірені як чисті функції, які отримують {req,res} обʼєкти JS. Щоб перевірити функцію мідлвари, потрібно просто викликати її та перевірити (використовуючи, наприклад, Sinon) взаємодію з об’єктами {req,res}, щоб переконатися, що функція виконала правильну дію. Бібліотека node-mock-http йде ще далі й враховує об’єкти {req,res} разом зі спостереженням за їхньою поведінкою. Наприклад, він може стверджувати, чи статус http, встановлений для об’єкта res, відповідає очікуванням (див. приклад нижче)
❌ Інакше: Помилка в мідлварі Express === помилка в усіх або більшості запитів
✏ Приклади коду
👏Приклад правильного виконання: Тестування мідлвари в ізоляції без здійснення мережевих викликів і пробудження всієї машини Express
//the middleware we want to test
const unitUnderTest = require("./middleware");
const httpMocks = require("node-mocks-http");
// Синтаксис Jest, еквівалентний describe() і it() у Mocha
test("A request without authentication header, should return http status 403", () => {
const request = httpMocks.createRequest({
method: "GET",
url: "/user/42",
headers: {
authentication: ""
}
});
const response = httpMocks.createResponse();
unitUnderTest(request, response);
expect(response.statusCode).toBe(403);
});
✅ Роби: Використання інструментів статичного аналізу допомагає, надаючи об’єктивні способи покращити якість коду та зберегти ваш код придатним для обслуговування. Ви можете додати інструменти статичного аналізу до збірки CI, щоб перервати її, коли вона виявить запах коду. Його головні переваги над звичайним лінтуванням — це можливість перевіряти якість у контексті кількох файлів (наприклад, виявляти дублікати), виконувати розширений аналіз (наприклад, складності коду) і стежити за історією та прогресом проблем із кодом. Два приклади інструментів, які ви можете використовувати, це SonarQube (4900+ зірочок) і [Code Climate](https ://codeclimate.com/) (2000+ зірочок)
Credit: Keith Holliday
❌ Інакше: З низькою якістю коду помилки та продуктивність завжди будуть проблемою, яку не зможе вирішити жодна блискуча нова бібліотека чи сучасні функції
✏ Приклади коду
✅ Роби: Дивно, але більшість тестувань програмного забезпечення стосуються лише логіки та даних, але деякі з найгірших речей, які трапляються (і їх справді важко мігрувати), — це проблеми з інфраструктурою. Наприклад, ви коли-небудь перевіряли, що відбувається, коли пам’ять вашого процесу перевантажується, або коли сервер/процес вмирає, чи ваша система моніторингу розуміє, коли API стає на 50% повільнішим?. Щоб перевірити та пофіксити такі погані речі — Інженерія хаосу створена Netflix. Він має на меті забезпечити обізнаність, структуру та інструменти для перевірки стійкості нашої програми до хаотичних проблем. Наприклад, один із його відомих інструментів, мавпа хаосу, випадково вимикає сервери, щоб гарантувати, що наш сервіс усе ще може обслуговувати користувачів і не покладатися на один сервер (є також версія Kubernetes, kube-monkey, яка вбиває модулі). Усі ці інструменти працюють на рівні хостингу/платформи, але що, якщо ви хочете перевірити та створити чистий хаос Node, наприклад перевірити, як ваш процес Node справляється з неперехопленими помилками, необробленим відхиленням обіцянок, перевантаженням пам’яті v8 із максимально допустимим 1,7 ГБ чи чи ваш UX залишається задовільним, коли цикл подій часто блокується? щоб вирішити цю проблему, я написав node-chaos (альфа), який надає всілякі хаотичні дії, пов’язані з Node
❌ Інакше: Тут немає виходу, закон Мерфі вдарить по вашому виробництву без жалю
✏ Приклади коду
✅ Роби: Дотримуючись золотого правила (Розділ 0), кожен тест повинен додавати власний набір рядків БД і діяти на них, щоб запобігти зчепленню та легко обґрунтувати потік тесту. Насправді це часто порушується тестувальниками, які заповнюють БД даними перед запуском тестів (також відомих як «test fixture») заради підвищення продуктивності. Хоча продуктивність справді є обґрунтованою проблемою —«її можна пофіксити (див. пункт «Тестування компонентів»), однак складність тесту є дуже болісним сумом, який у більшості випадків повинен керувати іншими міркуваннями. На практиці зробіть так, щоб кожен тестовий приклад явно додавав необхідні записи БД і діяв лише з цими записами. Якщо продуктивність стає критичною проблемою — збалансований компроміс може прийти у формі заповнення єдиного набору тестів, які не змінюють дані (наприклад, запити)
❌ Інакше: Кілька тестів зазнають невдач, розгортання перервано, наша команда витрачатиме дорогоцінний час зараз, у нас є помилка? дослідім, о ні — схоже, що два тести мутували однакові вихідні дані
✏ Приклади коду
👎 Приклад антишаблону: тести не є незалежними і покладаються на якийсь глобальний хук для передачі глобальних даних БД
before(async () => {
//додавання даних про сайти та адмінів до нашої БД. Де дані? назовні. У якомусь зовнішньому фреймворку json або міграції
await DB.AddSeedDataFromJson('seed.json');
});
it("When updating site name, get successful confirmation", async () => {
//Я знаю, що сайт з назвою "портал" існує - я бачив це в початкових файлах
const siteToUpdate = await SiteService.getSiteByName("Portal");
const updateNameResult = await SiteService.changeName(siteToUpdate, "newName");
expect(updateNameResult).to.be(true);
});
it("When querying by site name, get the right site", async () => {
//Я знаю, що сайт з назвою "портал" існує - я бачив це в початкових файлах
const siteToCheck = await SiteService.getSiteByName("Portal");
expect(siteToCheck.name).to.be.equal("Portal"); //Невдача! Попередній тест змінив назву :[
});
👏 Приклад правильного виконання: Ми можемо залишатися в межах тесту, кожен тест діє на власний набір даних
it("When updating site name, get successful confirmation", async () => {
//тест додає свіжі нові записи та діє лише на основі записів
const siteUnderTest = await SiteService.addSite({
name: "siteForUpdateTest"
});
const updateNameResult = await SiteService.changeName(siteUnderTest, "newName");
expect(updateNameResult).to.be(true);
});
✅ Роби: Час, коли тести очищають базу даних, визначає спосіб написання тестів. Два найбільш життєздатних варіанти: очищення після всіх тестів і очищення після кожного тесту. Вибираючи останній варіант, очищення після кожного окремого тесту гарантує чисті таблиці та створює зручні переваги тестування для розробника. На момент початку тесту не існує інших записів, можна бути впевненим, які дані запитуються, і навіть може виникнути спокуса підрахувати рядки під час тверджень. Це має серйозні недоліки: під час роботи в багатопроцесному режимі тести можуть заважати один одному. Поки процес-1 очищає таблиці, у той самий момент процес-2 запитує дані та зазнає помилки (оскільки БД раптово видалено процесом-1). Крім того, важче усунути невдалі тести – відвідування БД не покаже жодних записів.
Другий варіант — очищення після завершення всіх тестових файлів (або навіть щодня!). Такий підхід означає, що та сама БД з наявними записами обслуговує всі тести та процеси. Щоб не наступати один одному на ноги, тести повинні додавати та діяти на основі конкретних записів, які вони додали. Потрібно перевірити, чи додано якийсь запис? Припустімо, що є інші тисячі записів і запит на записи, які були додані явно. Потрібно перевірити, чи видалено запис? Неможливо припустити, що таблиця порожня, перевірте, чи немає цього конкретного запису. Ця техніка дає кілька значних переваг: вона працює нативно в багатопроцесному режимі, коли розробник хоче зрозуміти, що сталося – дані є, а не видаляються. Це також збільшує ймовірність виявлення помилок, оскільки БД заповнена записами, а не штучно порожня. Перегляньте повну порівняльну таблицю тут.
❌ Інакше: Без стратегії розділення записів або очищення - Тести будуть наступати один на одного; Використання транзакцій працюватиме лише для реляційної БД і, ймовірно, ускладниться, коли з’являться внутрішні транзакції
✏ Приклади коду
👏 Прибирання після ВСІХ перевірок. Не обов'язково після кожного запуску. Чим більше даних ми маємо під час виконання тестів, тим більше це нагадує переваги виробництва
// Після всього прибрати (рекомендовано)
// global-teardown.js
module.exports = async () => {
// ...
if (Math.ceil(Math.random() * 10) === 10) {
await new OrderRepository().cleanup();
}
};
✅ Роби: Ізолюйте тестований компонент, перехоплюючи будь-який вихідний HTTP-запит і надаючи потрібну відповідь, щоб HTTP API співавтора не постраждав. Nock є чудовим інструментом для цієї місії, оскільки він забезпечує зручний синтаксис для визначення поведінки зовнішніх служб. Ізоляція є обов’язковою для запобігання шуму та повільної продуктивності, але здебільшого для імітації різних сценаріїв і реакцій. Хороший симулятор польоту — це не малювання чистого блакитного неба, а створення безпечних штормів і хаосу. Це посилюється в архітектурі мікросервісу, де фокус завжди має бути зосереджений на одному компоненті без залучення решти світу. Хоча можна імітувати поведінку зовнішнього сервісу за допомогою повторюваних тестів (mocking), бажано не торкатися розгорнутого коду та діяти на рівні мережі, щоб тести залишалися чисто чорною скринькою. Негативною стороною ізоляції є відсутність виявлення змін компонента співавтора та непорозуміння між двома службами. Обов’язково компенсуйте це за допомогою кількох контрактів або тестів E2E
❌ Інакше: Деякі служби надають фальшиву версію, яку абонент може розгорнути локально, зазвичай за допомогою Docker. Це полегшить налаштування та підвищить продуктивність, але не допоможе з симуляцією різних відповідей; Деякі служби надають середовище «пісочниці», тому справжня служба зазнає удару, але не спричиняє жодних витрат або побічних ефектів. Це зменшить шум під час налаштування сторонньої служби, але також не дозволить імітувати сценарії
✏ Приклади коду
👏 Запобігання мережевим викликам зовнішніх компонентів дозволяє імітувати сценарії та мінімізувати шум
// Перехоплювати запити для сторонніх API і повертати попередньо визначену відповідь
beforeEach(() => {
nock('http://localhost/user/').get(`/1`).reply(200, {
id: 1,
name: 'John',
});
});
✅ Роби: Якщо неможливо підтвердити певні дані, перевірте наявність і типи обов’язкових полів. Іноді відповідь містить важливі поля з динамічними даними, які неможливо передбачити під час написання тесту, як-от дати та числа, що збільшуються. Якщо договір API обіцяє, що ці поля не будуть нульовими і містять правильні типи, обов’язково перевірте це. Більшість бібліотек тверджень підтримують типи перевірки. Якщо відповідь невелика, перевірте дані, що повертаються, і введіть разом в одному твердженні (див. приклад коду). Ще один варіант — перевірити всю відповідь за документом OpenAPI (Swagger). Більшість тестувальників мають розширення спільноти, які перевіряють відповіді API на їхню документацію.
❌ Інакше: Хоча програма виклику коду/API покладається на деякі поля з динамічними даними (наприклад, ідентифікатор, дата), вона не прийде у відповідь і не порушить контракт
✏ Приклади коду
test('When adding a new valid order, Then should get back approval with 200 response', async () => {
// ...
// Стверджування
expect(receivedAPIResponse).toMatchObject({
status: 200,
data: {
id: expect.any(Number), // Будь-яке число задовольняє цей тест
mode: 'approved',
},
});
});
✅ Роби: Перевіряючи інтеграції, виходьте за рамки щасливих і сумних шляхів. Перевіряйте не лише відповіді з помилками (наприклад, помилка HTTP 500), а й аномалії на рівні мережі, як-от повільні відповіді та відповіді, що закінчилися. Це доведе, що код стійкий і може обробляти різні мережеві сценарії, як-от вибір правильного шляху після тайм-ауту, відсутність крихких умов змагання та містить автоматичний вимикач для повторних спроб. Інструменти перехоплення з авторитетним досвідом можуть легко імітувати різні поведінки мережі, як-от неспокійне обслуговування, яке час від часу дає збій. Він навіть може зрозуміти, коли значення тайм-ауту HTTP-клієнта за замовчуванням перевищує змодельований час відповіді, і відразу, не чекаючи, викликати виняток часу очікування
❌ Інакше: Усі ваші тести проходять успішно, тільки виробництво буде аварійно або неправильно повідомляти про помилки, коли треті сторони надсилатимуть виняткові відповіді
✏ Приклади коду
test('When users service replies with 503 once and retry mechanism is applied, then an order is added successfully', async () => {
// Упорядкування
nock.removeInterceptor(userServiceNock.interceptors[0])
nock('http://localhost/user/')
.get('/1')
.reply(503, undefined, { 'Retry-After': 100 });
nock('http://localhost/user/')
.get('/1')
.reply(200);
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};
// Дія
const response = await axiosAPIClient.post('/order', orderToAdd);
// Стверджування
expect(response.status).toBe(200);
});
✅ Роби: Плануючи свої тести, подумайте про охоплення п’яти типових виходів потоку. Коли ваш тест запускає певну дію (наприклад, виклик API), відбувається реакція, відбувається щось значуще та вимагає тестування. Зауважте, що нам байдуже, як все працює. Ми зосереджені на результатах, речах, які помітні ззовні та можуть вплинути на користувача. Ці результати/реакції можна розділити на 5 категорій:
• Відповідь - Тест викликає дію (наприклад, через API) і отримує відповідь. Тепер він займається перевіркою правильності даних відповіді, схеми та статусу HTTP
• Новий стан - Після виклику дії деякі загальнодоступні дані, ймовірно, змінюються
• Зовнішні виклики - Після виклику дії програма може викликати зовнішній компонент через HTTP або будь-який інший транспорт. Наприклад, дзвінок для відправки SMS, електронної пошти або стягнення коштів з кредитної картки
• Message queues - Результатом потоку може бути повідомлення в черзі
• Спостережливість - Деякі речі необхідно відстежувати, як-от помилки чи значні ділові події. Коли транзакція зазнає невдачі, ми очікуємо не лише правильної відповіді, але й правильної обробки помилок і належного журналювання/метрик. Ця інформація надходить безпосередньо до дуже важливого користувача – користувача ops (SRE/адміністратора)
✅ Роби: Коли ви зосереджуєтеся на тестуванні логіки компонентів, деталі інтерфейсу користувача стають шумом, який слід виділити, щоб ваші тести могли зосередитися на чистих даних. На практиці витягуйте потрібні дані з розмітки абстрактним способом, який не надто пов’язаний із графічною реалізацією, затверджуйте лише чисті дані (на відміну від графічних деталей HTML/CSS) і вимикайте анімацію, яка сповільнюється. У вас може виникнути спокуса уникнути візуалізації та протестувати лише задню частину інтерфейсу користувача (наприклад, служби, дії, магазин), але це призведе до вигаданих тестів, які не схожі на реальність, і не виявлять випадків, коли правильні дані навіть не потрапляє в інтерфейс користувача
❌ Інакше: Чисті обчислені дані вашого тесту можуть бути готові через 10 мс, але тоді весь тест триватиме 500 мс (100 тестів = 1 хв) через деяку фантастичну та нерелевантну анімацію
✏ Приклади коду
test("When users-list is flagged to show only VIP, should display only VIP members", () => {
// Arrange
const allUsers = [{ id: 1, name: "Yoni Goldberg", vip: false }, { id: 2, name: "John Doe", vip: true }];
// Act
const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true} />);
// Assert - Спочатку витягніть дані з інтерфейсу користувача
const allRenderedUsers = getAllByTestId("user").map(uiElement => uiElement.textContent);
const allRealVIPUsers = allUsers.filter(user => user.vip).map(user => user.name);
expect(allRenderedUsers).toEqual(allRealVIPUsers); //compare data with data, no UI here
});
test("When flagging to show only VIP, should display only VIP members", () => {
// Arrange
const allUsers = [{ id: 1, name: "Yoni Goldberg", vip: false }, { id: 2, name: "John Doe", vip: true }];
// Act
const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true} />);
// Assert - Змішуйте інтерфейс і дані в твердженні
expect(getAllByTestId("user")).toEqual('[<li data-test-id="user">John Doe</li>]');
});
✅ Роби: Виконуйте запит до HTML-елементів на основі атрибутів, які, ймовірно, переживуть графічні зміни, на відміну від селекторів CSS і міток форм. Якщо призначений елемент не має таких атрибутів, створіть спеціальний тестовий атрибут, наприклад «test-id-submit-button». Виконання цього шляху не тільки гарантує, що ваші функціональні/логічні тести ніколи не зламаються через зміни зовнішнього вигляду, але також стає зрозумілим для всієї команди, що цей елемент і атрибут використовуються тестами, і їх не слід видаляти
❌ Інакше: Ви хочете перевірити функціональність входу, яка охоплює багато компонентів, логіку та служби, все налаштовано ідеально – заглушки, шпигуни, виклики Ajax ізольовані. Все здається ідеальним. Тоді тест не вдається, тому що дизайнер змінив клас CSS div з 'thick-border' на 'thin-border'
✏ Приклади коду
// Код розмітки (React component)
<h3>
<Badge pill className="fixed_badge" variant="dark">
<span data-test-id="errorsLabel">{value}</span>
<!-- зверніть увагу на атрибут data-test-id -->
</Badge>
</h3>
// у цьому прикладі використовується бібліотека react-testing-library
test("Whenever no data is passed to metric, show 0 as default", () => {
// Arrange
const metricValue = undefined;
// Act
const { getByTestId } = render(<dashboardMetric value={undefined} />);
expect(getByTestId("errorsLabel").text()).toBe("0");
});
<!-- розмітка (React component) -->
<span id="metric" className="d-flex-column">{value}</span>
<!-- що якщо дизайнер змінить клас? -->
// Приклад з Еnzyme
test("Whenever no data is passed, error metric shows zero", () => {
// ...
expect(wrapper.find("[className='d-flex-column']").text()).toBe("0");
});
✅ Роби: Щоразу, коли ваш компонент має прийнятний розмір, тестуйте свій компонент ззовні, як це роблять ваші користувачі, повністю візуалізуйте користувальницький інтерфейс, реагуйте на нього та стверджуйте, що оброблений користувальницький інтерфейс поводиться належним чином. Уникайте будь-якого знущального, часткового та поверхневого рендерингу — такий підхід може призвести до невловлених помилок через брак деталей і посилити технічне обслуговування, оскільки тести зіпсуються з внутрішніми елементами (див. маркований пункт 'Favour blackbox testing'. .com/goldbergyoni/javascript-testing-best-practices#-%EF%B8%8F-14-stick-to-black-box-testing-test-only-public-methods)). Якщо один із дочірніх компонентів значно уповільнює (наприклад, анімація) або ускладнює налаштування, подумайте про явну заміну його на підробку
З огляду на все сказане, варто зробити одне застереження: ця техніка працює для малих/середніх компонентів, які містять дочірні компоненти розумного розміру. Повне відтворення компонента із занадто великою кількістю дочірніх компонентів ускладнить обґрунтування помилок тестування (аналіз першопричини) і може працювати надто повільно. У таких випадках напишіть лише кілька тестів проти цього жирного батьківського компонента та більше тестів щодо його дочірніх компонентів
❌ Інакше: Під час перегляду внутрішніх компонентів, викликаючи його приватні методи та перевіряючи внутрішній стан, вам доведеться виконати рефакторинг усіх тестів під час рефакторингу реалізації компонентів. Чи справді у вас є можливості для такого рівня обслуговування?
✏ Приклади коду
class Calendar extends React.Component {
static defaultProps = { showFilters: false };
render() {
return (
<div>
/* Панель фільтрів із кнопкою, щоб приховати/показати фільтри */
<FiltersPanel showFilter={showFilters} title="Choose Filters" />
</div>
);
}
}
// Приклад з React & Enzyme
test("Realistic approach: When clicked to show filters, filters are displayed", () => {
// Arrange
const wrapper = mount(<Calendar showFilters={false} />);
// Act
wrapper.find("button").simulate("click");
// Assert
expect(wrapper.text().includes("Choose Filter"));
// Ось як користувач підійде до цього елемента: за текстом
});
test("Shallow/mocked approach: When clicked to show filters, filters are displayed", () => {
// Arrange
const wrapper = shallow(<Calendar showFilters={false} title="Choose Filter" />);
// Act
wrapper
.find("filtersPanel")
.instance()
.showFilters();
// Доторкніться до внутрішніх елементів, обійдіть інтерфейс і викликайте метод. Підхід білого ящика
// Assert
expect(wrapper.find("Filter").props()).toEqual({ title: "Choose Filter" });
// що, якщо ми змінимо ім’я пропу або не передамо нічого відповідного?
});
⚪ ️ 3.4 Не спіть, використовуйте вбудовану підтримку фреймворків для асинхронних подій. Також спробуйте прискорити процес
✅ Роби: У багатьох випадках час завершення блоку, що тестується, просто невідомий (наприклад, анімація призупиняє появу елемента) - у такому випадку уникайте сплячого режиму (наприклад, setTimeOut) і віддайте перевагу більш детермінованим методам, які пропонують більшість платформ. Деякі бібліотеки дозволяють очікувати виконання операцій (наприклад, Cypress cy.request('url')), інші надають API для очікування, як @testing-library/dom method wait(expect(element)). Іноді більш елегантним способом є заглушка повільного ресурсу, наприклад API, а потім, коли момент відповіді стане детермінованим, компонент може бути явно повторно відтворений. Якщо залежно від зовнішнього компонента, який спить, може виявитися корисним поспішити годинник. Сон — це шаблон, якого слід уникати, оскільки він змушує ваш тест бути повільним або ризикованим (якщо ви чекаєте занадто короткий період). Щоразу, коли сплячий режим і опитування неминучі, а платформа тестування не підтримує, деякі бібліотеки npm, як-от wait-for-expect, можуть допомогти з напів - детерміноване рішення
❌ Інакше: При тривалому сні тести будуть на порядок повільніше. Під час спроби переходу в режим сну для невеликих чисел тест не вдасться, якщо тестований пристрій не відповів своєчасно. Отже, це зводиться до компромісу між нестабільністю та поганою продуктивністю
✏ Приклади коду
👏 Приклад правильного виконання: API E2E, який вирішується лише після виконання асинхронних операцій (Cypress)
// Використання Cypress
cy.get("#show-products").click(); // Навігація
cy.wait("@products"); // зачекайте, поки з'явиться маршрут
// цей рядок буде виконано лише тоді, коли маршрут буде готовий
// @testing-library/dom
test("movie title appears", async () => {
// елемент спочатку відсутній...
// чекати появи
await wait(() => {
expect(getByText("the lion king")).toBeInTheDocument();
});
// дочекатися появи і повернути елемент
const movie = await waitForElement(() => getByText("the lion king"));
});
test("movie title appears", async () => {
// елемент спочатку відсутній...
// спеціальна логіка очікування (застереження: спрощено, без часу очікування)
const interval = setInterval(() => {
const found = getByText("the lion king");
if (found) {
clearInterval(interval);
expect(getByText("the lion king")).toBeInTheDocument();
}
}, 100);
// дочекатися появи і повернути елемент
const movie = await waitForElement(() => getByText("the lion king"));
});
✅ Роби: Застосуйте деякий активний монітор, який гарантує оптимізацію завантаження сторінки в реальній мережі - це стосується будь-яких проблем з UX, як-от повільне завантаження сторінки або немініфікований пакет. Ринок інструментів перевірки не короткий: такі базові інструменти, як pingdom, AWS CloudWatch, gcp StackDriver /) можна легко налаштувати, щоб стежити за тим, чи працює сервер і чи відповідає він відповідно до прийнятної угоди про рівень обслуговування. Це лише дряпає поверхню того, що може піти не так, тому краще вибрати інструменти, які спеціалізуються на інтерфейсі (наприклад, lighthouse, pagespeed) і виконувати детальніший аналіз. Слід зосередитися на симптомах, показниках, які безпосередньо впливають на UX, як-от час завантаження сторінки, [важливий малюнок](https://scotch.io/courses/10-web-performance-audit-tips-for-your-next- billion-users-in-2018/fmp-first-meaningful-paint), час, поки сторінка не стане інтерактивною (TTI). Крім того, можна також спостерігати за технічними причинами, такими як забезпечення стиснення вмісту, час до першого байта, оптимізація зображень, забезпечення розумного розміру DOM, SSL та багато інших. Бажано мати ці багаті монітори як під час розробки, як частину CI, так і, що найголовніше, - 24x7 на робочих серверах/CDN
❌ Інакше: Мабуть, прикро усвідомлювати, що після такої ретельної роботи над створенням інтерфейсу користувача, проходження 100% функціональних тестів і складного об’єднання – UX жахливий і повільний через неправильну конфігурацію CDN
✏ Приклади коду
✅ Роби: Під час кодування основних тестів (не тестів E2E) уникайте залучення будь-яких ресурсів, які знаходяться поза межами вашої відповідальності та контролю, як-от серверний API, і використовуйте натомість заглушки (тобто test double). На практиці замість реальних мережевих викликів API використовуйте якусь тестову подвійну бібліотеку (наприклад, Sinon, Test doubles тощо) для заглушки відповіді API. Основною перевагою є запобігання нестабільності — API тестування або проміжної обробки за визначенням не є дуже стабільними і час від часу не проходять ваші тести, хоча ВАШ компонент поводиться нормально (виробниче середовище не призначене для тестування, і зазвичай воно гальмує запити). Це дозволить симулювати різні дії API, які повинні керувати поведінкою вашого компонента, наприклад, коли дані не знайдено або випадок, коли API видає помилку. І останнє, але не менш важливе, мережеві виклики значно сповільнюють тести
❌ Інакше: The average test runs no longer than few ms, a typical API call last 100ms>, this makes each test ~20x slower
✏ Приклади коду
// unit under test
export default function ProductsList() {
const [products, setProducts] = useState(false);
const fetchProducts = async () => {
const products = await axios.get("api/products");
setProducts(products);
};
useEffect(() => {
fetchProducts();
}, []);
return products ? <div>{products}</div> : <div data-test-id="no-products-message">No products</div>;
}
// test
test("When no products exist, show the appropriate message", () => {
// Arrange
nock("api")
.get(`/products`)
.reply(404);
// Act
const { getByTestId } = render(<ProductsList />);
// Assert
expect(getByTestId("no-products-message")).toBeTruthy();
});
✅ Роби: AХоча E2E (наскрізне) зазвичай означає тестування лише інтерфейсу користувача за допомогою справжнього браузера (див. [пункт 3.6](https://github.com/goldbergyoni/javascript-testing-best-practices#-%EF%B8% 8F-36-stub-flaky-and-slow-resources-like-backend-apis)), для інших вони означають тести, які розтягують всю систему, включаючи справжній сервер. Останній тип тестів є дуже цінним, оскільки вони охоплюють помилки інтеграції між інтерфейсом і сервером, які можуть виникнути через неправильне розуміння схеми обміну. Вони також є ефективним методом для виявлення проблем міжсервісної інтеграції (наприклад, мікросервіс A надсилає неправильне повідомлення мікросервісу B) і навіть для виявлення збоїв розгортання – для тестування E2E не існує таких дружніх і зрілих інтерфейсів, як UI. такі фреймворки, як Cypress і Puppeteer. Недоліком таких тестів є висока вартість налаштування середовища з такою кількістю компонентів і, здебільшого, їхня крихкість — з огляду на 50 мікросервісів, навіть якщо один зазнає невдачі, тоді зазнає невдачі весь E2E. З цієї причини ми повинні використовувати цю техніку економно і, можливо, мати 1-10 таких і не більше. Тим не менш, навіть невелика кількість тестів E2E, швидше за все, виявить тип проблем, на які вони націлені – помилки розгортання та інтеграції. Бажано запускати їх у середовищі, схожому на виробництво
❌ Інакше: Інтерфейс користувача може багато вкладати в тестування своєї функціональності, але дуже пізно зрозуміє, що корисне навантаження (схема даних, з якою має працювати інтерфейс користувача) сильно відрізняється від очікуваного.
✅ Роби: У тестах E2E, які включають справжню серверну частину та покладаються на дійсний маркер користувача для викликів API, ізолювати тест до рівня, на якому користувач створюється та входить у систему під час кожного запиту, не виправдовується. Замість цього увійдіть лише один раз перед початком виконання тестів (тобто хук before-all), збережіть маркер у локальному сховищі та повторно використовуйте його для запитів. Схоже, це порушує один із основних принципів тестування — підтримувати тест автономним без зв’язку ресурсів. Хоча це обґрунтоване занепокоєння, у тестах E2E продуктивність є ключовою проблемою, і створення 1-3 запитів API перед початком кожного окремого тесту може призвести до жахливого часу виконання. Повторне використання облікових даних не означає, що тести повинні діяти з тими самими записами користувачів. Якщо ви покладаєтеся на записи користувачів (наприклад, історію платежів тестового користувача), переконайтеся, що створили ці записи як частину тесту та не повідомляли про їх існування іншим тестам. Також пам’ятайте, що бекенд може бути підробленим – якщо ваші тести зосереджені на інтерфейсі, можливо, краще ізолювати його та заглушити бекенд API (див. [пункт 3.6](https://github.com/goldbergyoni/javascript-testing- best-practices#-%EF%B8%8F-36-stub-flaky-and-slow-resources-like-backend-apis)).
❌ Інакше: Надано 200 тестів і припущення, що login=100ms=20 секунд лише для входу знову і знову
✏ Приклади коду
let authenticationToken;
// відбувається перед виконанням УСІХ тестів
before(() => {
cy.request('POST', 'http://localhost:3000/login', {
username: Cypress.env('username'),
password: Cypress.env('password'),
})
.its('body')
.then((responseFromLogin) => {
authenticationToken = responseFromLogin.token;
})
})
// відбувається перед КОЖНИМ тестом
beforeEach(setUser => {
cy.visit('/home', {
onBeforeLoad (win) {
win.localStorage.setItem('token', JSON.stringify(authenticationToken))
},
})
})
✅ Роби: Для моніторингу виробництва та перевірки працездатності під час розробки запустіть єдиний тест E2E, який відвідує всі/більшість сторінок сайту та гарантує, що ніхто не зламався. Цей тип тесту забезпечує високу окупність інвестицій, оскільки його дуже легко писати та підтримувати, але він може виявити будь-які збої, включаючи функціональні проблеми, проблеми з мережею та розгортання. Інші стилі перевірки диму та працездатності не такі надійні та вичерпні — деякі оперативні команди просто перевіряють домашню сторінку (виробництво) або розробники, які запускають багато інтеграційних тестів, які не виявляють проблем з пакуванням і браузером. Само собою зрозуміло, що димовий тест не замінює функціональні тести, а лише служить швидким детектором диму
❌ Інакше: Все може здатися ідеальним, усі тести пройшли, перевірка робочого стану також позитивна, але платіжний компонент мав деякі проблеми з упаковкою, і лише маршрут /Payment не відображається
✏ Приклади коду
it("When doing smoke testing over all page, should load them all successfully", () => {
// Smoke тест на всіх сторінках
// використовуючи будь-який пакет E2E
cy.visit("https://mysite.com/home");
cy.contains("Home");
cy.visit("https://mysite.com/Login");
cy.contains("Login");
cy.visit("https://mysite.com/About");
cy.contains("About");
});
✅ Роби: Окрім підвищення надійності додатка, тести надають ще одну привабливу можливість – слугувати живою документацією додатка. Оскільки тести за своєю суттю розмовляють менш технічною мовою та мовою продукту/UX, використання правильних інструментів може служити артефактом спілкування, який значною мірою зближує всіх колег – розробників та їхніх клієнтів. Наприклад, деякі фреймворки дозволяють виражати потік і очікування (тобто план тестування) за допомогою зрозумілої людині мови, щоб будь-яка зацікавлена сторона, включаючи менеджерів із продуктів, могла читати, затверджувати та співпрацювати над тестами, які щойно стали актуальним документом вимог. Цю техніку також називають «приймальним тестом», оскільки вона дозволяє клієнту визначити свої критерії прийнятності простою мовою. Це BDD (тестування, кероване поведінкою) у чистому вигляді. Одним із популярних фреймворків, які це дозволяють, є Cucumber, який має смак JavaScript, див. приклад нижче. Інша схожа, але інша можливість, StoryBook, дозволяє виставляти компоненти інтерфейсу користувача як графічний каталог, де можна переглядати різні стани кожного компонента (наприклад, візуалізувати сітку без фільтрів). , візуалізуйте цю сітку з кількома рядками або без жодного тощо), подивіться, як це виглядає та як активувати цей стан - це також може сподобатися спеціалістам із продуктів, але здебільшого служить живою документацією для розробників, які використовують ці компоненти.
❌ Інакше: Після інвестування найкращих ресурсів у тестування просто шкода не використати ці інвестиції та отримати велику цінність
✏ Приклади коду
// ось як можна описати тести за допомогою Cucumber: проста мова, яка дозволяє будь-кому розуміти та співпрацювати
Feature: Twitter new tweet
I want to tweet something in Twitter
@focus
Scenario: Tweeting from the home page
Given I open Twitter home
Given I click on "New tweet" button
Given I type "Hello followers!" in the textbox
Given I click on "Submit" button
Then I see message "Tweet saved"
✅ Роби: Налаштуйте автоматичні інструменти для створення скріншотів інтерфейсу користувача, коли представлені зміни, і виявлення візуальних проблем, як-от накладення вмісту або порушення. Це гарантує, що не тільки правильні дані підготовлені, але й користувач може їх зручно переглядати. Ця техніка не є широко поширеною, наше мислення щодо тестування схиляється до функціональних тестів, але користувач відчуває візуальні ефекти, а з такою кількістю типів пристроїв дуже легко не помітити неприємну помилку інтерфейсу користувача. Деякі безкоштовні інструменти можуть надати основи — створювати та зберігати знімки екрана для огляду очей людини. Хоча цього підходу може бути достатньо для невеликих додатків, він має недоліки, як і будь-яке інше ручне тестування, яке потребує людської праці щоразу, коли щось змінюється. З іншого боку, досить складно автоматично виявляти проблеми з інтерфейсом користувача через відсутність чіткого визначення – тут спрацьовує поле «Візуальна регресія» та розв’язує цю загадку, порівнюючи старий інтерфейс користувача з останніми змінами та виявляючи відмінності. Деякі OSS/безкоштовні інструменти можуть надавати деякі з цих функцій (наприклад, wraith, PhantomCSS, але може стягувати значний час налаштування.Комерційна лінія інструментів (наприклад, Applitools, [Percy.io](https ://percy.io/)) робить крок далі, згладжуючи інсталяцію та розширюючи такі функції, як інтерфейс користувача користувача, сповіщення, інтелектуальне захоплення шляхом усунення «візуального шуму» (наприклад, реклами, анімації) і навіть аналіз першопричини DOM /CSS зміни, які призвели до проблеми
❌ Інакше: Наскільки якісною є сторінка з вмістом, яка відображає чудовий вміст (пройдено 100% тестів), завантажується миттєво, але половина області вмісту прихована?
✏ Приклади коду
👏 Приклад правильного виконання: Налаштування wraith для захоплення та порівняння знімків інтерфейсу користувача
# Add as many domains as necessary. Key will act as a label
domains:
english: "http://www.mysite.com"
# Type screen widths below, here are a couple of examples
screen_widths:
- 600
- 768
- 1024
- 1280
# Type page URL paths below, here are a couple of examples
paths:
about:
path: /about
selector: '.about'
subscribe:
selector: '.subscribe'
path: /subscribe
👏 Приклад правильного виконання: Використання Applitools для порівняння знімків та інших розширених функцій
import * as todoPage from "../page-objects/todo-page";
describe("visual validation", () => {
before(() => todoPage.navigate());
beforeEach(() => cy.eyesOpen({ appName: "TAU TodoMVC" }));
afterEach(() => cy.eyesClose());
it("should look good", () => {
cy.eyesCheckWindow("empty todo list");
todoPage.addTodo("Clean room");
todoPage.addTodo("Learn javascript");
cy.eyesCheckWindow("two todos");
todoPage.toggleTodo(0);
cy.eyesCheckWindow("mark as completed");
});
});
✅ Роби: Мета тестування полягає в тому, щоб отримати достатню впевненість для швидкого просування; очевидно, чим більше коду тестується, тим впевненішою може бути команда. Покриття — це показник того, скільки рядків коду (і розгалужень, операторів тощо) охоплено тестами. То скільки вистачить? 10–30% — це, очевидно, занадто мало, щоб отримати будь-яке уявлення про правильність збірки, з іншого боку, 100% — це дуже дорого й може перемістити вашу увагу з критичних шляхів на екзотичні куточки коду. Довга відповідь полягає в тому, що це залежить від багатьох факторів, таких як тип застосування —«якщо ви створюєте наступне покоління Airbus A380, 100% є обов’язковим, для веб-сайту з мультфільмами 50% може бути забагато. Хоча більшість ентузіастів тестування стверджують, що правильний поріг охоплення є контекстним, більшість із них також згадує число 80% як правило ([Fowler: "in the upper 80s or 90s"](https://martinfowler. com/bliki/TestCoverage.html)), який, імовірно, повинен задовольнити більшість програм.
Поради щодо впровадження: Можливо, ви захочете налаштувати безперервну інтеграцію (CI), щоб мати поріг покриття (Jest link) і зупинити збірку що не відповідає цьому стандарту (також можна налаштувати порогове значення для кожного компонента, див. приклад коду нижче). На додаток до цього, подумайте про виявлення зменшення покриття збірки (коли нещодавно зафіксований код має менше покриття) — це підштовхне розробників збільшити або принаймні зберегти кількість перевіреного коду. Загалом, охоплення — це лише один показник, кількісний, якого недостатньо, щоб визначити надійність вашого тестування. І його також можна обдурити, як показано в наступних пунктах
❌ Інакше: Впевненість і цифри йдуть пліч-о-пліч, навіть не знаючи, що ви перевірили більшу частину системи -також буде певний страх, і страх сповільнить вас
✏ Приклади коду
✅ Роби: Деякі проблеми непомічені, і їх дуже важко знайти за допомогою традиційних інструментів. Насправді це не помилки, а скоріше дивовижна поведінка програми, яка може мати серйозні наслідки. Наприклад, часто деякі області коду ніколи або рідко викликаються — ви думали, що клас «PricingCalculator» завжди встановлює ціну продукту, але виявилося, що він насправді ніколи не викликається, хоча ми маємо 10000 продуктів у БД і багато продажів… Покриття коду звіти допомагають зрозуміти, чи працює програма так, як ви думаєте. Окрім цього, він також може підкреслити, які типи коду не перевірено —«інформація про те, що 80% коду перевірено, не говорить про те, чи охоплено критичні частини. Створювати звіти легко — просто запустіть свою програму в робочому стані або під час тестування з відстеженням покриття, а потім перегляньте кольорові звіти, у яких показано, як часто викликається кожна область коду. Якщо ви не поспішаєте зазирнути в ці дані — ви можете знайти деякі проблеми
❌ Інакше: Якщо ви не знаєте, які частини вашого коду залишилися необробленими, ви не знаєте, звідки можуть виникнути проблеми
✏ Приклади коду
На основі реального сценарію, коли ми відстежували використання нашої програми в QA та знаходили цікаві шаблони входу (Підказка: кількість помилок входу непропорційна, щось явно не так. Нарешті виявилося, що якась помилка інтерфейсу постійно вражає серверний API входу)
✅ Роби: Традиційний показник покриття часто брехня: він може показати вам 100% покриття коду, але жодна з ваших функцій, навіть жодна, не повертає правильну відповідь. Як так? він просто вимірює, які рядки коду відвідав тест, але не перевіряє, чи тести справді щось перевіряли — заявлено для правильної відповіді. Як хтось, хто подорожує у справах і показує штампи в паспорті —«це не доводить жодної роботи, лише те, що він відвідав кілька аеропортів і готелів.
Тестування на основі мутацій тут, щоб допомогти, вимірюючи кількість коду, який був фактично ПРОТЕСТОВАНИЙ, а не просто ВІДВІДАНИЙ. Stryker — це бібліотека JavaScript для тестування на мутації, реалізація якої дуже гарна:
(1) він навмисно змінює код і «підсаджує помилки». Наприклад, код newOrder.price===0 стає newOrder.price!=0. Ці «помилки» називаються мутаціями
(2) він запускає тести, якщо всі вдаються, тоді у нас є проблема —«тести не виконали своєї мети виявлення помилок, мутації так звані вижили. Якщо тести провалилися, то чудово, мутації були вбиті.
Знання того, що всі або більшість мутацій були знищені, дає набагато більшу впевненість, ніж традиційне покриття, а час налаштування подібний
❌ Інакше: Ви будете обдурені, якщо повірите, що 85% покриття означає, що ваш тест виявить помилки в 85% вашого коду
✏ Приклади коду
function addNewOrder(newOrder) {
logger.log(`Adding new order ${newOrder}`);
DB.save(newOrder);
Mailer.sendMail(newOrder.assignee, `A new order was places ${newOrder}`);
return { approved: true };
}
it("Test addNewOrder, don't use such test names", () => {
addNewOrder({ assignee: "John@mailer.com", price: 120 });
}); // Запускає 100% покриття коду, але нічого не перевіряє
✅ Роби: Набір плагінів ESLint створено спеціально для перевірки шаблонів коду тестів і виявлення проблем. Наприклад, eslint-plugin-mocha попереджатиме, коли тест написаний на глобальному рівні (а не син оператора describe()) або коли тести skipped, що може призвести до хибного переконання, що всі тести успішно пройдені. Подібним чином eslint-plugin-jest може, наприклад, попередити, коли тест взагалі не має тверджень (нічого не перевіряє)
❌ Інакше: Побачивши 90% охоплення коду та 100% зелених тестів, ваше обличчя буде широко посміхатися, доки ви не зрозумієте, що багато тестів нічого не стверджують, а багато наборів тестів просто пропущено. Сподіваюся, ви нічого не розгорнули на основі цього помилкового спостереження
✏ Приклади коду
👎 Приклад антишаблону: Тестовий приклад, повний помилок, на щастя, усі вони виявлені за допомогою лінтерів (Linters)
describe("Too short description", () => {
const userToken = userService.getDefaultToken() // *error:no-setup-in-describe, use hooks (sparingly) instead
it("Some description", () => {});//* error: valid-test-description. Must include the word "Should" + at least 5 words
});
it.skip("Test name", () => {// *error:no-skipped-tests, error:error:no-global-tests. Put tests only under describe or suite
expect("somevalue"); // error:no-assert
});
it("Test name", () => {*//error:no-identical-title. Assign unique titles to tests
});
✅ Роби: Linters — це безкоштовний обід, після 5-хвилинного налаштування ви безкоштовно отримуєте автопілот, який охороняє ваш код і виявляє значні проблеми під час введення. Пройшли ті часи, коли розмови стосувалися косметики (без крапок з комою!). Сьогодні Linters може виявляти серйозні проблеми, як-от помилки, які неправильно видаються та втрачають інформацію. На додаток до вашого базового набору правил (наприклад, стандарт ESLint або [стиль Airbnb](https://www.npmjs.com/package /eslint-config-airbnb)), подумайте про включення деяких спеціалізованих лінтерів, таких як eslint-plugin-chai-expect, які можуть виявляти тести без твердження, eslint-plugin-promise може виявляти обіцянки без вирішення (ваш код ніколи не продовжиться), eslint-plugin-security, який може виявити активні регулярні вирази, які можуть бути використані для атак DOS, і eslint-plugin-you-dont-need-lodash-underscore здатний викликати тривогу, коли код використовує методи службової бібліотеки, які є частиною V8 основні методи, такі як Lodash._map(…)
❌ Інакше: Уявіть собі дощовий день, коли ваша продуктивність продовжує давати збої, але журнали не відображають трасування стека помилок. Що трапилось? Ваш код помилково викинув об’єкт без помилки, і трасування стека було втрачено, що є вагомою причиною для того, щоб битися головою об цегляну стіну. 5-хвилинне налаштування Linter може виявити цю TYPO і врятувати ваш день
✏ Приклади коду
✅ Роби: Використовуєте CI з блискучою перевіркою якості, як-от тестування, лінтування, перевірка вразливостей тощо? Допоможіть розробникам запустити цей конвеєр також локально, щоб отримати миттєвий відгук і скоротити [цикл зворотного зв’язку](https://www.gocd.org/2016/03/15/are-you-ready-for-continuous-delivery-part-2 -петлі зворотного зв'язку/). чому ефективний процес тестування складається з багатьох ітераційних циклів: (1) тестування -> (2) зворотній зв'язок -> (3) рефакторинг. Чим швидший зворотній зв’язок, тим більше ітерацій покращення може виконати розробник для кожного модуля та покращити результати. З іншого боку, коли зворотній зв’язок надходить із запізненням, менше ітерацій покращення може бути упаковано в один день, команда може вже перейти до іншої теми/завдання/модуля та не готова вдосконалювати цей модуль.
На практиці деякі постачальники CI (Приклад: CircleCI local CLI) дозволяють запускати конвеєр локально. Деякі комерційні інструменти, як-от wallaby, надають дуже цінну інформацію для тестування як прототип розробника (без зв’язку). Крім того, ви можете просто додати сценарій npm до package.json, який запускає всі команди якості (наприклад, test, lint, vulnerabilities) — використовуйте такі інструменти, як concurrently для розпаралелювання і ненульовий код виходу, якщо один із інструментів вийшов з ладу. Тепер розробник має просто викликати одну команду — наприклад. «Якість виконання npm» — щоб отримати миттєвий відгук. Також подумайте про переривання коміту, якщо перевірка якості не вдалася за допомогою githook (husky може допомогти)
❌ Інакше: Коли якісні результати надходять наступного дня після коду, тестування не стає плавною частиною розробки, а формальним артефактом.
✏ Приклади коду
👏 Приклад правильного виконання: Сценарії npm, які виконують перевірку якості коду, усі виконуються паралельно на вимогу або коли розробник намагається надіслати новий код
"scripts": {
"inspect:sanity-testing": "mocha **/**--test.js --grep \"sanity\"",
"inspect:lint": "eslint .",
"inspect:vulnerabilities": "npm audit",
"inspect:license": "license-checker --failOn GPLv2",
"inspect:complexity": "plato .",
"inspect:all": "concurrently -c \"bgBlue.bold,bgMagenta.bold,yellow\" \"npm:inspect:quick-testing\" \"npm:inspect:lint\" \"npm:inspect:vulnerabilities\" \"npm:inspect:license\""
},
"husky": {
"hooks": {
"precommit": "npm run inspect:all",
"prepush": "npm run inspect:all"
}
}
✅ Роби: Наскрізне тестування (e2e) є головною проблемою кожного конвеєра CI —«створення ідентичного ефемерного робочого дзеркала на льоту з усіма пов’язаними хмарними службами може бути виснажливим і дорогим. Пошук найкращого компромісу — ваша гра: Docker-compose дозволяє створювати ізольоване докеризоване середовище з ідентичними контейнерами за допомогою одного простого текстового файлу, але технологія підтримки (наприклад, мережа, модель розгортання) відрізняється з реальних виробництв. Ви можете поєднати його з ‘AWS Local’, щоб працювати із заглушкою справжніх служб AWS. Якщо ви використовуєте безсерверні кілька фреймворків, як-от безсерверні, і AWS SAM дозволяє локальний виклик коду FaaS.
Величезна екосистема Kubernetes ще має формалізувати стандартний зручний інструмент для локального та CI-дзеркалювання, хоча часто запускається багато нових інструментів. Одним із підходів є запуск «мінімізованого Kubernetes» за допомогою таких інструментів, як Minikube і MicroK8s, які нагадують справжній річ лише з меншими накладними витратами. Інший підхід полягає в тестуванні на віддаленому «реальному Kubernetes». Деякі постачальники CI (наприклад, Codefresh) мають власну інтеграцію з середовищем Kubernetes і дозволяють легко запускати конвеєр CI через реальний інші дозволяють створювати спеціальні сценарії для віддаленого Kubernetes.
❌ Інакше: Використання різних технологій для виробництва та тестування вимагає підтримки двох моделей розгортання та роз’єднує розробників і команду операцій
✏ Приклади коду
👏 Приклад: конвеєр CI, який генерує кластер Kubernetes на льоту (Авторство: Dynamic-environments Kubernetes)
deploy:
stage: deploy
image: registry.gitlab.com/gitlab-examples/kubernetes-deploy
script:
- ./configureCluster.sh $KUBE_CA_PEM_FILE $KUBE_URL $KUBE_TOKEN
- kubectl create ns $NAMESPACE
- kubectl create secret -n $NAMESPACE docker-registry gitlab-registry --docker-server="$CI_REGISTRY" --docker-username="$CI_REGISTRY_USER" --docker-password="$CI_REGISTRY_PASSWORD" --docker-email="$GITLAB_USER_EMAIL"
- mkdir .generated
- echo "$CI_BUILD_REF_NAME-$CI_BUILD_REF"
- sed -e "s/TAG/$CI_BUILD_REF_NAME-$CI_BUILD_REF/g" templates/deals.yaml | tee ".generated/deals.yaml"
- kubectl apply --namespace $NAMESPACE -f .generated/deals.yaml
- kubectl apply --namespace $NAMESPACE -f templates/my-sock-shop.yaml
environment:
name: test-for-ci
✅ Роби: Якщо все зроблено правильно, тестування стане вашим другом цілодобово та без вихідних, що надає майже миттєвий відгук. На практиці виконання 500 обмежених ЦП модульних тестів в одному потоці може тривати занадто довго. На щастя, сучасні тестувальники та платформи CI (як-от Jest, AVA і [розширення Mocha](https ://github.com/yandex/mocha-parallel-tests)) можна розпаралелити тест на кілька процесів і значно покращити час зворотного зв’язку. Деякі постачальники CI також розпаралелюють тести між контейнерами (!), що ще більше скорочує цикл зворотного зв’язку. Незалежно від того, локально над кількома процесами чи над деяким хмарним CLI з використанням кількох машин — розпаралелювання попиту, зберігаючи автономність тестів, оскільки кожен може виконуватися на різних процесах
❌ Інакше: Отримання результатів тестування через 1 годину після введення нового коду, оскільки ви вже кодуєте наступні функції, є чудовим рецептом для того, щоб зробити тестування менш актуальним
✏ Приклади коду
👏 Приклад правильного виконання: Mocha parallel & Jest легко випереджають традиційну Mocha завдяки розпаралелюванню тестування (Авторство: JavaScript Test-Runners Benchmark)
✅ Роби: Проблеми ліцензування та плагіату, ймовірно, не є вашою головною проблемою зараз, але чому б також не поставити галочку в цьому полі через 10 хвилин? Купа пакетів npm, таких як перевірка ліцензії і перевірка на плагіат ( рекламний ролик із безкоштовним планом) можна легко вставити у ваш конвеєр CI та перевірити на наявність таких проблем, як залежності з обмежувальними ліцензіями або код, який було скопійовано з Stack Overflow і, очевидно, порушує деякі авторські права
❌ Інакше: Ненавмисно розробники можуть використати пакети з невідповідними ліцензіями або скопіювати комерційний код і зіткнутися з юридичними проблемами
✏ Приклади коду
//install license-checker in your CI environment or also locally
npm install -g license-checker
//ask it to scan all licenses and fail with exit code other than 0 if it found unauthorized license. The CI system should catch this failure and stop the build
license-checker --summary --failOn BSD
✅ Роби: Навіть найавторитетніші залежності, такі як Express, мають відомі вразливості. Це можна легко приборкати за допомогою інструментів спільноти, таких як npm audit, або комерційних інструментів, таких як [snyk](https:// snyk.io/) (пропонуємо також безкоштовну версію спільноти). Обидва можна викликати з вашого КІ під час кожної збірки
❌ Інакше: Щоб захистити ваш код від уразливостей без спеціальних інструментів, потрібно постійно стежити за публікаціями в Інтернеті про нові загрози. Досить нудно
✅ Роби: Yarn і npm останнє представлення package-lock.json поставило серйозну проблему (дорога в пекло вимощена благими намірами) — тепер за замовчуванням пакунки більше не отримують оновлення. Навіть команда, яка виконує багато свіжих розгортань за допомогою «встановлення npm» і «оновлення npm», не отримає нових оновлень. У кращому випадку це призводить до неповноцінних версій пакунків або до вразливого коду в гіршому. Команди тепер покладаються на добру волю та пам’ять розробників, щоб вручну оновити package.json або використовувати інструменти наприклад, ncu вручну. Надійнішим способом може бути автоматизація процесу отримання найнадійніших версій залежностей, хоча немає універсальних рішень, але є два можливі шляхи автоматизації:
(1) CI може давати збій збіркам із застарілими залежностями — використанням таких інструментів, як ‘npm outdated’ або ‘npm-check-updates (ncu)’ . Це змусить розробників оновити залежності.
(2) Використовуйте комерційні інструменти, які сканують код і автоматично надсилають запити на отримання з оновленими залежностями. Залишається одне цікаве питання: якою має бути політика оновлення залежностей —«оновлення кожного патча створює надто багато накладних витрат, оновлення безпосередньо під час випуску основного може вказувати на нестабільну версію (багато пакетів виявляються вразливими в перші дні після випуску, перегляньте інцидент eslint-scope).
Ефективна політика оновлення може дозволити певний «період набуття прав» — нехай код відстає від @latest на деякий час і версії, перш ніж вважати локальну копію застарілою (наприклад, локальна версія — 1.3.1, а версія сховища — 1.3.8)
❌ Інакше: У вашому виробництві працюватимуть пакунки, які були явно позначені їх автором як ризиковані
✏ Приклади коду
👏 Приклад: ncu можна використовувати вручну або в конвеєрі CI, щоб визначити, наскільки код відстає від останніх версій
✅ Роби: Ця публікація зосереджена на порадах щодо тестування, які пов’язані з Node JS або, принаймні, можуть бути представлені на прикладі Node JS. Однак у цьому розділі згруповано кілька добре відомих порад, не пов’язаних з Node
- Use a declarative syntax. This is the only option for most vendors but older versions of Jenkins allows using code or UI
- Opt for a vendor that has native Docker support
- Fail early, run your fastest tests first. Create a ‘Smoke testing’ step/milestone that groups multiple fast inspections (e.g. linting, unit tests) and provide snappy feedback to the code committer
- Make it easy to skim-through all build artifacts including test reports, coverage reports, mutation reports, logs, etc
- Create multiple pipelines/jobs for each event, reuse steps between them. For example, configure a job for feature branch commits and a different one for master PR. Let each reuse logic using shared steps (most vendors provide some mechanism for code reuse)
- Never embed secrets in a job declaration, grab them from a secret store or from the job’s configuration
- Explicitly bump version in a release build or at least ensure the developer did so
- Build only once and perform all the inspections over the single build artifact (e.g. Docker image)
- Test in an ephemeral environment that doesn’t drift state between builds. Caching node_modules might be the only exception
❌ Інакше: Ви пропустите роки мудрості
✅ Роби: Перевірка якості пов’язана з інтуїцією, чим більше ви охопите, тим більше вам пощастить у виявленні проблем на ранній стадії. Під час розробки пакетів для багаторазового використання або запуску виробництва для кількох клієнтів із різними конфігураціями та версіями Node, CI має запустити конвеєр тестів для всіх перестановок конфігурацій. Наприклад, якщо припустити, що ми використовуємо MySQL для одних клієнтів, а Postgres для інших —«деякі постачальники CI підтримують функцію під назвою «Матриця», яка дозволяє виконувати тестування всіх перестановок MySQL, Postgres і кількох версій Node, таких як 8, 9 і 10. Це робиться лише за допомогою конфігурації без будь-яких додаткових зусиль (за умови, що у вас є тестування чи будь-які інші перевірки якості). Інші КІ, які не підтримують Matrix, можуть мати розширення або налаштування, щоб дозволити це
❌ Інакше: Отже, після всієї цієї важкої роботи з написання тестування, ми дозволимо помилкам прокрадатися лише через проблеми з конфігурацією?
✏ Приклади коду
👏 Приклад: Використання визначення збірки Travis (постачальник CI) для запуску одного тесту на кількох версіях Node
language: node_js
node_js:
- "7"
- "6"
- "5"
- "4"
install:
- npm install
script:
- npm run test
Роль: Письменник
Опис: Я незалежний консультант, який працює з компаніями зі списку Fortune 500 і гаражними стартапами над вдосконаленням їхніх додатків JS і Node.js. Більше ніж будь-яка інша тема мене захоплює, і я прагну оволодіти мистецтвом тестування. Я також автор найкращих практик Node.js
📗 Онлайн-курс: Сподобався цей посібник і ви бажаєте вдосконалити свої навички тестування? Відвідайте мій комплексний курс Тестування Node.js і JavaScript від А до Я
Follow:
Роль: Технічний оглядач і радник
Подбав про те, щоб переглянути, вдосконалити, відшліфувати та відшліфувати всі тексти
Опис: full-stack веб-інженер, ентузіаст Node.js та GraphQL
Роль: Concept, design and great advice
Опис: Кмітливий розробник інтерфейсу, експерт із CSS і фанат емодзі
Роль: Допомагає підтримувати роботу цього проекту та переглядає методи безпеки
Опис: Любить працювати над проектами Node.js і безпекою веб-додатків.
Дякуємо цим чудовим людям, які зробили внесок у це сховище!