Данное руководство гарантирует надежность JavaScript и Node.JS от A до Я. В качестве источника в данном руководстве используется обобщенная информация, взятая из самых надежных книг, статей и блогов, которые можно найти на рынке в данный момент.
Отправляйтесь в путешествие, которое выходит далеко за пределы базовых практик тестирования и включает в себя такие продвинутые темы, как: тестирование в рабочей среде (TIP), мутационное тестирование (mutation testing), тестирование на основе свойств (property-based testing) и многие другие профессиональные подходы. После прочтения данного руководства, ваши навыки тестирования станут намного выше среднего.
Начните с понимания общеиспользуемых способов тестирования, являющиеся основными для приложений любого уровня, а затем углубитесь в выбранную вами область: frontend/UI, backend, CI или всё вместе.
🚀 Для отработки изученных в процессе чтения навыков тестирования Вы можете использовать наш Node.js starter - Practica.js. Вы сможете использовать его как для создания нового шаблона, так и для практики с примерами кода.
- Консультант по вопросам JavaScript & Node.js
- 📗 Testing Node.js & JavaScript From A To Z - Мой полный онлайн-курс, включающий более, чем 7 часов видео, 14 типов тестирования и 40+ практических занятий
- Мой твиттер
- Следующий workshop: Verona, Italy 🇮🇹, April 20th
- 🇨🇳Китайский - Переведено Yves yao
- 🇰🇷Koрейский - Переведено Rain Byun
- 🇵🇱Польский - Переведено Michal Biesiada
- 🇪🇸Испанский - Переведено Miguel G. Sanguino
- 🇧🇷Португальский - Переведено Iago Angelim Costa Cavalcante , Douglas Mariano Valero and koooge
- 🇫🇷Французский - Переведено Mathilde El Mouktafi
- 🇯🇵Японский (черновик) - Переведено of Yuichi Yogo and ryo
- 🇹🇼Традиционный китайский - Переведено Yubin Hsu
- 🇷🇺 Русский - Переведено Alex Popov
- Хотите перевести на собственный язык? Переходите в Issues 💜
Ключевое правило для написания качественных тестов (1 пункт)
Фундамент - структура чистых тестов (12 пунктов)
Разработка эффективных Backend and Microservices тестов (13 пунктов)
Написание тестов для UI, включая тестирование компонентов и E2E тесты (11 пунктов)
Измерение качества тестов (4 пункта)
Руководство для CI в мире JS (9 пунктов)
✅ Сделать: Тесты - это не продакшн-код. Тестовый код должен быть коротким, плоским и простым, чтобы с ним было приятно работать. Он должен быть таким, чтобы взглянув на него, можно было сразу понять замысел разработчика.
Во время разработки, наш разум полностью сфокусирован на продакшн-коде. Наш интеллект полностью погружен в работу и, как правило, введение дополнительной сложности может привести к "перегрузке". Если мы попытаемся добавить еще одну комплексную систему, это может привести к торможению целого рабочего процесса, что противоречит самой идее тестирования. На практике, многие команды разработчиков пренебрегают тестированием.
Тестирование нужно рассматривать, как личного помощника, второго пилота, который за небольшую плату предоставляет бесценные услуги и пользу. Ученые утверждают, что у человека есть 2 типа мышления: тип 1 используется при выполнении простых задач, не требующих усилий, таких как вождение автомобиля по пустой дороге, и тип 2, предназначенный для сложных мыслительных процессов, как, например, решение математических уравнений. Тесты должны быть написаны так, чтобы при просмотре на код использовался 1 тип мышления, и это было похоже на простое изменение HTML-документа, а не решение математического уравнения 2 × (17 × 24).
Этого можно достичь путем тщательного выбора способов, инструментов и целей тестирования, которые будут одновременно и эффективными и "окупаемыми". Тестируйте только то, что необходимо. Старайтесь увеличить скорость разработки. Иногда даже стоит отказаться от некоторых тестов, чтобы сделать код более простым и гибким.
Большинство дальнейших советов являются производными от этого принципа.
✅ Сделать: Отчет о тестировании должен предоставлять информацию о том, удовлетворяет ли текущая версия приложения требованиям человека, который часто незнаком с кодом или забыл его: тестировщик, DevOps-инженер, который разворачивает проект, а также Вы сами через 2 года. Наилучший способ добиться этого, если тесты будут проводиться на уровне требований, а его описание состоять из 3-х частей:
(1) Что именно тестируется? Например: ProductsService.addNewProduct method
(2) При каких обстоятельствах? Например: no price is passed to the method
(3) Какой ожидаемый результат? Например: the new product is not approved
❌ Иначе: A deployment just failed, a test named “Add product” failed. Говорит ли это вам о том, в чем именно заключается сбой?
👇 Обрати внимание: Каждый пункт содержит примеры кода, а иногда и иллюстрацию к нему. Нажмите, чтобы открыть
✏ Примеры кода
//1. Тестируемый блок
describe('Products Service', function() {
describe('Add new product', function() {
//2. сценарий and 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');
});
});
});
© Читать далее...
1. Roy Osherove - Naming standards for unit tests✅ Сделать: Разделите написанные тесты согласно 3 категориям: Arrange, Act & Assert (AAA). Следование данной структуре позволит человеку, анализирующему тесты, быстрее разобраться в стратегии тестирования.
-
A - Организуйте (Arrange): В данную категорию входит настройки, которые приведут код к тому сценарию, который должен быть имитирован. Они включают в себя организация трестируемого блока, добавления записей в БД, mocking/stubbing и многое другое.
-
A - Действуйте (Act): Выполните тестируемый блок кода. Как правило, занимает 1 строку кода.
-
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 не содержат нужные assertions, которые часто повторяются при написании, то рассмотрите extending Jest matcher (Jest) или написание custom Chai plugin
❌ Иначе: При разработке будет написано меньше тестов, а ненужные тесты будут проигнорированы с помощью .skip().
✏ Примеры кода
👎 Неправильно: Чтобы просмотреть реализацию тестов, разработчик должен просмотреть весь объёмный и императивный код
test("When asking for an admin, ensure only ordered admins in results", () => {
// предположим, что мы добавили здесь "admin1", "admin2" and "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", () => {
// предположим, что мы добавили здесь 2-х администраторов
const allAdmins = getUsers({ adminOnly: true });
expect(allAdmins)
.to.include.ordered.members(["admin1", "admin2"])
.but.not.include.ordered.members(["user1"]);
});
✅ Сделать: Тестирование внутренних компонентов сопровождается большими затратами. Если ваш код/API работает корректно, целесообразно ли будет потратить следующие 3 часа на написание тестов для внутренней реализации и потом все это поддерживать? Каждый раз, когда тестируется внешнее поведение, внутренняя реализация также проверяется неявным образом. Тесты могут упасть только при возникновении определенной проблемы (например, неправильный вывод). Такой подход также называют поведенческое тестирование (behavioral testing)
. С другой стороны, если вы тестируете внутренние компоненты (white-box тестирование), ваш фокус может сместиться с планирования результата работы данного компонента на мелкие детали. Как следствие, тест может упасть из-за незначительных исправлений в коде, несмотря на то, что результат работы компонента будет удовлетворительны - это усложняет поддержку такого кода.
❌ Иначе: Ваши тесты начинают работать словно мальчик, который кричал: "Волк!", сигнализируя о ложных срабатываниях (например, A test fails because a private variable name was changed). Неудивительно, что люди вскоре начнут игнорировать CI-уведомления, пока в один прекрасный день не проигнорируют настоящую ошибку...
✏ Примеры кода
class ProductService {
// этот метод используется только внутри
// изменение имени приведет к падению тестов
calculateVATAdd(priceWithoutVAT) {
return { finalPrice: priceWithoutVAT * 1.2 };
// изменение формата результата или ключа выше приведет падению тестов
}
// public method
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).
Прежде чем использовать объекты-имитации, вы должны спросить себя: используется ли он для тестирования функциональности, которая требуется в процессе разработки?. Если нет, то это появление предпосылок white-box тестирования.
Например, если вы хотите протестировать корректную работу вашего приложения, в то время, как платежный сервис не работает, то можете поставить заглушку (stub)
на платежный сервис и вернуть несколько 'No Response', чтобы убедится в том, что тестируемый модуль возвращает правильное значение. Такой подход проверяет поведение/ответ/результат работы нашего приложения при определённых сценариях. Также можно использовать spy
для подтверждения того, что письмо было отправлено в ситуациях, когда сервис упал. Такая проверка поведения также может быть указана в техническом задании (“Send an email if payment couldn’t be saved”). C другой стороны, мокинг платежного сервиса и дальнейший его вызов с корректными типами данных говорит о том, что ваш тест предназначен для проверки внутренней реализации, которая не имеет отношения к функциональности приложения и может часто подвергаться изменениям.
❌ Иначе: Любой рефакторинг кода требует поиска всех mocks
в коде и последующего обновления. Тесты начинают вредить, нежели, чем приносить пользу
✏ Примеры кода
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();
});
👏 Правильно: spies сосредоточены на проверке требований, но в качестве побочного эффекта неизбежно затрагивают внутренние компоненты
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
✅ Делать: Часто производственные ошибки обнаруживаются при очень специфических и неожиданных входных данных - чем реалистичнее тестовые данные, тем больше шансов обнаружить ошибки на ранней стадии. Используйте специальные библиотеки, такие как Chance или Faker, для генерации псевдореальных данных, напоминающих по разнообразию и форме производственные данные. Например, такие библиотеки могут генерировать реалистичные телефонные номера, имена пользователей, кредитные карты, названия компаний и даже текст "lorem ipsum". Вы также можете создать несколько тестов (поверх модульных тестов, а не в качестве замены), которые рандомизируют поддельные данные, чтобы растянуть тестируемый модуль или даже импортировать реальные данные из производственной среды. Хотите перейти на новый уровень? Смотрите следующий пункт (тестирование на основе свойств).
❌ Иначе: Все тесты при разработке могут ложно показать зеленый цвет при использовании данных типа "Foo". Однако, все может упасть, если на вход попадет строка вида "@3e2ddsf . ##' 1 fdsfds . fds432 AAAA".
✏ Примеры кода
const addProduct = (name, price) => {
const productNameRegexNoSpace = /^\S*$/; //no white-space allowed
if (!productNameRegexNoSpace.test(name)) return false; //this path never reached due to dull input
// здесь какая-то логика
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);
// positive-false: операция прошла успешно, так как мы не пробовали использовать длинное название продукта, включающее пробелы
});
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;
// тест провалился: данные, которые мы подали на вход, сработали не так, как планировалось
// мы обнаружили ошибку
});
✅ Сделать: Обычно для тестирования выбирается несколько комбинаций входных данных. Даже когда они напоминают реальные данные (смотри пункт ‘1.6’), мы охватываем только несколько наборов входных данных (method(‘’, true, 1), method(“string” , false , 0)), Однако, в продакшене, API вызываемый с 5 параметрами, может быть вызван с тысячами различных перестановок, одна из которых может уронить весь процесс (см. Fuzz Testing). Представьте, что вы бы могли написать тест, который автоматически генерирует 1000 перестановок различных входных данных и отслеживает те данные, на которых код падает? Тестирование на основе свойств позволяет это сделать: отправляя на вход множество комбинации различных данных, вы увеличиваете вероятность случайного обнаружения ошибки. Например, при наличии метода addNewProduct(id, name, isDiscount) поддерживающие библиотеки будут вызывать этот метод со многими комбинациями (number, string, boolean), например (1, "iPhone", false), (2, "Galaxy", true). Такой тип тестирования может быть организован, используя разные тестовые фреймворки (Mocha, Jest, etc) с различными библиотеками 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");
})
));
});
});
✅ Сделать: Если существует необходимость в snapshot testing, используйте короткие снимки (i.e. 3-7 lines), которые являются частью теста (Inline Snapshot), а не во внешних файлах. Соблюдение этого правила позволит вашим тестам оставаться понятными и надежными.
С другой стороны, руководства по "классическим снимкам" призывают хранить большие файлы (например, разметку рендеринга компонента, результат API JSON) на каком-то внешнем носителе и каждый раз при выполнении теста сравнивать полученный результат с сохраненной версией. Это может привести к тому, что, тест будет привязан к огромному количеству данных (например, 1000 строк), о которых разработчик никогда не задумывался. Почему так нельзя делать? аким образом, существует 1000 причин, по которым ваш тест может не пройти - достаточно изменения одной строки, чтобы снимок стал недействительным, а это, скорее всего, будет происходить часто. Как часто? Каждый пробел, комментарий или незначительное изменение HTML/CSS. Мало того, что название теста не дает представления о том, что пошло не так, поскольку он просто проверяет, что 1000 срок кода остались неизменны, так еще и вводит разработчика в заблуждение, заставляя принимать за желаемую истину документ, который он не сможет проверить. Все это является признаками теста, который стремиться захватить сразу многое.
Стоит отметить, что иногда, такие большие снимки приемлемы - когда проверяется схема, а не данные (извлечение значений и фокус на определенном поле) или при редком изменении документа.
❌ Иначе: UI тест провалился. Код кажется правильным, а на экране отображаются идеальные пиксели. Так что же случилось? Ваше snapshot-тестирование просто обнаружило разницу между исходным документом и текущим полученным - в разметку был добавлен 1 пробел...
✏ Примеры кода
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 patterns, такой паттерн называется "таинственный гость" - что-то скрытое повлияло на результат наших тестов, но мы не знаем, что именно.
Для улучшения ситуации, мы можем извлечь повторяющиеся детали, оставляя только то, что имеет значение для теста. Как, например, в данной ситуации: transferFactory.getJSON({sender: undefined}). Здесь видно, что пустое поле отправителя является той самой причиной, которая приведет к ошибке валидации.
❌ Иначе: Копирование огромного количества строк 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) будет сложно сделать вывод о том, что пошло не так
✏ Примеры кода
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;
// если это утверждение не сработает, то результаты тестов покажут,
// что какое-то значение равно null, а об отсутствующем исключении не будет ни слова
});
it("When no product name, it throws error 400", async () => {
await expect(addNewProduct({}))
.to.eventually.throw(AppError)
.with.property("code", "InvalidInput");
});
✅ Сделать: Разные тесты должны запускаться по-разному: quick smoke, IO-less, тесты должны запускаться, когда разработчик сохраняет код или делает коммит. Сквозные тесты запускаются при попытке нового pull-request и многое другое. Этого можно достичь, если помечать тесты ключевыми словами #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 () {
// здесь логика
});
});
});
✅ Сделать: Придайте набору тестов некую структуру, чтобы любой, кто посмотрит на них, мог легко понять, что происходит (тесты - лучшая документация). Общим методом для этого является размещение как минимум двух блоков "describe" над тестами: первый - для названия тестируемого блока, а второй - для дополнительного разделения тестов, например, сценария или пользовательских категорий (см. примеры кода и скриншот ниже). Данное разделение также улучшить финальные отчеты по тестированию. Читатель сможет легко разобраться с категориями тестов, найти нужный раздел и понять, где могла возникнуть ошибка. Более того, разработчику будет легче ориентироваться в случае большого количества тестов. Существует несколько способов придать тестам структуру, которое можно найти здесь given-when-then и здесь RITE
❌ Иначе: При просмотре отчета с длинным и плоским списком тестов приходиться бегло просматривать длинные тексты, чтобы понять основные сценарии и выяснить причину неудачи определенных тестов. Рассмотрим следующий случай: если при наборе в 100 тестов 7 из них окажутся неудачными, то придется прочитать их описание, чтобы понять, как они друг с другом связаны. Однако, если существует разделение на структуры, то причина падения тестов, может быть общей для них и разработчик быстро сделает вывод о том, что стало причиной или, по крайне мере, где она находится.
✏ Примеры кода
// тестируемый блок
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, которые хорошо известны
Изучайте и практикуйте принципы TDD — они являются очень ценным инструментов для многих разработчиков, однако не пугайтесь, если они вам не подойдут. Рассмотрите возможность написания тестов до начала разработки в стиле red-green-refactor style, убедитесь, что каждый тест проверяет ровно один смысловой элемент и после того, как вы найдете ошибку, перед тем как ее исправить - напишите тест, который обнаружит эту ошибку в будущем. Пусть каждый тест упадет хотя бы один раз, прежде его цвет станет зеленым. Начните разработку модуля с написания простого и быстрого кода, удовлетворяющего тесту, а после постепенно рефакторите и доведите его до уровня продакшн-кода. Избегайте любой зависимости от окружения (файловые пути, ОС и другое).
❌ Иначе: Мудрость, которая копилась десятилетиями, пройдет мимо вас
✅ Сделать: Пирамида тестирования, несмотря на то, что ей уже более 10 лет, является отличной и актуальной моделью, которая предлагает 3 типа тестирования и влияет на стратегию тестирования большинства разработчиков. В то же время, появились методы тестирования, которые скрываются в тени данной пирамиды. Принимая во внимание все кардинальные изменения, которые наблюдались в течение последних 10 лет (микросервисы, облака, бессерверные технологии), встает вопрос: возможно ли применить одну модель тестирования для всех типов приложений? Или может быть лучше рассмотреть возможность внедрения новых практик?
Не поймите меня неправильно, в 2019 году пирамида тестирования, TDD и юнит-тесты по-прежнему являются мощной техникой и, вероятно, лучше всего подходят для многих приложений. Только, как и любая другая модель, несмотря на свою ценность, иногда она дает сбои. Например, рассмотрим IoT-приложение, которое получает множество событий типа Kafka/RabbitMQ, которые затем попадают в хранилище данных и в конечном итоге запрашиваются аналитическим пользовательским интерфейсом. Действительно ли мы должны тратить 50% бюджета на тестирование на написание модульных тестов для приложения, которое ориентировано на интеграцию и почти не имеет логики. По мере увеличения разнообразия типов приложений (боты, криптовалюты, Alexa-skills) также растут шансы найти такие сценарии, в которых пирамида тестирования не является идеальным решением.
Пришло время увеличить свое портфолио тестировщика и познакомиться еще большим количеством подходов к написанию тестов (следующие пункты содержат определенные идеи), начать использовать модели тестирования, подобные пирамиде и также научится сопоставлять типы тестирования с реальными проблемами, с которыми вы можете столкнуться ("Эй, наш API сломан, давайте напишем тестирование контрактов, ориентированное на потребителя!"). Необходимо также научиться диверсифицировать свои тесты, как инвестор, который создает портфель на основе анализа рисков - оценить, где могут возникнуть проблемы, и подобрать превентивные меры для снижения этих потенциальных рисков.
Предостережение: спор о TDD в мире программного обеспечения принимает типичный облик ложной дихотомии: одни проповедуют его повсеместное использование, другие считают, что это дьявол. Каждый, кто абсолютно уверен в чем то одном - ошибается :]
❌ Иначе: Вы можете пропустить такие инструменты, как Fuzz, Lint и мутации, которые могут принести пользу за 10 минут.
✏ Примеры кода
👏 Правильно: Синди Шридхаран предлагает большое разнообразие подходов к тестированию в своем замечательном посте ‘Testing Microservices — the same way’.
✅ Сделать: Каждый модульный тест покрывает небольшую часть приложения. Покрыть его целиком является дорогим удовольствием, в то время как сквозное тестирование легко покрывает большую часть приложения, но является нестабильным и медленным. Почему бы не применить сбалансированный подход, и не писать тесты, которые по размеру больше, чем модульные, но меньше, чем сквозное тестирование. Компонентное тестирование - это то, что вобрало в себя лучшее из двух перечисленных подходов. Одно сочетает в себе разумную производительность и возможность применения паттернов TDD, а также реалистичное и большое покрытие.
Компонентные тесты фокусируются на "единице" микросервиса, они работают с API, не имитируют ничего, что принадлежит самому микросервису (например, реальную БД или, по крайней мере, ее версию в памяти), но затыкают все, что является внешним, например, вызовы других микросервисов. Поступая таким образом, мы тестируем то, что развертываем, подходим к приложению от внешнего к внутреннему и обретаем большую уверенность за разумное время.
❌ Иначе: Вы можете потратить огромное количество времени на написание модульных тестов и обнаружить, что покрытие системы составляет всего 20%.
✏ Примеры кода
✅ Сделать: Итак, у вашего микросервиса есть несколько клиентов, и вы запускаете несколько версий сервиса из соображений совместимости (чтобы все были довольны). Затем вы изменяете какое-то поле и "бум!", какой-то важный клиент, который полагается на это поле, возмущен. Это и есть Catch-22 в мире интеграции: Для серверной стороны очень сложно учесть все многочисленные ожидания клиентов— С другой стороны, клиенты не могут провести никакого тестирования, потому что сервер контролирует даты выпуска. Существует целый ряд методов, которые могут смягчить проблему контрактов, некоторые из них просты, другие более функциональны и требуют более сложного обучения.
При простом подходе, API предоставляется вместе с npm-пакетом с типизацией (JSDoc, TypeScript). Потребители данного пакета могут получить библиотеку и воспользоваться преимуществами автодополнения (IntelliSense) и валидация во время разработки. Более сложный подход включает в себя PACT, который был создан для формализации этого процесса с помощью очень разрушительного подхода - не сервер определяет план тестирования для себя, а клиент определяет тесты для... сервера! PACT может записывать ожидания клиента и помещать их в общее место, "broker", так что сервер может извлекать эти ожидания и запускать на каждой сборке, используя библиотеку PACT, чтобы обнаружить нарушенные контракты - ожидания клиента, которые не выполнены. Таким образом, все несоответствия API сервера и клиента будут обнаружены на ранних стадиях сборки и могут уберечь разработчика от больших проблем.
❌ Иначе: Альтернативой является ручное тестирование или развертывание
✅ Сделать: Большинство разработчиков пренебрегают тестированием Middleware, потому что они являются небольшой частью системы и требуют живого сервера Express. Обе причины ошибочны, так как Middleware, несмотря на свой размер, имеют огромное влияние на все или большинство запросов и могут быть легко протестированы, как чистые функции. Для тестирования функции middleware нужно просто вызвать ее и "шпионить" (например, с помощью Sinon) за взаимодействием с объектами {req,res}, чтобы убедиться, что функция отработала корректно. Библиотека node-mock-http идет еще дальше и анализирует объекты {req,res}, а также следит за их поведением. Например, он может утверждать, соответствует ли статус http, который был установлен на объекте res, ожидаемому (см. пример ниже).
❌ Иначе: Ошибка в middleware Express === ошибка большинства/всех запросов
✏ Примеры кода
👏 Правильно: Тестирование middleware изолированно без выполнения сетевых запросов и включения Express полностью
// middleware, которое мы хотим протестировать
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 (4,900+ stars) и Code Climate (2,000+ stars)
Благодарность: Keith Holliday
❌ Иначе: При низком качестве кода ошибки и производительность всегда будут проблемой, которую не смогут исправить ни новые библиотеки, ни современный функционал.
✏ Примеры кода
✅ Сделать: Странно, но большинство тестов направлено на проверку логики и данных, в то время как иногда наибольшей проблемой (и это правда трудно исправить) может стать вопросы, касающиеся инфраструктуры. Например, вы когда-нибудь тестировали перегрузку памяти, падение сервера? Ваша система отслеживания понимает, когда ваш API становится в половину медленнее? Для того, чтобы протестировать и исправить такие ситуации Netflix придумали Chaos engineering. Его целью является осведомленность для тестирования устойчивости приложения к chaos-проблемам, а также различные фреймворки, чтобы это можно было протестировать. Например, один из его известных инструментов, the chaos monkey, случайным образом кладет серверы, чтобы убедиться, что наш сервис все еще может обслуживать пользователей и не полагается на один сервер (есть также версия для Kubernetes, kube-monkey). Все эти инструменты работают на уровне хостинга или платформы, но что, если вы хотите протестировать чистейший Node-хаос. Например, вы хотите проверить, как ваш Node справляется с ошибками, которые были не пойманы, ошибками промисов, перегрузкой v8 при максимально допустимых 1.7GB. Вдруг вы хотите проверить, остается ли ваш UX удовлетворительных при частых блокировках цикла событий? Для решения данной проблемы я написал node-chaos (alpha), который генерирует все возможные виды хаоса, связанных с Node.
❌ Иначе: От этого не спрятаться. Закон Мерфи будет преследовать вас везде.
✏ Примеры кода
⚪ ️2.7 Избегайте использования глобальных фикстур и seeds. Для каждого теста должны быть свои собственные данные
✅ Сделать: Согласно золотому правилу (пункт 0), каждый тест должен использовать свой собственный набор строк в БД, чтобы избежать перекрытия данных. На деле же, данное правило часто нарушается тестировщиками, которые загружают данные в БД перед запуском тестов (фикстуры), чтобы улучшить производительность. Хотя производительность является реальной причиной для беспокойства - она далеко не главная (см. пункт "Компонентное тестирование"). Сложность тестирования - гораздо более болезненная проблема, решение которой в большинстве случаев определяется другими соображениями. На практике, нужно сделать так, чтобы каждый тест явным образом добавляет нужные ему записи в БД и работал только с ними. Если производительность становится критической проблемой - компромиссом может быть реализация тестов, которые не изменяют данные (например, запросы).
❌ Иначе: Какие-то тесты не сработали, развертывание проекта прервано и разработчики тратят время на выяснение ошибки. А есть ли она? Кажется, что нет, ведь два теста просто изменили одни и те же seed-данные.
✏ Примеры кода
👎 Неправильно: тесты не являются независимыми и полагаются на некий глобальный хук для получения глобальных данных
before(async () => {
// добавление данных о сайтах и администраторах в БД. Где они сейчас находятся? Снаружи, во внешнем JSON или фреймворке
await DB.AddSeedDataFromJson("seed.json");
});
it("When updating site name, get successful confirmation", async () => {
// я знаю, что название сайта "portal" существует - я видел его в seed-файлах
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 () => {
// я знаю, что название сайта "portal" существует - я видел его в seed-файлах
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);
});
✅ Сделать: Время, когда тесты начинают удаление данных, определяет способ их написания. Наиболее жизнеспособные варианты: after-all и after-each. При выборе второго варианта, удаление данных гарантирует полную очистку и создает преимущества для разработчика. В начале теста не существует других записей. Можно быть уверенным, какие данные запрашиваются. Иногда даже возникает соблазн посчитать строки при проверке assertions. Однако, существуют и серьезные недостатки. При работе в мульти-процессном режиме тесты могут мешать друг другу. Пока процесс-1 очищает таблицы, процесс-2 в тот же момент запрашивает данные и падает (потому что БД была внезапно удалена процессом-1). Кроме того, сложнее исправить проблемы в неудачных тестах - при посещении БД не будет обнаружено никаких записей.
Второй вариант - это after-all - удаление данных после завершения всех тестов (или даже ежедневно!). Такой подход означает, что одна и та же БД с существующими записями служит для всех процессов и тестов. Чтобы не наступать друг другу на пятки, тесты должны работать с конкретными записями, которые они добавили. Нуждаетесь в проверке, какая запись была добавлена? Предположите, что есть еще тысячи записей, и сделайте запрос к записи, которая была добавлена явно. Нужно проверить, что запись удалена? Нельзя предполагать, что таблица пуста - проверьте, что этой конкретной записи там нет. Такая техника дает несколько преимуществ: она работает мульти-процессном режиме, когда разработчик хочет понять, что произошло - данные есть и не удалены. Также это увеличивает шанс найти ошибки так как БД полна записей. Смотрите полную таблицу сравнений здесь.
.
❌ Иначе: При отсутствии разделения записей или удаления - тесты будут наступать на пятки сами себе. Использование транзакций работает только в реляционных БД и, при появлении внутренних транзакций, может стать сложнее.
✏ Примеры кода
👏 After-all: Необязательно удалять данные после каждого запуска. Чем больше данных у нас есть во время выполнения тестов, тем больше это похоже на продакшн.
// after-all очистка (рекомендуется)
// global-teardown.js
module.exports = async () => {
// ...
if (Math.ceil(Math.random() * 10) === 10) {
await new OrderRepository().cleanup();
}
};
✅ Сделать: Изолируйте тестируемый компонент, перехватывая любой исходящий HTTP-запрос и предоставляя желаемый ответ, чтобы HTTP API не пострадал. Nock - отличный инструмент для этой задачи, поскольку он предоставляет удобный синтаксис для определения поведения внешних сервисов. Изоляция необходима для предотвращения шума и снижения производительности, но в основном для моделирования различных сценариев и ответов - хороший симулятор полета не рисует чистое голубое небо, а приносит спокойные бури. Это усиливается в микросервисной архитектуре, где фокус всегда должен быть сосредоточен на одном компоненте без привлечения остальных. Хотя можно имитировать поведение внешнего сервиса с помощью тестов-двойников (mocking), предпочтительнее не трогать развернутый код и действовать на сетевом уровне, чтобы тесты были чисто "черным ящиком". Недостатком изоляции является невозможность обнаружения изменений в компоненте коллаборатора и недопонимания между двумя сервисами - обязательно компенсируйте это с помощью нескольких контрактных или E2E-тестов.
❌ Иначе: Некоторые сервисы предоставляют фейк-версию, которая может быть развернута локально, обычно с помощью Docker-Это сделает настройку легче и увеличит производительность. Однако, это не решит проблему моделирования различных responses. Некоторые сервисы предоставляют "песочницу" таким образом, что реальный сервис работает, но без побочных эффектов. Такой вариант не поможет моделировать различные сценарии и снизить шум.
✏ Примеры кода
👏 Предотвращение сетевых обращений к внешним компонентам позволяет моделировать сценарии и минимизировать шум
// перехват запросов к стороннему API и возврат заранее определенного ответа
beforeEach(() => {
nock('http://localhost/user/').get(`/1`).reply(200, {
id: 1,
name: 'John',
});
});```
✅ Сделать: Если невозможно подтвердить наличие определенных данных, проверяйте наличие и типы обязательных полей. Иногда ответ может содержать важные поля с динамическими данными, которые невозможно предугадать при написании теста, такие как даты. В случае, когда точно известно, что поля не будут иметь значения null, а также будут содержать правильные типы, то все равно нужно обязательно это проверить. Большинство библиотек поддерживают проверку типов. Если ответ - небольшой, то можно выполнить проверку в рамках одного утверждения (см. пример кода). Другой вариант - проверка полного ответа по OpenAPI (Swagger). Большинство фреймворков для тестирования имеют расширения, которые проверяют ответы API, согласно их документации.
❌ Иначе: Несмотря на то, что вызывающий код или API полагается на некоторое поле с динамическими данными (например, ID или дата), оно может не вернуться в ответе
✏ Примеры кода
test("When adding a new valid order, Then should get back approval with 200 response", async () => {
// ...
// Assert
expect(receivedAPIResponse).toMatchObject({
status: 200,
data: {
id: expect.any(Number), // Any number satisfies this test
mode: "approved",
},
});
});
✅ Сделать: При проверке интеграций нельзя ограничиваться только проверками либо "happy", либо "sad" вариантов. Помимо проверки на ошибки (например, HTTP 500), нужно так же смотреть на аномалии на уровне сети или скорость ответов таймера. Это демонстрирует то, что ваш код устойчив и может обрабатывать различные сетевые сценарии. Надежные перехватчики могут легко имитировать различное поведение сетевого взаимодействия. Он даже может понять, когда стандартное значение тайм-аута HTTP-клиента больше, чем смоделированное время ответа, и сразу же бросить исключение по тайм-ауту, не дожидаясь ответа.
❌ Иначе: Все тесты пройдут, однако, продакшн не сможет корректно сообщить об ошибке, когда со стороны будут приходить исключения
✏ Примеры кода
test("When users service replies with 503 once and retry mechanism is applied, then an order is added successfully", async () => {
// Arrange
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",
};
// Act
const response = await axiosAPIClient.post("/order", orderToAdd);
// Assert
expect(response.status).toBe(200);
});
✅ Сделать: При разработке тестов, продумайте, как минимум, охватить 5 потоков вывода. Когда ваш тест вызывает какой-то действие (например, вызов API), возникает ситуация, когда нужно протестировать результат этого действия. Важно понимать, что нас не интересует, как все работает. Наше внимание сосредоточено на конечном результате, на том, что может повлиять на пользователя. Конечные результаты можно разделить на следующие категории:
• Ответ - Тест вызывает действие (например, через API) и получает ответ. Теперь он занимается проверкой корректности данных ответа, схемы и статуса HTTP.
• Новое состояние - После вызова действия некоторые общедоступные данные, вероятно, будут изменены.
• Внешние вызовы - После вызова действия приложение может вызвать внешний компонент через HTTP или любой другой транспорт. Например, вызов для отправки SMS, электронной почты или списания средств с кредитной карты.
• Очереди сообщений - Результатом потока может быть сообщение в очереди.
• Наблюдаемость - Некоторые вещи необходимо отслеживать, например, ошибки или значимые бизнес-события. Когда транзакция терпит неудачу, мы ожидаем не только правильного ответа, но и корректной обработки ошибок и правильного протоколирования/метрики. Эта информация поступает непосредственно к очень важному пользователю - оперативному пользователю (т.е. производственному 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 - сперва извлеките данные из UI
const allRenderedUsers = getAllByTestId("user").map(
(uiElement) => uiElement.textContent
);
const allRealVIPUsers = allUsers
.filter((user) => user.vip)
.map((user) => user.name);
expect(allRenderedUsers).toEqual(allRealVIPUsers); // сравнение данных
});
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 - использование UI и данных в assertion
expect(getAllByTestId("user")).toEqual(
'[<li data-test-id="user">John Doe</li>]'
);
});
✅ Сделать: Запрашивайте элементы HTML на основе атрибутов, которые, скорее всего, переживут графические изменения, в отличие от селекторов CSS и, например, меток формы. Если заданный элемент не имеет таких атрибутов, создайте специальный тестовый атрибут, например 'test-id-submit-button'. Такой путь не только гарантирует, что ваши функциональные/логические тесты никогда не сломаются из-за изменений внешнего вида, но и даст понять, что данный элемент и атрибут используются тестами и не должны быть удалены.
❌ Иначе: Вы хотите протестировать функциональность входа в систему, которая охватывает множество компонентов, логики и сервисов, все настроено идеально - stubs, spies, вызовы Ajax изолированы. Все кажется идеальным. Затем тест не проходит, потому что дизайнер изменил CSS-класс div с "thick-border" на "thin-border".
✏ Примеры кода
// код разметки (часть компонента React)
<h3>
<Badge pill className="fixed_badge" variant="dark">
<span data-test-id="errorsLabel">{value}</span>
<!-- note the attribute 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");
});
<!-- the markup code (part of React component) -->
<span id="metric" className="d-flex-column">{value}</span>
<!-- what if the designer changes the classs? -->
// в данном примере используется enzyme
test("Whenever no data is passed, error metric shows zero", () => {
// ...
expect(wrapper.find("[className='d-flex-column']").text()).toBe("0");
});
✅ Сделать: При любом разумном размере нужно тестировать компонент снаружи, как это делают пользователи. Полностью рендерите UI и проверяйте, что он ведет себя, как ожидается. Избегайте мокинга и частичного рендеринга - такой подход может привести к невыявленным ошибкам из-за недостатка деталей и сделать поддержку сложнее, так как тесты начинают влиять на внутренние компоненты (смотри пункт 'Favour blackbox testing'). Если один из дочерних компонентов значительно замедляет работу (например, анимация) или усложняет настройку - подумайте о явной замене его на фейковый.
Учитывая все вышесказанное, следует сказать: эта техника работает для небольших/средних компонентов, которые содержат разумное количество дочерних компонентов. Полный рендеринг компонента со слишком большим количеством дочерних компонентов повлечет за собой трудности в анализе (анализ первопричины) и может стать слишком медленным. В таких случаях пишите несколько тестов на один большой родительский компонент и больше тестов на дочерние компоненты.
❌ Иначе: Если залезть во внутренности компонента, вызывая его приватные методы, и проверить - вам придется рефакторить все тесты в случае изменения реализации компонента. У вас действительно есть возможности для такого уровня сопровождения?
✏ Примеры кода
class Calendar extends React.Component {
static defaultProps = { showFilters: false };
render() {
return (
<div>
A filters panel with a button to hide/show filters
<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();
// обратитесь к внутренним компонентам, не затрагивая UI и вызовите метод (white-box)
// Assert
expect(wrapper.find("Filter").props()).toEqual({ title: "Choose Filter" });
// что если мы изменим имя свойства или ничего не передадим
});
⚪ ️ 3.4 Используйте встроенную по фреймворки поддержку асинхронного кода. Постарайтесь ускорить работу.
✅ Сделать: Во многих случаях время завершения тестируемого блока просто неизвестно (например, анимация приостанавливает появление элемента) - в этом случае предпочитайте детерминированные методы, которые предоставляют большинство платформ. Некоторые библиотеки позволяют ожидать выполнение операций (например, Cypress cy.request('url')), другие предоставляют API для ожидания, например @testing-library/dom method wait(expect(element)). Иногда более элегантным способом является заглушка, использованная вместо медленно выполняющегося компонента, например, API, а затем, когда момент получения ответа станет детерминированным, компонент можно отрендерить явным способом. Если существует зависимость от какого-то "спящего" компонента, то может оказаться полезным поторопить часы. Спящий компонент - это паттерн, которого следует избегать, поскольку он заставляет ваш тест быть медленным (при слишком коротком периоде ожидания). Когда спящий режим неизбежен, а поддержка со стороны фреймворка для тестировании отсутствует, некоторые библиотеки npm, такие как wait-for-expect, могут помочь с полудетерминированным решением.
❌ Иначе: При длительном спящем режиме тесты будут работать на порядок медленнее. Попытка "заснуть" на небольшое количество времени приведет к тому, что тест будет падать, каждый раз, когда тестируемый модуль не отреагировал вовремя. Таким образом, все сводится к компромиссу между хрупкостью и плохой производительностью.
✏ Примеры кода
// используем 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 () => {
// элемент изначально отсуствует...
// пользовательская логика (внимание: упрощенная, без timeout)
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. Они могут быть легко настроены для наблюдения за тем, жив ли сервер и отвечает ли он в рамках SLA. Это лишь поверхностный обзор того, что может пойти не так, поэтому предпочтительнее выбирать инструменты, специализирующиеся на фронтенде (например, lighthouse, pagespeed), которые выполняют более качественный анализ. В центре внимания должны быть метрики, которые непосредственно влияют на UX, такие как время загрузки страницы, meaningful paint, время, пока страница не станет интерактивной (TTI). Кроме того, можно также следить за техническими причинами, такими как cжатие контента, время до первого байта, оптимизация изображений, обеспечение разумного размера DOM, SSL и многие другие. Желательно придерживаться этого правила, как во время разработки, так и в рамках CI и, самое главное, постоянно на продакшн-серверах/CDN.
❌ Иначе: Будет обидно осознавать, что после такой тщательной проработки пользовательского интерфейса, 100% прохождения функциональных тестов и сложной комплектации - UX ужасен и медлителен из-за неправильной настройки CDN.
✅ Сделать: При написании основных тестов (не E2E-тестов) избегайте привлечения ресурсов, которые находятся вне вашей ответственности и контроля, таких как бэкенд API, и используйте вместо них заглушки (т.е. test double). Вместо реальных сетевых вызовов API, используйте какую-нибудь библиотеку test double (например, Sinon, Test doubles и т.д.) для создания заглушки ответа API. Основным преимуществом является предотвращение флейкинга - тестовые или staging API по определению не очень стабильны и время от времени будут проваливать ваши тесты, хотя компонент ведет себя просто отлично (production env не был предназначен для тестирования и обычно ограничивает запросы). Это позволит смоделировать различное поведение API, которое должно определять поведение вашего компонента, например, когда данные не найдены или когда API выдает ошибку. И последнее, но не менее важное: сетевые вызовы значительно замедлят работу тестов
❌ Иначе: Средний тест длится не более нескольких мс, типичный вызов API длится 100 мс>, это делает каждый тест в ~20 раз медленнее.
✏ Примеры кода
// Тестируемый блок
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("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();
});
✅ Сделать: Несмотря на то, что E2E (end-to-end) обычно означает тестирование только пользовательского интерфейса с помощью реального браузера (см. пункт 3.6), для других - это означает тесты, которые охватывают всю систему, включая реальный backend. Последний тип тестов является очень ценным, так как он покрывает ошибки интеграции между frontend и backend, которые могут возникнуть из-за неправильного понимания схемы обмена. Они также являются эффективным методом обнаружения проблем интеграции между бэкендами (например, микросервис A посылает неверное сообщение микросервису B) и даже выявления сбоев развертывания - не существует бэкенд-фреймворков для тестирования E2E, которые были бы настолько же дружественными и зрелыми, как UI-фреймворки, такие как Cypress и Puppeteer. Недостатком таких тестов является высокая стоимость настройки среды с таким количеством компонентов, а также их хрупкость - при наличии 50 микросервисов, даже если один из них откажет, то весь E2E просто не работает. По этой причине мы должны использовать данный подход очень экономно и, вероятно, иметь 1-10 таких тестов и не более. Тем не менее, даже небольшое количество тестов E2E, скорее всего, выявит те проблемы, на которые они нацелены - ошибки развертывания и интеграции. Рекомендуется запускать такие тесты на продакшене.
❌ Иначе: Пользовательский интерфейс может хорошо справляться с тестированием своей функциональности, но поздно понять, что схема данных, с которой должен работать UI, отличается от ожидаемой.
⚪ ️ 3.8 Ускорение тестов E2E за счет использования повторного использования данных для входа в систему
✅ Сделать: В тестах E2E, которые задействуют реальный backend и полагаются на валидный пользовательский токен для вызовов API, не нужно изолировать тест до уровня, на котором пользователь создается и регистрируется при каждом запросе. Вместо этого, авторизуйтесь только один раз перед началом выполнения теста (т.е. before-all hook), сохраните токен в каком-нибудь локальном хранилище и используйте его повторно во всех запросах. Может казаться, что это нарушает один из основных принципов тестирования - сохранять автономность теста без привязки к ресурсам. Хотя это обоснованное беспокойство, в тестах E2E производительность является ключевым фактором, и создание 1-3 запросов API перед запуском каждого отдельного теста может привести к большому времени выполнения. Повторное использование учетных данных не означает, что тесты должны работать с одними и теми же пользовательскими записями - если вы полагаетесь на пользовательские записи (например, история платежей пользователя), убедитесь, что эти записи создаются в рамках теста, и не делитесь их существованием с другими тестами. Также помните, что на backend может стоять заглушка - если ваши тесты сосредоточены на фронтенде, возможно, лучше изолировать его и заглушить API бэкенда (см. пункт 3.6).
❌ Иначе: При наличии 200 тестов и, предполагая, что каждый login = 100 ms, что в сумме каждый раз дает 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, который посещает большинство или все станицы сайта и гарантирует, что все работает исправно. Данный вид теста приносит большую пользу, так как его легко написать и поддерживать, в то время, как он может обнаружить любую проблему, включая сетевые сбои и проблемы при развертывании. Другие виды smoke-тестов не такие надёжные и информативные - иногда просто пингуют домашнюю страницу или запускают множество интеграционных тестов, которые не находят проблем. Очевидно, что данный вид тестов не может полностью заменить функциональные тесты.
❌ Иначе: Все может казаться идеальным, все тесты пройдены, однако, у компонента Payment возникли проблемы с упаковкой или не маршрут может не рендериться
✏ Примеры кода
it("When doing smoke testing over all page, should load them all successfully", () => {
// пример с использованием Cypress, но может быть реализован
// с другим набором 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, при использовании правильных инструментов они могут служить в качестве определенного коммуникатора, который выравнивает всех участников проекта - разработчиков и клиентов. Некоторые фреймворки, позволяют выразить план тестов на "человекоподобном" языке так, что любая заинтересованная сторона, включая менеджеров продукта, также может читать и понимать тесты, которые превратились в документацию. Этот подход также называют "acceptance test", поскольку она позволяет заказчику выразить свои требования к продукту на простом языке. Это [BDD (behavior-driven testing)] (https://en.wikipedia.org/wiki/Behavior-driven_development) в чистом виде. Одним из популярных фреймворков, позволяющих это сделать, является Cucumber, см. пример ниже. Еще одна похожая, но другая возможность, StoryBook, позволяет представить компоненты пользовательского интерфейса в виде графического каталога, где можно пройтись по различным состояниям каждого компонента (например, отобразить сетку без фильтров, отобразить сетку с несколькими рядами или без них и т.д.), посмотреть, как это выглядит, и как вызвать это состояние - это может понравиться и product-менеджерам, но в основном это документация для разработчиков, которые используют эти компоненты.
❌ Иначе: После огромного количества усилий, потраченного на разработку тестов, будет жаль не использовать его возможности
✏ Примеры кода
// так можно описывать тесты с помощью 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"
✅ Сделать: Настройте автоматизированные инструменты для создания скриншотов пользовательского интерфейса при представлении изменений и обнаружения визуальных проблем, таких как перекрытие или разрыв контента. Это гарантирует, что не только правильные данные будут подготовлены, но и пользователь сможет удобно их посмотреть. Эта техника не получила широкого распространения, так как мы привыкли к функциональным тестам, но именно визуальные тесты являются тем, что испытывает пользователь, а с таким количеством типов устройств очень легко упустить из виду какую-нибудь неприятную ошибку пользовательского интерфейса. Некоторые бесплатные инструменты могут обеспечить основы - генерировать и сохранять скриншоты для визуальной проверки. Хотя такой подход может быть оправданным для небольших приложений, он несовершенен, как и любое другое ручное тестирование, требующее человеческого труда при каждом изменении. С другой стороны, довольно сложно обнаружить проблемы пользовательского интерфейса автоматически из-за отсутствия четкого определения - именно здесь вступает в дело область "визуальной регрессии", которая решает эту головоломку, сравнивая старый пользовательский интерфейс с последними изменениями и обнаруживая различия. Некоторые бесплатные инструменты могут предоставить некоторые из этих функций (например, wraith, PhantomCSS, но могут потребовать значительного времени на настройку. Коммерческая линейка инструментов (например, Applitools, Percy.io) делает шаг вперед, сглаживая установку и предоставляет расширенные возможности, такие как управление пользовательским интерфейсом, оповещение, интеллектуальный захват путем устранения "визуального шума" (например, рекламы, анимации) и даже анализ первопричины изменений DOM/CSS, которые привели к проблеме.
❌ Иначе: Является ли хорошей страница с контентом, которая все отображает, 100% тестов проходит, быстро загружается, но половина области контента скрыта?
✏ Примеры кода
# Добавьте столько доменов, сколько нужно. Ключ действует, как метка
domains:
english: "http://www.mysite.com"
# Введите ширину экрана ниже, вот несколько примеров
screen_widths:
- 600
- 768
- 1024
- 1280
# Введите URL страницы, вот несколько примеров:
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: "в верхних 80-х или 90-х"] (https://martinfowler.com/bliki/TestCoverage.html)), которое предположительно должно удовлетворять большинству приложений.
Советы: Можно настроить непрерывную интеграцию (CI) на порог покрытия (ссылка на Jest) и останавливать сборку, которая не соответствует этому стандарту (также можно настроить порог для каждого компонента, см. пример кода ниже). В дополнение к этому, рассмотрите возможность обнаружения снижения покрытия сборки (когда только что зафиксированный код имеет меньшее покрытие) - это подтолкнет разработчиков к увеличению или, по крайней мере, сохранению количества тестируемого кода. При всем этом, покрытие - это только одна из мер, основанная на количественных показателях, которой недостаточно для определения надежности вашего тестирования. Помимо всего прочего, его можно обмануть, как будет продемонстрировано в следующих пунктах.
❌ Иначе: Уверенность и цифры идут рука об руку, не зная, что вы протестировали большую часть системы - будет присутствовать сомнение, которое будет вас тормозить.
✏ Примеры кода
✅ Сделать: Некоторые проблемы с кодом иногда остаются незамеченными. Их действительно трудно обнаружить даже с помощью традиционных инструментов. Они не всегда являются ошибками, а иногда это скорее неожиданное поведение приложения, которое может иметь серьезные последствия. Например, часто некоторые области кода редко или почти никогда не вызываются - вы думали, что класс 'PricingCalculator' всегда устанавливает цену продукта, но оказалось, что он фактически никогда не вызывается, хотя у нас 10000 продуктов в БД и много продаж... Отчеты о покрытии кода помогут вам понять, ведет ли приложение себя так, как вы считаете. Кроме того, они могут показать, какие типы кода не протестированы - если вам сообщают, что 80% кода протестировано, это еще не говорит о том, покрыты ли критические части. Для генерации отчетов нужно просто запустить приложения либо в продакшн, либо во время тестирования, а затем их просмотреть. Они показывают, как часто вызывается, каждая область кода и, если вы потратите время на их изучение, то сможете обнаружить некоторые проблемы с кодом.
❌ Иначе: Если вы не знаете, какая часть кода у вас не протестирована, то вы не сможете узнать, откуда могут возникнуть проблемы
✏ Примеры кода
На основе реального сценария, когда мы отслеживали использование нашего приложения в QA и обнаружили интересные закономерности входа в систему (подсказка: количество отказов входа непропорционально, что-то явно не так. В итоге выяснилось, что какая-то ошибка во фронтенде постоянно бьет по API входа в бэкенд).
✅ Сделать: Традиционная метрика Coverage часто лжет: Она может показать вам 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: "[email protected]", price: 120 });
}); // Запускает 100% покрытие кода, но ничего не проверяет
✅ Сделать: Набор плагинов ESLint был создан специально для проверки шаблонов тестового кода и обнаружения проблем. Например, eslint-plugin-mocha предупредит, когда тест написан на глобальном уровне (не дочерний элемент describe()) или когда тесты пропущены что может привести к ложному убеждению, что все тесты пройдены. Аналогично, eslint-plugin-jest может, например, предупредить, когда тест вообще ничего не проверяет.
❌ Иначе: При виде 90% покрытия кода и 100% зеленых тестов на вашем лице появится широкая улыбка только до тех пор, пока вы не поймете, что многие тесты ничего не проверяют, а многие тестовые наборы были просто пропущены. Надеюсь, вы ничего не развернули, основываясь на этом ошибочном наблюдении.
✏ Примеры кода
describe("Too short description", () => {
const userToken = userService.getDefaultToken(); // ошибка:no-setup-in-describe, вместо этого используйте hooks (экономно)
it("Some description", () => {}); // ошибка: valid-test-description. Должно включать "Should" + не менее 5 слов
});
it.skip("Test name", () => {
// ошибка:no-skipped-tests, ошибка:no-global-tests. Поместите тесты только в describe или suite
expect("somevalue"); // ошибка:no-assert
});
it("Test name", () => {
// ошибка:no-identical-title. Присваивайте тестам уникальные названия
});
✅ Сделать: Линтинг - это бесплатный обед, за 5 минут настройки вы получаете бесплатный автопилот, охраняющий ваш код и отлавливающий существенные проблемы по мере набора текста. Прошли те времена, когда линтинг сводился к косметике (никаких полутонов!). Сегодня линтеры могут отлавливать серьезные проблемы, такие как неправильно брошенные ошибки и потеря информации. Помимо основного набора правил (например, ESLint standard или Airbnb style), включите несколько специализированных линтеров, например eslint-plugin-chai-expect, который может обнаружить тесты, которые ничего не проверяют, eslint-plugin-promise может обнаружить промисы без разрешения (your code will never continue), eslint-plugin-security который может обнаружить регулярные выражения, которые могут быть использованы для DOS атак, и eslint-plugin-you-dont-need-lodash-underscore, способный предупредить, когда код использует методы библиотеки утилит, которые являются частью методов ядра V8, например Lodash._map(...).
❌ Иначе: Подумайте о том дне, когда ваш код упал, но ваш лог не отображает трассировку стека ошибок. Что произошло? Ваш код по ошибке выбросил объект, не являющийся ошибкой, и трассировка стека была потеряна - хорошая причина для того, чтобы биться головой о кирпичную стену. 5-минутная настройка линтера может обнаружить эту ошибку и спасти ваш день
✏ Примеры кода
✅ Сделать: Внедрение CI с блестящими проверками качества, такими как тестирование, линтинг, проверка на уязвимости и другое. Помогите разработчикам запустить этот конвейер также локально, чтобы получить мгновенную обратную связь и сократить петлю обратной связи. Почему? Эффективный процесс тестирования состоит из множества итеративных циклов: (1) пробное тестирование -> (2) обратная связь -> (3) рефакторинг. Чем быстрее обратная связь, тем больше итераций улучшения разработчик может провести в каждом модуле и улучшить результаты. С другой стороны, когда обратная связь приходит с запозданием, разработчик может перейти уже к другой теме или задаче и он будет не готов к доработке предыдущего.
Некоторые разработчики CI (пример: CircleCI local CLI) позволяют запускать конвейер локально: коммерческие сервисы, такие как wallaby provide highly-valuable & testing insights в качестве прототипа для разработчиков. В качестве альтернативы можно просто добавить в package.json скрипт npm, запускающий все команды на проверку качества (например, test, lint, vulnerabilities), используйте инструменты вроде concurrently для распараллеливания и ненулевого кода выхода в случае сбоя одного из инструментов. Теперь разработчику достаточно вызвать одну команду - например, 'npm run quality' - чтобы получить мгновенную обратную связь. Также существует возможность прерывания коммита, если проверка качества не удалась, с помощью githook (husky can help).
❌ Иначе: Когда результаты приходят на следующей день после кода, тестирование не становится неотъемлемой частью разработки
✏ Примеры кода
👏 Правильно: Скрипты 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. Если вы пошли по пути serverless, то несколько фреймворков типа serverless и AWS SAM позволяют локально вызывать код FaaS.
❌ Иначе: Использование разных технологий для тестирования требует поддержки 2-х моделей развертывания, разделяя разработчиков и OC.
✏ Примеры кода
👏 Пример: 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
✅ Сделать: Если все сделано правильно, тестирование - это ваш друг 24/7, обеспечивающий практически мгновенную обратную связь. На практике выполнение 500 юнит-тестов на одном процессоре в один поток может занять слишком много времени. К счастью, современные программы для запуска тестов и CI-платформы (такие как Jest, AVA и Mocha extensions) могут распараллелить тест на несколько процессов и добиться значительного улучшения времени обратной связи. Некоторые поставщики CI также распараллеливают тесты по контейнерам (!), что еще больше сокращает цикл обратной связи. Будь то локально на нескольких процессах или через облачный CLI с использованием нескольких машин - распараллеливание требует сохранения автономности тестов, поскольку каждый из них может выполняться на разных процессах.
❌ Иначе: Получение результатов тестирования через час после внедрения нового кода, когда вы уже пишете следующие функции - отличный рецепт для того, чтобы сделать тестирование менее актуальным
✏ Примеры кода
👏 Правильно: Mocha parallel & Jest легко обгоняют традиционную Mocha благодаря распараллеливанию тестирования (Благодарность: JavaScript Test-Runners Benchmark)
⚪ ️5.5 Держитесь подальше от проблем, связанных с правовым полем, используя проверку на лицензию и антиплагиат
✅ Сделать: Вопросы лицензирования и плагиата, вероятно, не являются сейчас вашей главной заботой, но почему бы не поставить галочку и в этой графе за 10 минут? Куча пакетов npm, таких как проверка лицензии и проверка на плагиат (коммерческий и бесплатный план), могут быть легко встроены в ваш CI-конвейер и проверены на наличие таких проблем, как зависимости с ограничительными лицензиями или код, который был скопирован из Stack Overflow и, очевидно, нарушает авторские права.
❌ Иначе: Непреднамеренно разработчики могут использовать пакеты с несоответствующими лицензиями или копировать коммерческий код и столкнуться с юридическими проблемами
✏ Примеры кода
// установите license-checker в CI или локально
npm install -g license-checker
// попросите его просканировать все лицензии и выдать ошибку с кодом выхода, отличным от 0, в случае обнаружения неавторизованной лицензии. Система CI должна уловить этот сбой и остановить сборку
license-checker --summary --failOn BSD
✅ Сделать: Даже самые авторитетные зависимости, такие как Express, имеют известные уязвимости. Это можно легко устранить с помощью инструментов сообщества, таких как npm audit, или коммерческих инструментов, таких как snyk (предлагается также бесплатная версия для сообщества). Оба инструмента могут быть вызваны из вашего CI при каждой сборке
❌ Иначе: Для поддержания кода чистым от уязвимостей без специальных инструментов придется постоянно следить за публикациями в Интернете о новых угрозах. Довольно утомительно
✅ Сделать: Последнее внедрение в Yarn и npm package-lock.json создало серьезную проблему (дорога в ад вымощена благими намерениями) - по умолчанию пакеты больше не получают обновлений. Разработчик, выполняющий множество свежих развертываний с помощью 'npm install' и 'npm update', не получит никаких новых обновлений. Это приводит в лучшем случае к некачественным версиям зависимых пакетов, а в худшем - к уязвимому коду. Сейчас команды разработчиков полагаются на добрую волю и память разработчиков, чтобы вручную обновлять 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, которые хорошо известны
- Используйте декларативный синтаксис. Это единственный вариант для большинства поставщиков, но старые версии Jenkins позволяют использовать код или UI
- Выбирайте продукт, который имеет встроенную поддержку Docker
- Запускайте сначала самые быстрые тесты. Создайте шаг/этап "Smoke testing", который группирует несколько быстрых проверок (например, линтинг, модульные тесты) и обеспечивает быструю обратную связь с коммиттером кода.
- Упростить просмотр сборки, включая отчеты о тестировании, отчеты о покрытии, отчеты о мутациях, журналы и т.д.
- Создайте несколько заданий для каждого события, повторно используя шаги между ними. Например, настройте одно задание для коммитов ветки функций и другое - для PR мастера. Пусть каждый из них повторно использует логику, используя общие шаги (большинство продуктов предоставляют некоторые механизмы для повторного использования кода).
- Никогда не вставляйте секреты в объявление задания, берите их из хранилища секретов или из конфигурации задания
- Явно повышайте версию в сборке релиза или, по крайней мере, убедитесь, что разработчик это сделал
- Сборка только один раз и выполнение всех проверок над единственным артефактом сборки (например, образом Docker)
- Тестируйте в эфемерной среде, которая не переносит состояние между сборками. Кэширование модулей node_modules может быть единственным исключением
❌ Иначе: Вы можете многое упустить
✅ Сделать: Проверка качества - это случайность, чем больше места вы охватываете тем больше шанс обнаружить проблемы на ранней стадии. При разработке многократно используемых пакетов или работе с несколькими клиентами с различными конфигурации и версиями Node, CI должен выполнят конвейер тестов для различных перестановок этих конфигураций. Например, если мы используем MySQL для одних клиентов и Postgres для других, некоторые поставщики СI поддёргивают функцию 'matrix', которая позволяет запустить конвейер тестов для всех вариантов MySQL, Postgres и нескольких версий Node (8, 9, 10). Это делается только с помощью конфигурации без каких-либо дополнительных усилий (при условии, что у вас есть тестирование или любые другие проверки качества). Другие CI, не поддерживающие 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 Best Practices] (https://github.com/goldbergyoni/nodebestpractices).
📗 Онлайн-курс: Понравилось это руководство и вы хотите довести свои навыки тестирования до совершенства? Посетите мой комплексный курс [Testing Node.js & JavaScript From A To Z] (https://www.testjavascript.com).
Follow:
Роль: Консультант и технический рецензент
Пересмотреть, улучшить, и отшлифовать все тексты.
Информация: full-stack web-разработчик, Node.js & GraphQL любитель
Роль: Концепция, дизайн и отличные советы
Информация: Мастерский frontend-разработчик, CSS-эксперт и любитель эмоджи
Роль: Помогает поддерживать проект в рабочем состоянии и анализирует методы, связанные с безопасностью
About: Любит работать с Node.js проектами и безопасностью веб-приложений.
Thanks goes to these wonderful people who have contributed to this repository!