The Pragmatic Programmer представляет метод тестирования, называемый тестированием на основе свойств, в котором приводится пример на языке Python с использованием фреймворка hypothesis.
Использование hypothesis очень интуитивно понятно и просто, и прекрасно представляет концепцию тестирования на основе свойств. Поэтому я также хотел найти эквивалентную альтернативу в Node. Два из них имеют высокий рейтинг на Github: JSVerify с 1,6 тыс. звезд и fast-check с 2,8 тыс. звезд. Поэтому я выделил некоторое время, чтобы немного изучить fast-check
и попытаться приблизить его к моей повседневной работе.
Эта статья является кратким обзором и простым примером для документирования опыта.
Почему тестирование на основе свойств?
Прежде чем приводить примеры, давайте объясним, почему мы используем тесты на основе свойств. На самом деле, мне не нравится термин «имущественные». Я бы сказал «чрезвычайно объемное» тестирование.
Все мы знаем, что пирамида тестирования выглядит следующим образом.
И в своей предыдущей статье я упоминал, в чем разница между модульными и интеграционными тестами. Чем ниже уровень пирамиды, тем больше тестовых случаев требуется.
Но даже в этом случае трудно сгенерировать большое количество тестовых случаев. Обычно мы пишем соответствующие тесты на основе известных условий или спецификаций продукта, иногда мы можем вспомнить о написании граничных тестов (иногда нет), а иногда мы можем полагаться на простую случайную проверку функциональности, например, с помощью фейкера.
Однако в целом, даже если мы изо всех сил стараемся придумать тестовые случаи, мы не можем охватить все сценарии, и мы называем этот метод тестирования тестированием на примерах. Это связано с тем, что тестовые случаи, которые мы придумываем, в основном расширяются на основе определенного примера и не могут охватить все неизвестные контексты или проверить все граничные условия.
На данном этапе мы хотели бы, чтобы фреймворк автоматически генерировал достаточное количество сценариев (разумных или нет) для проверки написанного нами кода, а написанные нами тестовые случаи должны лишь обеспечивать корректность их «свойств». Это и есть истоки тестирования на основе свойств.
Тем не менее
Реальность такова, что интеграционное тестирование — это примерно то же самое, что и модульное тестирование.
Я работал во многих организациях, от крупных национальных предприятий до небольших стартапов. Независимо от того, являюсь ли я разработчиком или наставником, исходя из прошлого опыта, модульное тестирование примерно так же актуально, как и интеграционное.
Для большинства разработчиков правильно разделить модульное тестирование и интеграционное тестирование — непростая задача. Чтобы иметь возможность полностью разделить тестовые случаи, им необходимо обладать навыками паттернов проектирования, инъекции зависимостей, инверсии зависимостей и т.д., чтобы сделать это хорошо. Поэтому большинство тестовых сред основаны на конкретном тестовом окружении, например, использование docker-compose
для создания одноразовой базы данных и тестовых данных и тестирования на ней.
Документы fast-check
написаны на основе стандарта юнит-тестов, и, похоже, предусмотрен только проверочный булеан, то есть fc.assert
, поэтому я потратил некоторое время на исследование, чтобы написать тестовый пример, близкий к ежедневному использованию.
В общем, мне нужно несколько способностей.
- Уметь тестировать async/await.
- Уметь проверять больше контекстов, таких как
assertEqual
.
Введение в fast-check
Прежде чем мы начнем писать тестовые примеры, давайте посмотрим на базовое использование fast-check
.
Сначала представим структуру fast-check
.
- Утверждение (fc.assert)
- Свойства (fc.property или fc.asyncProperty)
Функция fc.assert
заключается в проверке корректности всех тестов, автоматически генерируемых свойствами. Свойства необходимы для описания двух важных блоков.
- Runner
- Arbitraries
Runner — это тестируемый контекст, т.е. цель. С другой стороны, арбитражи — это входные параметры цели, которые автоматически генерируются свойствами, и все, что нам нужно сделать, это задать для них правила, например, только целые числа.
Ниже приведен простой пример.
fc.assert(
fc.property(fc.integer(), fc.integer(), (i, j) => {
return i + j === add(i, j);
})
);
Два fc.integer()
являются арбитражами, а последующая анонимная функция — бегунком, который принимает два аргумента i
и j
, соответствующие предыдущим арбитражам. Мы хотим проверить, действительно ли функция add
правильно суммирует два аргумента, так что результат add
должен соответствовать +
.
Давайте рассмотрим два требования, которые мы только что упомянули.
- Хотя нашей целью тестирования является
add
, но хорошая интеграция с некоторыми условиями в бегунке может сделать не только эффект boolean.
Примеры быстрой проверки
Теперь перейдем к более практическому примеру. Предположим, у меня есть таблица базы данных с деньгами для каждого пользователя.
идентификатор пользователя | деньги |
---|---|
123 | 100 |
456 | 200 |
абк | 9999 |
def | 0 |
Существует функция async function getMoney(limit)
, которая будет сортировать деньги в порядке возрастания, а также определять, сколько денег вернуть, основываясь на параметрах.
Теперь мы хотим протестировать этот «черный ящик».
describe("fast-check test", () => {
before(async () => {
// generate 10 random records
});
it("#1", async () => {
const result = await getMoney(100);
expect(result.length).to.be.equal(10);
});
it("#2", async () => {
await fc.assert(
fc.asyncProperty(fc.integer(), async (i) => {
const result = await getMoney(i);
return result.length <= 10 && result.length >= 0;
})
);
});
it("#3", async () => {
await fc.assert(
fc.asyncProperty(fc.integer({ min: 0, max: 10 }), async (i) => {
const result = await getMoney(i);
return result.length === i;
})
);
});
it("#4", async () => {
await fc.assert(
fc.asyncProperty(fc.integer(), async (i) => {
const result = await getMoney(i);
if (result.length > 1) {
let prev = parseFloat(result[0]);
for (let i = 1; i < result.length; i++) {
const curr = parseFloat(result[i]);
if (curr < prev) {
return false;
}
prev = curr;
}
}
return true;
})
);
});
});
Позвольте мне объяснить вкратце.
- Просто проверьте, что функция действительно работает, здесь не используется
fast-check
. - Учитывая произвольное целое число, длина возвращаемого результата должна быть между 0 и 10, потому что мы создали только десять записей в
before
. - Учитывая диапазон целых чисел, длина возвращаемого результата должна быть равна заданной длине.
- Убедитесь, что порядок всего массива действительно является возрастающим. Из этого бегунка видно, что даже очень сложные условия могут быть проверены, но будьте осторожны, чтобы не допустить ошибок в тестовом примере, приводящих к необходимости проверки тестового примера.
Если проблема обнаружена, fast-check
также сообщит вам, какой вид арбитража он использует для обнаружения проблемы. Например,
Контрпример: [-1234567890]
Это означает, что тест не прошел, когда i = -1234567890
. Возможно, отрицательное число обрабатывается неправильно или «большое» отрицательное число обрабатывается неправильно. Это самое время написать настоящий модульный тест (или интеграционный тест) и проверить -1234567890, чтобы потом использовать такой неудачный случай в качестве регрессионного теста.
Заключение
В идеале при тестировании подобного поведения базы данных мы должны использовать такие методы, как инъекция зависимостей, чтобы изолировать физическую базу данных для повышения производительности тестирования. Но, как я уже говорил, не так-то просто правильно отделить код от внешних зависимостей в зависимости от опыта и квалификации разработчика.
Поэтому во многих организациях мы все еще видим, что большинство тестовых примеров должны полагаться на физическую базу данных для тестирования. Но я должен сказать, что это неверно.
В этой статье я объясняю использование fast-check
на реальном примере и то, насколько он близок к практике. Тем не менее, я надеюсь, что нам не придется столкнуться с этим снова, по крайней мере, прочитав мою предыдущую статью, давайте попробуем перевернуть эти неразумные тест-кейсы.