В идеальном мире вы пишете доказательства вместо тестов. Например, рассмотрим следующие функции.
const negate = (x: number): number => -x;
const reverse = (x: string): string => x.split("").reverse().join("");
const transform = (x: number|string): number|string => {
switch (typeof x) {
case "number": return negate(x);
case "string": return reverse(x);
}
};
Допустим, вы хотите доказать, что transform
, примененный дважды, является идемпотентным , т. Е. Для всех действительных входных данных x
, transform(transform(x))
-равно x
. Что ж, сначала нужно доказать, что negate
и reverse
, примененные дважды, являются идемпотентными. Теперь предположим, что доказательство идемпотентности negate
и reverse
, примененных дважды, тривиально, т.е. компилятор может это выяснить. Таким образом, у нас есть следующие леммы .
const negateNegateIdempotent = (x: number): negate(negate(x))≡x => refl;
const reverseReverseIdempotent = (x: string): reverse(reverse(x))≡x => refl;
Мы можем использовать эти две леммы, чтобы доказать, что transform
является идемпотентом следующим образом.
const transformTransformIdempotent = (x: number|string): transform(transform(x))≡x => {
switch (typeof x) {
case "number": return negateNegateIdempotent(x);
case "string": return reverseReverseIdempotent(x);
}
};
Естьздесь многое происходит, поэтому давайте разберемся с этим.
- Так же, как
a|b
является типом объединения, а a&b
является типом пересечения, a≡b
является типом равенства. - Значение
x
типа равенства a≡b
является доказательством равенства a
и b
. - Если два значения,
a
и b
, не являютсяравным тогда невозможно построить значение типа a≡b
. - значение
refl
, сокращенное до рефлексивность , имеет тип a≡a
. Это тривиальное доказательство того, что значение равно самому себе. - Мы использовали
refl
в доказательстве negateNegateIdempotent
и reverseReverseIdempotent
. Это возможно, потому что предложения достаточно тривиальны для автоматического подтверждения компилятором. - Мы используем леммы
negateNegateIdempotent
и reverseReverseIdempotent
для доказательства transformTransformIdempotent
. Это пример нетривиального доказательства.
Преимущество написания доказательств состоит в том, что компилятор проверяет доказательство. Если доказательство неверно, то программе не удается проверить тип, и компилятор выдает ошибку. Доказательства лучше, чем тесты по двум причинам. Во-первых, вам не нужно создавать тестовые данные. Трудно создать тестовые данные, которые обрабатывают все крайние случаи. Во-вторых, вы не забудете случайно протестировать любые крайние случаи. Если вы это сделаете, компилятор выдаст ошибку.
К сожалению, TypeScript не имеет типа равенства, поскольку он не поддерживает зависимые типы, то есть типы, которые зависят от значений. Следовательно, вы не можете писать доказательства в TypeScript. Вы можете писать доказательства на зависимо типизированных функциональных языках программирования, таких как Agda .
Однако вы можете писать предложения в TypeScript.
const negateNegateIdempotent = (x: number): boolean => negate(negate(x)) === x;
const reverseReverseIdempotent = (x: string): boolean => reverse(reverse(x)) === x;
const transformTransformIdempotent = (x: number|string): boolean => {
switch (typeof x) {
case "number": return negateNegateIdempotent(x);
case "string": return reverseReverseIdempotent(x);
}
};
Затем вы можете использовать такую библиотекукак jsverify для автоматической генерации тестовых данных для нескольких тестовых случаев.
const jsc = require("jsverify");
jsc.assert(jsc.forall("number", transformTransformIdempotent)); // OK, passed 100 tests
jsc.assert(jsc.forall("string", transformTransformIdempotent)); // OK, passed 100 tests
Вы также можете вызвать jsc.forall
с помощью "number | string"
, но я не могу заставить его работать.
Итак, чтобы ответить на ваши вопросы.
Как нужно идти на тестирование foo()
?
Функциональное программирование поощряет тестирование на основе свойств,Например, я протестировал функции negate
, reverse
и transform
, примененные дважды для идемпотентности. Если вы следуете тестированию на основе свойств, то ваши функции предложения должны быть похожи по структуре на функции, которые вы тестируете.
Если учесть тот факт, что он делегирует fnForString()
и fnForNumber()
как детали реализации и, по существу, дублировать тесты для каждого из них при написании тестов для foo()
? Это повторение приемлемо?
Да, приемлемо ли это. Хотя вы можете полностью отказаться от тестирования fnForString
и fnForNumber
, потому что тесты для них включены в тесты для foo
. Однако для полноты я бы рекомендовал включить все тесты, даже если они вводят избыточность.
Если вы пишете тесты, которые "знают", что foo()
делегируют fnForString()
и fnForNumber()
, например, путем насмешкиих и проверяет, что он делегирует им?
Предложения, которые вы пишете в тестировании на основе свойств, соответствуют структуре тестируемых функций. Следовательно, они «знают» о зависимостях, используя предложения других тестируемых функций. Не надо издеваться над ними. Вам нужно всего лишь высмеивать такие вещи, как сетевые вызовы, вызовы файловой системы и т. Д.