Тестирование чистой функции на типе объединения, который делегирует другим чистым функциям

Тестирование чистой функции на типе объединения, который делегирует другим чистым функциям

В идеальном мире вы будете писать доказательства вместо тестов. Например, рассмотрим следующие функции.

 const negate = (x: number): number ={amp}gt; -x; const reverse = (x: string): string ={amp}gt; x.split("").reverse().join(""); const transform = (x: number|string): number|string ={amp}gt; { 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 ={amp}gt; refl; const reverseReverseIdempotent = (x: string): reverse(reverse(x))≡x ={amp}gt; refl; 

Мы можем использовать эти две леммы для доказательства идемпотентности transform следующим образом.

 const transformTransformIdempotent = (x: number|string): transform(transform(x))≡x ={amp}gt; { switch (typeof x) { case "number": return negateNegateIdempotent(x); case "string": return reverseReverseIdempotent(x); } }; 

Здесь много чего происходит, поэтому давайте разберемся с этим.

  1. Так же, как a|b является типом объединения и a{amp}amp;b является типом пересечения, a≡b является типом равенства.
  2. Значение x типа равенства a≡b является доказательством равенства a и b .
  3. Если два значения, a и b , не равны, то невозможно создать значение типа a≡b .
  4. Значение refl , короткое по рефлексивности , имеет тип a≡a . Это тривиальное доказательство того, что ценность равна самой себе.
  5. Мы использовали refl в доказательстве negateNegateIdempotent и reverseReverseIdempotent . Это возможно, потому что предложения достаточно тривиальны для компилятора, чтобы доказать автоматически.
  6. Мы используем леммы negateNegateIdempotent и reverseReverseIdempotent чтобы доказать transformTransformIdempotent . Это пример нетривиального доказательства.

Преимущество написания доказательств состоит в том, что компилятор проверяет доказательства. Если доказательство неверно, то программе не удается проверить тип, и компилятор выдает ошибку. Доказательства лучше, чем тесты по двум причинам. Во-первых, вам не нужно создавать тестовые данные. Трудно создать тестовые данные, которые обрабатывают все крайние случаи. Во-вторых, вы не забудете случайно протестировать любые крайние случаи. Компилятор выдаст ошибку, если вы это сделаете.


К сожалению, TypeScript не имеет типа равенства, потому что он не поддерживает зависимые типы, то есть типы, которые зависят от значений. Следовательно, вы не можете писать доказательства в TypeScript. Вы можете написать доказательства на зависимых типах функциональных языков программирования, таких как Agda .

Тем не менее, вы можете написать предложения в TypeScript.

 const negateNegateIdempotent = (x: number): boolean ={amp}gt; negate(negate(x)) === x; const reverseReverseIdempotent = (x: string): boolean ={amp}gt; reverse(reverse(x)) === x; const transformTransformIdempotent = (x: number|string): boolean ={amp}gt; { 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() например, путем их fnForNumber() и проверки, что он делегирует их?

Предложения, которые вы пишете в тестировании на основе свойств, соответствуют структуре тестируемых вами функций. Следовательно, они «знают» о зависимостях, используя предложения других тестируемых функций. Не надо издеваться над ними. Вам нужно всего лишь высмеивать такие вещи, как сетевые вызовы, вызовы файловой системы и т. Д.

Понравилась статья? Поделиться с друзьями:
JavaScript & TypeScript
Adblock
detector