Лучшие практики тестирования Node.js и JavaScript в 2019
0. Золотое правило: Тесты обязано должны оставаться простыми и понятными как дважды два
Тесты не должны рассматриваться как традиционный код приложения - типичная команда в любом случае сталкивается с необходимостью поддерживать свое основное приложение (функции, которые мы пишем). Тестовый проект не должен быть еще одним дополнительным сложным «проектом». Если тестирование станет дополнительным источником боли - оно значительно замедлит развитие или вовсе будет заброшено.
В этом смысле код тестов должен быть максимально простым, с минимальными зависимостями, абстракциями и уровнями косвенности. Большинство советов ниже являются производными от этого принципа.
Раздел 1: Анатомия тестов
1. Используйте подход к написанию и именованию тестов который состоит из 3 частей
Про то как именовать тесты у нас есть отдельная статья на сайте.
✔ Делайте: в отчете о выполненных тестах должно быть указано, удовлетворяет ли текущая версия приложения требованиям для людей, которые не обязательно знакомы с кодом: для тестировщика, разработчика DevOps, который развертывается, и для вас через два года. Это может быть достигнуто , если тесты говорят на уровне требований и состоят из 3 частей:
- Что тестируется? Например, метод
ProductsService.addNewProduct
- При каких обстоятельствах и сценарии? Например, в метод не передается цена
- Каков ожидаемый результат? Например, новый продукт не утвержден
Х В противном случае: Деплой зафейлился, тест с именем «Добавить продукт» не прошел. Это говорит вам, что именно работает со сбоями?
Пример правильной реализации теста, состоящего из 3 частей
//1. unit under test
describe('Products Service', function() {
describe('Add new product', function() {
//2. scenario and 3. expectation
it('When no price is specified, then the product status is pending approval', ()=> {
const newProduct = new ProductService().add(...);
expect(newProduct.status).to.equal('pendingApproval');
});
});
});
✔ Пример правильного использования: Отчет результатов тестирования должен напоминать документ с требованиями:
2. Опишите ожидания на языке продукта: используйте утверждения в стиле BDD тестов
✔ Делайте: Описывайте тесты в человеко-подобном стиле. Это повысит читаемость ваших тестов.
Х В противном случае: Команда напишет меньше тестов и добавит к надоедливым .skip()
которые постоянно фейлятся
Пример Anti Patternа: Тот кто читает отчет о тесте должен просмотреть так же код, чтобы понять что тестирует тест и какой результат выполнения теста.
it("When asking for an admin, ensure only ordered admins in results" , ()={
//assuming we've added here two admins "admin1", "admin2" and "user1"
const allAdmins = getUsers({adminOnly:true});
const 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" , ()={
//assuming we've added here two admins
const allAdmins = getUsers({adminOnly:true});
expect(allAdmins).to.include.ordered.members(["admin1" , "admin2"])
.but.not.include.ordered.members(["user1"]);
});
3. Используйте линтер с плагинами для тестирования
✔ Делайте: Набор плагинов ESLint был создан специально для проверки шаблонов кода тестов и выявления проблем. Например, eslint-plugin-mocha будет предупреждать, когда тест написан на глобальном уровне или когда тесты пропущены, что может привести к ложному убеждению, что все тесты выполняются успешно. Точно так же eslint-plugin-jest может предупредить когда тест не имеет никаких проверок вовсе.
Х В противном случае: Наблюдая за 90-процентным кавереджем кода и 100-процентным успешным выполнением всех тестов у вас будет ложная мысть о том что все хорошо, пока вы не поймете, что многие тесты ничего по факту не делают, а многие тесты просто скипнуты
Пример Anti Patternа: Тест кейсы полны ошибок, благо большинство пойманы линтером
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
});
4. Придерживайтесь black-box тестирования: тестируйте только общедоступные методы
✔ Делайте: Глубокое тестирование внутренних компонентов достаточно трудозатратно. Если ваш код / API возвращает правильные результаты, должны ли вы действительно потратить следующие 3 часа на тестирование, КАК оно работает внутри, а затем поддерживать эти хрупкие тесты? Всякий раз, когда проверяется публичное поведение, частная реализация также неявно тестируется, и ваши тесты будут прерываться, только если есть определенная проблема (например, неправильный вывод). Этот подход также называют поведенческим тестированием. С другой стороны, если вы протестируете внутреннее устройство (подход «белого ящика») - ваш фокус сместится с планирования исходных данных компонента на мельчайшие детали, и ваш тест может сломаться из-за незначительных рефакторингов кода.
Х В противном случае: ваш тест ведет себя как ребенок, который "воет как волк": раздает громкие ложноположительные крики (например, тест не пройден из-за изменения имени закрытой переменной). Неудивительно, что люди скоро начнут игнорировать уведомления CI, пока однажды не будет проигнорирована настоящая ошибка ...
Пример Anti Patternа: Тест кейс покрывает внутреннюю логику без всякой причины.
class ProductService{
//this method is only used internally
//Change this name will make the tests fail
calculateVAT(priceWithoutVAT){
return {finalPrice: priceWithoutVAT * 1.2};
//Change the result format or key name above will make the tests fail
}
//public method
getPrice(productId){
const desiredProduct= DB.getProduct(productId);
finalPrice = this.calculateVATAdd(desiredProduct.price).finalPrice;
}
}
it("White-box test: When the internal methods get 0 vat, it return 0 response", async () => {
//There's no requirement to allow users to calculate the VAT, only show the final price. Nevertheless we falsely insist here to test the class internals
expect(new ProductService().calculateVATAdd(0).finalPrice).to.equal(0);
});
️️5. Выберайте правильные test doubles: изебгайте моков в пользу stubs и spies
✔ Делайте: Test doubles являются неизбежным злом, потому что они связаны с внутренними компонентами приложения, но некоторые предоставляют огромное значение. Используйте test doubles чтобы избежать тестирования не общедоступных методов (то что описывалось в прошлом пункте)
Прежде чем использовать test doubles, задайте очень простой вопрос: я использую его для проверки функциональности, которая появляется или может появиться в документе с требованиями? Если нет, это запашок тестирования "белого ящика".
Например, если вы хотите проверить, как ваше приложение ведет себя когда платежный сервис не работает, вы можете заблокировать платежный сервис и вызвать возврат «Нет ответа», чтобы убедиться, что тестируемое устройство возвращает правильное значение. Это проверяет поведение / ответ / результат нашего приложения при определенных сценариях. Вы также можете использовать spies, чтобы утверждать, что электронное письмо было отправлено, когда эта служба не работает - это опять-таки поведенческая проверка, которая, вероятно, появится в документе с требованиями («Отправьте электронное письмо, если платеж не может быть сохранен»).
Х В противном случае: Любой рефакторинг кода требует поиска всех моков в коде и соответствующего его апдейта. Тесты становятся напрягом, а не полезным другом
Пример Anti Patternа: Когда мы мокаем внутренний закрытый код
t("When a valid product is about to be deleted, ensure data access DAL was called once, with the right product and right config", async () => {
//Assume we already added a product
const dataAccessMock = sinon.mock(DAL);
//hmmm BAD: testing the internals is actually our main goal here, not just a side-effecr
dataAccessMock.expects("deleteProduct").once().withArgs(DBConfig, theProductWeJustAdded, true, false);
new ProductService().deletePrice(theProductWeJustAdded);
mock.verify();
Пример правильной реализации: spies сосредоточены на проверке требований, но как побочный эффект неизбежно касаются внутренних методов
it("When a valid product is about to be deleted, ensure an email is sent", async () => {
//Assume we already added here a product
const spy = sinon.spy(Emailer.prototype, "sendEmail");
new ProductService().deletePrice(theProductWeJustAdded);
//hmmm OK: we deal with internals? Yes, but as a side effect of testing the requirements (sending an email)
});
6. Не используйте «foo», используйте реалистичные входные данные
✔ Делайте: Часто production ошибки выявляются при каких-то очень специфических и неожиданных данных - чем более реалистичны тестовые данные, тем выше шансы на раннее обнаружение ошибок. Используйте специальные библиотеки, такие как Faker, для генерации псевдо-реальных данных, которые напоминают разнообразие и форму production данных. Например, такие библиотеки будут генерировать случайные, но реалистичные телефонные номера, имена пользователей, кредитные карты, названия компаний и даже текст «lorem ipsum». Попробуйте даже импортировать реальные данные из production среды и использовать их в своих тестах. Хотите поднять его на следующий уровень? см. следующий пункт (property-based тестирование)
Х В противном случае: Все ваши дев тесты будут ошибочно казаться зелеными, когда вы используете синтетические входные данные, такие как «Foo», но на продакшне код может зафейлится, когда хакер передает неприятную строку, такую как «@ 3e2ddsf». ## ’1 fdsfds. fds432 AAAA ”
Пример Anti Patternа: Набор тестов, который проходит из-за нереалистичных данных
const addProduct = (name, price) =>{
const productNameRegexNoSpace = /^\S*$/;//no white-space allowd
if(!productNameRegexNoSpace.test(name))
return false;//this path never reached due to dull input
//some logic here
return true;
};
it("Wrong: When adding new product with valid properties, get successful confirmation", async () => {
//The string "Foo" which is used in all tests never triggers a false result
const addProductResult = addProduct("Foo", 5);
expect(addProductResult).to.be.true;
//Positive-false: the operation succeeded because we never tried with long
//product name including spaces
});
Пример правильной реализации:
it("Better: When adding new valid product, get successful confirmation", async () => {
const addProductResult = addProduct(faker.commerce.productName(), faker.random.number());
//Generated random input: {'Sleek Cotton Computer', 85481}
expect(addProductResult).to.be.true;
//Test failed, the random input triggered some path we never planned for.
//We discovered a bug early!
});
7. Протестируйте множество входных комбинаций, используя Property-based тестирование.
✔ Делайте: Обычно мы выбираем несколько input вариантов для каждого теста. Даже когда формат ввода напоминает данные реального мира (см. Пункт «Не foo»), мы покрываем только несколько комбинаций ввода (метод ('', true, 1), метод (“string”, false ”, 0) ), Однако, в продакшн API, вызываемый с 5 параметрами, может быть вызван с тысячами различных перестановок.
Что если бы вы могли написать один тест, который автоматически отправляет 1000 перестановок различных входных данных и улавливает, для какого ввода наш код не возвращает правильный ответ? Тестирование на основе свойств - это метод, который делает именно это: посылая все возможные входные комбинации на тестируемое устройство, вы увеличиваете вероятность обнаружения ошибки. Например, для данного метода - addNewProduct (id, name, isDiscount) - вспомогательные библиотеки будут вызывать этот метод с множеством комбинаций (число, строка, логическое значение), например (1, «iPhone», false), (2, «Galaxy» ", правда). Вы можете запустить тестирование на основе свойств, используя ваш любимый test runner (Mocha, Jest и т. Д.), Используя такие библиотеки, как js-verify, checkout fast-check, или testcheck.
Х В противном случае: Обычно вы выбираете входные данные теста, которые охватывают только те случаи в коде, которые работают хорошо. К сожалению, это снижает эффективность тестирования как средство выявления ошибок
Пример правильной реализации:
require('mocha-testcheck').install();
const {expect} = require('chai');
const faker = require('faker');
describe('Product service', () => {
describe('Adding new', () => {
//this will run 100 times with different random properties
check.it('Add new product with random yet valid properties, always successful',
gen.int, gen.string, (id, name) => {
expect(addNewProduct(id, name).status).to.equal('approved');
});
})
});
8. Оставайтесь в рамках теста: минимизируйте внешние helperы и абстракции
✔ Делайте: Позвольте тому кто читает ваш код понять ваш код, не выходя из теста, свести к минимуму утилиты, хуки или любое внешнее воздействие на ваш тест кейс. Слишком много повторений и копирование? ОК, тест можно оставить с одним внешним хелпером и оставить его очевидным. Но когда он растет до трех и четырех хелперов и хуков, это означает, что вы формируете сложную структуру.
Х В противном случае: Вдруг оказалось что вы имеете 4 хелпера на каждый набор тестов, 2 из которых наследуют от базового Util, так же много хуков? Поздравляем, вы только что выиграли еще один сложный проект для поддержки.
Пример Anti Patternа: Непонятная и запутанная структура теста. Вы понимаете тестовый пример без перехода к внешним зависимостям?
test("When getting orders report, get the existing orders", () => {
const queryObject = QueryHelpers.getQueryObject(config.DBInstanceURL);
const reportConfiguration = ReportHelpers.getReportConfig();//What report config did we get? have to leave the test and read
userHelpers.prepareQueryPermissions(reportConfiguration);//what this one is doing? have to leave the test and read
const result = queryObject.query(reportConfiguration);
assertThatReportIsValid();//I wonder what this one does, have to leave the test and read
expect(result).to.be.an('array').that.does.include({id:1, productd:2, orderStatus:"approved"});
//how do we know this order exist? have to leave the test and check
})
Пример правильной реализации: Тест, который вы можете прочитать и понять, не просматривая различные файлы проекта
it("When getting orders report, get the existing orders", () => {
//This one hopefully is telling a direct and explicit story
const orderWeJustAdded = ordersTestHelpers.addRandomNewOrder();
const queryObject = newQueryObject(config.DBInstanceURL, queryOptions.deep, useCache:false);
const result = queryObject.query(config.adminUserToken, reports.orders, pageSize:200);
expect(result).to.be.an('array').that.does.include(orderWeJustAdded);
})
9. Избегайте глобальных тестовых фикстур и seed методов, добавляйте данные для каждого теста внутри тестов
✔ Делайте: Следуя пункту 0, каждый тест должен добавлять и воздействовать с своим собственным набором строк в БД, чтобы предотвратить связывание с другими тестами. В действительности это часто нарушается тестерами, которые загружают данные в БД перед запуском тестов (также называемых «text fixture») для повышения производительности. Хотя производительность действительно является серьезной проблемой - ее можно митигейтить (см. пункт 13), однако, сложность тестов - это очень болезненная вещь. Практически нужно сделать каждый тест явно добавляющим необходимые ему записи БД и действуйте только на эти записи. Если производительность становится критической проблемой - сбалансированный компромисс может прийти в виде общих методов которые не изменяют данные (например, запросы)
Х В противном случае: Несколько тестов не прошли, деплой прерван, наша команда собирается потратить драгоценное время, есть ли у нас ошибка? давайте исследуем, о нет - похоже, два теста работали с одними данными
Пример Anti Patternа: Тесты не являются независимыми и полагаются на некоторые глобальные хуки для обработки глобальных данных с БД
before(() => {
//adding sites and admins data to our DB. Where is the data? outside. At some external json or migration framework
await DB.AddSeedDataFromJson('seed.json');
});
it("When updating site name, get successful confirmation", async () => {
//I know that site name "portal" exists - I saw it in the seed files
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 () => {
//I know that site name "portal" exists - I saw it in the seed files
const siteToCheck = await SiteService.getSiteByName("Portal");
expect(siteToCheck.name).to.be.equal("Portal"); //Failure! The previous test change the name :[
});
Пример правильной реализации: Мы можем оставаться в рамках теста, каждый тест выполняется с своим набором тестовых данных
10. Не юзайте catch для ошибок, ожидайте их
✔ Делайте: При попытке использования Assertов, некоторые входные данные вызывают ошибку, может показаться правильным использовать try-catch-finally и Assert'ить, catch блок отработал. Результатом является плохой тест кейс (пример ниже), который скрывает простую цель теста и ожидания результата
Более элегантной альтернативой является использование однострочного выделенного Chai assertion: expect(method).to.throw (или в Jest: expect(method).toThrow()). Обязательно также убедиться, что исключение содержит свойство, которое сообщает тип ошибки, в противном случае, учитывая только общую ошибку, приложение не сможет сделать ничего кроме того как показывать пользователю генерик сообщение об ошибке.
Х В противном случае: Из отчета запусков тестов (например в CI) будет сложно понять что пошло не так.
Пример Anti Patternа: Длинный тест, который пытается доказать наличие ошибки с помощью блоков try-catch
it("When no product name, it throws error 400", async() => {
let errorWeExceptFor = null;
try {
const result = await addNewProduct({name:'nest'});}
catch (error) {
expect(error.code).to.equal('InvalidInput');
errorWeExceptFor = error;
}
expect(errorWeExceptFor).not.to.be.null;
//if this assertion fails, the tests results/reports will only show
//that some value is null, there won't be a word about a missing Exception
});
Пример правильной реализации: Человеко-понятные описания теста должны быть читаемы не только для разработчика а так же для QA или даже для PMа
10. Используйте теги в ваших тестах
✔ Делайте: Различные тесты должны выполняться в разных сценариях: Вы можете помечать тесты ключевыми словами, такими как #cold #api #sanity, чтобы вы могли группировать ваши тесты и запускать нужные группы.
Х В противном случае: Запуск всех тестов, включая тесты, которые выполняют десятки запросов к БД, каждый раз, когда разработчик вносит небольшие изменения, может быть очень медленным и не позволяет разработчикам запускать тесты для проверки конкретного функционала.
Пометка тестов как "#cold-test" позволяет исполнителю тестов выполнять только быстрые тесты (Cold === быстрые тесты, которые не делают трудозатратных операций и могут выполняться часто, даже когда разработчик печатает)
//this test is fast (no DB) and we're tagging it correspondigly
//now the user/CI can run it frequently
describe('Order service', function() {
describe('Add new order #cold-test #sanity', function() {
it('Scenario - no currency was supplied. Excpectation - Use the default currency #sanity', function() {
//code logic here
});
});
});
11. TDD
Изучайте и применяйте принципы TDD - они чрезвычайно важны. Пишите тесты перед написанием кода и будет вам счастье.
Раздел 2: Типы тестов
12. Использование пирамиды тестирования или принцип "рожка мороженого"
Типы тестирования на разных этапах жизненого цикла текущей релиз версии:
13. Компонентное тестирование
✔ Делайте: каждый unit тест покрывает крошечную часть приложения, и охватить его целиком очень долго тогда как end-to-end тестирование легко охватывает большой объем, но является нестабильным и медленным, почему бы не применить сбалансированный подход и написать тесты, которые больше, чем юнит-тесты, но меньше, чем end-to-end тесты? Компонентное тестирование - это незамеченная песня мира тестирования - они обеспечивают лучшее из обоих миров: разумную производительность и возможность применять шаблоны TDD + хороший процент покрытия.
Компонентное тестирование фокусируются на «юните» микросервиса, они работают против API, не имитируют ничего, что принадлежит самому микросервису (например, реальная БД или, по крайней мере, версия этой БД в памяти), но заглушают все, что является внешним как вызовы к другим микросервисам. Таким образом мы тестируем то, что мы развертываем, приближаем приложение к внешнему виду и получаем большую уверенность в разумные сроки.
Х В противном случае: Вы можете потратить много времени на написание модульных тестов, чтобы узнать, что вы получили только 20% покрытия тестами.
Пример правильной реализации: Supertest позволяет работать с Express API в процессе (быстро и охватывать много слоев)
14. Убедитесь, что новые релизы не нарушают API, используя потребительские контракты
✔ Делайте: У вашего микросервиса может быть несколько клиентов, и вы выпускаете несколько версий микросервиса для совместимости. Затем вы изменяете какое-то поле и «бум!», Один важный клиент, который использует это поле, теперь очень зол на вас. Для серверной стороны очень сложно учесть все ожидания множества клиентов. С другой стороны, клиенты не могут выполнять какое-либо тестирование, поскольку сервер контролирует даты выпуска.
Х В противном случае: Вам нужно будет тестировать в ручную или у вас будет страх деплоя новых версий
Пример правильной реализации: Использовать брокера на подобии PACT
15. Тестируйте ваши middlewares изолированно
✔ Делайте: Многие избегают тестирования Middleware, потому что они представляют небольшую часть системы и требуют работающего экспресс-сервера. Обе причины ошибочны - Middleware это маленькая часть приложения, но влияет на все или на большинство запросов и может быть легко протестировано как обычные функции, которые получают JS объекты {request, response}. Чтобы протестировать Middleware, можно использовать например, с помощью Sinon) за взаимодействием с объектами {request, response}, чтобы убедиться, что функция выполнила правильное действие. Библиотека node-mock-http идет еще дальше и учитывает объекты {request, response}, а также следит за их поведением. Например, он может ассертить, соответствует ли состояние http, установленное для объекта res, ожиданиям (см. Пример ниже)
Х В противном случае: Ошибка в Middleware === ошибка во всех или большинстве запросов
Пример правильной реализации: Тестирование middleware в отдельности без сетевых вызовов.
//the middleware we want to test
const unitUnderTest = require('./middleware')
const httpMocks = require('node-mocks-http');
//Jest syntax, equivelant to describe() & it() in 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);
});
16. Измерение и рефакторинг с использованием инструментов статического анализа.
✔ Делайте: Использование инструментов статического анализа помогает, предоставляя объективные способы улучшить качество кода и обеспечить его поддержку. Вы можете добавить инструменты статического анализа в свою сборку CI, чтобы прервать ее при обнаружении запахов кода. Его основные преимущества при использовании простого линтинга - это возможность проверять качество в контексте нескольких файлов (например, обнаруживать дубликаты), выполнять расширенный анализ (например, сложность кода) и следить за историей и развитием проблем с кодом. Два примера инструментов, которые вы можете использовать:Sonarqube, Code Climate
Х В противном случае: при низком качестве кода ошибки и производительность всегда будут проблемой, которую не может исправить ни одна блестящая новая библиотека или современные функции
Пример правильной реализации: CodeClimat, коммерческая апликуха, которая может помочь в анализе кода.
17. Проверьте свою готовность к хаосу
✔ Делайте: Как ни странно, большинство программных тестов касается только логики и данных, но некоторые из худших вещей, которые случаются (и где действительно трудно смягчить риски), - это проблемы инфраструктуры. Например, проверяли ли вы когда-нибудь, что происходит, когда ваша память процесса перегружена, или когда сервер / процесс умирает, или ваша система мониторинга понимает, когда API становится на 50% медленнее? Чтобы протестировать и уменьшить вероятность возникновения подобных плохих вещей, была создана парадигма Chaos engineering от Netflix. Она призвана помочь предоставляя инструменты и фреймворки для проверки устойчивости нашего приложения к "хаосу". Например, один из его известных инструментов, chaos monkey, она случайным образом убивает серверы, чтобы гарантировать, что наш сервис все еще может обслуживать пользователей и не полагаться на один сервер (есть также версия Kubernetes, kube-monkey, которая убивает модули). Все эти инструменты работают на уровне хостинга / платформы.
Х В противном случае: Здесь нет выхода, закон Мерфи ударит по твоему продакшину безпощадно
Пример правильной реализации: Node-chaos может генерировать всевозможные пранкиNode.js, чтобы вы могли проверить, насколько устойчиво ваше приложение к хаосу.
Раздел 3: Измерение эффективности теста
18. Покройте достаточно тестов, чтобы быть уверенным в своем продукте, ~ 80% смотрится уже неплохо
✔ Делайте: Цель тестирования - получить достаточную уверенность в своем коде, очевидно, чем больше тестируется кода, тем увереннее может быть команда. Coverage - это мера того, сколько строк кода (и ветвей, операторов и тд) Было пройдено тестами. Так сколько достаточно? Очевидно, что 10–30% слишком мало, чтобы иметь представление о правильности сборки, с другой стороны, 100% очень дорого и могут сместить ваш фокус с критических путей на экзотические углы кода. Длинный ответ заключается в том, что это зависит от многих факторов, таких как тип приложения - если вы создаете следующее поколение Airbus A380, то 100% является обязательным, для веб-сайта с мультипликационными изображениями 50% может быть слишком много. Хотя большинство энтузиастов тестирования утверждают, что правильный порог охвата является контекстуальным, большинство из них также упоминают число 80% в качестве правила.
Советы по внедрению. Возможно, вы захотите настроить непрерывную интеграцию (CI), чтобы иметь порог покрытия и остановить сборку, которая не соответствует этому стандарту (также можно настроить порог для каждого компонента, см. Пример кода ниже) , Кроме того, рассмотрите возможность обнаружения уменьшения охвата сборки (когда новый зафиксированный код имеет меньшее покрытие) - это подтолкнет разработчиков к увеличению или, по крайней мере, сохранению объема тестируемого кода. Все это говорит о том, что процент покрытия- это всего лишь одна мера, количественная оценка, которой недостаточно для определения надежности вашего тестирования. И это также можно обмануть, как показано в следующих пунктах
Пример правильной реализации: Настройте покрытия для каждого компонента (используя Jest)
19. Изучите отчеты о покрытии (coverage reports), чтобы обнаружить области которые еще не тестировались и другие странности
✔ Делайте: Некоторые проблемы "скрываются под радаром" и их трудно найти с помощью традиционных инструментов. Это на самом деле не ошибки, а скорее удивительное поведение приложения, которое может оказать серьезное влияние. Например, некоторые области кода никогда или редко вызываются - вы думали, что класс 'PricingCalculator
' всегда устанавливает цену продукта, но оказывается, что он фактически никогда не вызывается, хотя у нас есть 10000 продуктов в БД и много продаж… Покрытие кода отчеты помогают понять, ведет ли себя приложение так, как вы считаете. Помимо этого, он также может выделить, какие типы кода не тестировались - информация о том, что 80% кода протестировано, не говорит о том, покрыты ли критические части. Генерация отчетов проста - просто запустите ваше приложение в продакшине или во время тестирования с отслеживанием покрытия, а затем просмотрите красочные отчеты, показывающие, как часто вызывается каждая область кода. Если вы потратите время, чтобы взглянуть на эти данные - вы можете найти некоторые ошибки
Х В противном случае: Если вы не знаете, какие части вашего кода не проверены, вы не знаете, откуда могут возникнуть проблемы.
Пример Anti Patternа:
Что не так с этим coverage report'om? на основе реального сценария, в котором мы отслеживали использование нашего приложения в QA и выясняли интересные шаблоны входа в систему (Подсказка: количество неудачных попыток входа в систему непропорционально, что-то явно не так. Наконец оказалось, что некоторая ошибка внешнего интерфейса продолжает попадать в API входа в бэкэнд)
20. Измерение логического покрытия тестов с помощью мутационного тестирования (mutation testing)
✔ Делайте: Показатель традиционного покрытия часто может ввести в заблуждение: он может показать вам 100% покрытие кода, но ни одна из ваших функций, не даст правильного ответа. Как так? он просто измеряет, по каким строкам кода был посещен тест, но не проверяет, действительно ли тесты что-либо тестировали, - утверждает правильный ответ. Как кто-то, кто путешествует по делам и показывает свои штампы в паспорте - это не доказывает никакой проделанной работы, только то, что он посетил несколько аэропортов и возмоно отелей.
Тестирование на базе мутаций (mutation testing) поможет вам измерить объем кода, который на самом деле был протестирован, а не просто был запущен. Stryker - это библиотека JavaScript для тестирования мутаций, рекомендуем использовать ее.
Х В противном случае: Вы будете обмануты, полагая, что 85% покрытия означает, что ваш тест будет обнаруживать ошибки в 85 процентах кода.
Пример Anti Patternа:100% покрытие, 0% протестировано
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({asignee: "John@mailer.com",price: 120});
});//Это даст 100% coverage, но тест не проверяет ничего
Пример правильной реализации: Отчеты Stryker, инструмент для тестирования мутаций, обнаруживает и подсчитывает количество кода, который не был протестирован
Раздел 4: CI & и другие показатели качества
21. Добавте линтеры к вашему проекту
✔ Делайте: Линтеры - с 5-минутной настройкой вы получаете бесплатно автопилота, который защищает ваш код и обнаруживает значительные проблемы при вводе текста. В настоящее время линтеры могут обнаружить серьезные проблемы. В дополнение к вашему базовому набору правил (например, стандарт ESLint или стиль Airbnb).
Х В противном случае: Рассмотрим вариант когда ваш продакшн продолжает падать, но логи не отображают трассировку стека ошибок. Что случилось? Ваш код по ошибке выбросил объект без ошибок, и трассировка стека была потеряна, что является хорошей причиной для того, чтобы зафейспалмить. 5-минутная установка линтера может обнаружить этот тип и сохранить ваш день.
Пример Anti Patternа: Неправильный объект Error генерируется по ошибке, для этой ошибки не будет отображаться трассировка стека.
22. Сократите цикл обратной связи
✔ Делайте: Использование CI с хорошими проверками качества, такими как тестирование, линтинг, проверка уязвимостей и т. Д. Помогите разработчикам запустить этот конвейер также локально, чтобы получить мгновенную обратную связь и сократить цикл обратной связи. Зачем? Эффективный процесс тестирования состоит из множества итерационных циклов: (1) попытки -> (2) обратная связь -> (3) рефакторинг. Чем быстрее обратная связь, тем больше итераций по улучшению разработчик может выполнить для каждого модуля и улучшить результаты. С другой стороны, когда обратная связь опаздывает, может быть упаковано меньше итераций улучшения в один день, команда может уже перейти к другой теме / задаче / модулю и, возможно, не сможет усовершенствовать этот модуль.
Х В противном случае: Когда результаты качества приходят на следующий день после коммита кода, тестирование не становится беглой частью разработки, а становится формальным артефактом после.
Пример правильной реализации: скпипты 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"
}
}
23. Проведите end-2-end -тестирование на настоящем зеркале продакшина.
✔ Делайте: Сквозное (e2e) тестирование является основной задачей каждого CI pipeline - создание идентичного эфемерного рабочего зеркала на лету со всеми связанными облачными сервисами может быть утомительным и дорогостоящим. Найти лучший компромисс Docker-compose позволяет создавать изолированную докеризированную среду с идентичными контейнерами, используя один простой текстовый файл, но технология поддержки (например, сеть, модель развертывания) отличается от реальной в продакшине. Вы можете комбинировать его с «AWS Local» для работы с заглушкой реальных сервисов AWS. Если вы перешли на несколько серверов, например, серверные и AWS SAM разрешает локальный вызов кода Faas.
Х В противном случае: Использование различных технологий для продакшина и тестирования требует поддержки двух моделей развертывания и разделения разработчиков и команды DevOps'ов
Пример правильной реализации: CI pipeline, который генерирует 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. Запускайте свои тесты параллельно
✔ Делайте: Если все сделано правильно, тестирование - ваш друг 24/7, предоставляющий практически мгновенную обратную связь. На практике выполнение 500 CPU-bounded модульного теста на одном потоке может занять слишком много времени. К счастью, современные тестеры и CI-платформы (такие как расширения Jest, AVA и Mocha) могут распараллелить тест на несколько процессов и добиться значительного улучшения времени обратной связи. Некоторые поставщики CI также распараллеливают тесты между контейнерами (!), Что еще больше сокращает цикл обратной связи. Либо локально через несколько процессов, либо через некоторый облачный CLI, использующий несколько машин - распараллеливание требований, сохраняющих тесты автономными, поскольку каждый из них может выполняться на разных процессах
Х В противном случае: Получение результатов теста через 1 час после коммитанового кода, когда вы уже делаете другую задачу, является отличным рецептом для того, чтобы сделать тестирование менее актуальным.
Пример правильной реализации: Mocha parallel & Jest легко опережает традиционный Mocha благодаря параллелизму
25. Держитесь подальше от юридических вопросов, используя проверку лицензии и плагиата
✔ Делайте: Вопросы лицензирования и плагиата, вероятно, не являются вашей главной задачей сейчас, но почему бы не поставить галочку в этом поле и через 10 минут? Куча пакетов npm, таких как license check and plagiarism check, может быть легко встроена в ваш CI pipeline и проверять наличие проблем, таких как зависимости с ограничительными лицензиями или код, который был скопирован из Stackoverflow и, очевидно, нарушает некоторые авторские права.
Х В противном случае: Разработчики могут непреднамеренно использовать пакеты с неподходящими лицензиями или путем copy-paste добавить коммерческий код и столкнуться с юридическими проблемами.
Пример правильной реализации: Тестирование
npm install -g license-checker
//ask it to scan all licenses and fail with an exit code other than 0 if it found the unauthorized license. The CI system should catch this failure and stop the build
license-checker --summary --failOn BSD
26. Постоянно проверять наличие уязвимых зависимостей
✔ Делайте: Даже самые уважаемые зависимости, такие как Express, имеют известные уязвимости. Это можно легко использовать, используя инструменты сообщества, такие как npm audit или коммерческие инструменты, такие как snyk (также предлагается бесплатная версия). Оба могут быть вызваны из вашего CI при каждой сборке.
Х В противном случае: Для обеспечения чистоты кода от уязвимостей без использования специальных инструментов потребуется постоянно следить за публикациями в Интернете о новых угрозах.
27. Автоматизируйте обновления зависимостей
✔ Делайте: ncu можно использовать вручную или внутри CI Pipeline для определения степени отставания кода от последних версий.
28. Прочие, не связанные с Node.js, советы по CI
✔ Делайте:
- Используйте декларативный синтаксис. Это единственный вариант для большинства вендоров, но более старые версии Jenkins позволяют использовать код или UI
- Выберите вендора, который имеет встроенную поддержку Docker
- Сначала запустите ваши самые быстрые тесты. Создайте шаг / этап «Дымового тестирования», который группирует несколько быстрых проверок (например, linting, модульные тесты) и обеспечивает быструю обратную связь с коммиттером кода.
- Упростите просмотр всех артефактов сборки, включая отчеты о тестировании, отчеты о покрытии, отчеты о мутациях, логи и тд.
- Создайте несколько pipelines/jobs для каждого события, переиспользуйте шаги между ними. Например, настройте джобу для feature branch commits и другое для мастера.
- Никогда не вставляйте секретные данные декларацию джобы, вычитывайте ее из конфигурации.
- Явно поднять версию в сборке релиза или, по крайней мере, убедиться, что разработчик сделал это
- Используйте концепцию докера
Х В противном случае: Вы упустите годы мудрости