在联合类型上测试委托给其他纯函数的纯函数

sam*_*ces 8 javascript testing functional-programming typescript

假设您有一个采用联合类型的函数,然后将该类型缩小并委托给其他两个纯函数之一。

function foo(arg: string|number) {
    if (typeof arg === 'string') {
        return fnForString(arg)
    } else {
        return fnForNumber(arg)
    }
}
Run Code Online (Sandbox Code Playgroud)

假定fnForString()fnForNumber()也是纯函数,并且它们本身已经过测试。

应该如何进行测试foo()

  • 如果你把一个事实,即它代表们fnForString(),并fnForNumber()作为一个实现细节,而且基本上编写测试时,重复测试,他们每个人的foo()?这种重复是否可以接受?
  • 如果您编写测试其“知道” foo()授人以fnForString()fnForNumber()如通过嘲笑出来,并检查其委托给他们呢?

小智 5

最好的解决方案就是测试foo

fnForString并且fnForNumber是一个实现细节,您将来可以更改它,而不必更改foo。如果发生这种情况,您的测试可能会无缘无故中断,则这种问题会使您的测试过于扩展和无用。

您的界面只需要进行foo测试。

如果您必须进行测试,fnForString并将fnForNumber这种测试与公共接口测试分开进行。

这是我对肯特·贝克说的以下原则的解释

程序员测试应该对行为更改敏感,对结构更改不敏感。如果从观察者的角度来看程序的行为是稳定的,则不应更改任何测试。


Aad*_*hah 1

在理想的世界中,您会编写证明而不是测试。例如,考虑以下函数。

\n\n
const negate = (x: number): number => -x;\n\nconst reverse = (x: string): string => x.split("").reverse().join("");\n\nconst transform = (x: number|string): number|string => {\n  switch (typeof x) {\n  case "number": return negate(x);\n  case "string": return reverse(x);\n  }\n};\n
Run Code Online (Sandbox Code Playgroud)\n\n

假设您想证明transform应用两次是幂等的,即对于所有有效输入xtransform(transform(x))等于x。好吧,您首先需要证明negatereverse应用两次是幂等的。现在,假设证明negatereverse应用两次的幂等性是微不足道的,即编译器可以计算出来。因此,我们有以下引理

\n\n
const negateNegateIdempotent = (x: number): negate(negate(x))\xe2\x89\xa1x => refl;\n\nconst reverseReverseIdempotent = (x: string): reverse(reverse(x))\xe2\x89\xa1x => refl;\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们可以使用这两个引理来证明它transform是幂等的,如下所示。

\n\n
const transformTransformIdempotent = (x: number|string): transform(transform(x))\xe2\x89\xa1x => {\n  switch (typeof x) {\n  case "number": return negateNegateIdempotent(x);\n  case "string": return reverseReverseIdempotent(x);\n  }\n};\n
Run Code Online (Sandbox Code Playgroud)\n\n

这里发生了很多事情,所以让我们来分解一下。

\n\n
    \n
  1. 正如a|b联合类型和a&b交集类型一样,a\xe2\x89\xa1b相等类型也是如此。
  2. \n
  3. x相等类型的值是和a\xe2\x89\xa1b相等的证明。ab
  4. \n
  5. 如果两个值ab不相等,则无法构造 类型的值a\xe2\x89\xa1b
  6. \n
  7. 值是自反性refl的缩写,具有类型。这是一个值等于其自身的简单证明。a\xe2\x89\xa1a
  8. \n
  9. 我们在和refl的证明中使用了。这是可能的,因为命题对于编译器来说足够简单,可以自动证明。negateNegateIdempotentreverseReverseIdempotent
  10. \n
  11. 我们用negateNegateIdempotentreverseReverseIdempotent引理来证明transformTransformIdempotent。这是一个重要证明的例子。
  12. \n
\n\n

编写证明的优点是编译器可以验证证明。如果证明不正确,则程序无法进行类型检查,并且编译器会抛出错误。证明比测试更好有两个原因。首先,您不必创建测试数据。创建处理所有边缘情况的测试数据很困难。其次,您不会意外地忘记测试任何边缘情况。如果这样做,编译器将抛出错误。

\n\n
\n\n

不幸的是,TypeScript 没有相等类型,因为它不支持依赖类型,即依赖于值的类型。因此,您无法用 TypeScript 编写证明。您可以使用Agda等依赖类型函数编程语言编写证明。

\n\n

但是,您可以用 TypeScript 编写命题。

\n\n
const negateNegateIdempotent = (x: number): boolean => negate(negate(x)) === x;\n\nconst reverseReverseIdempotent = (x: string): boolean => reverse(reverse(x)) === x;\n\nconst transformTransformIdempotent = (x: number|string): boolean => {\n  switch (typeof x) {\n  case "number": return negateNegateIdempotent(x);\n  case "string": return reverseReverseIdempotent(x);\n  }\n};\n
Run Code Online (Sandbox Code Playgroud)\n\n

然后,您可以使用jsverify等库自动生成多个测试用例的测试数据。

\n\n
const jsc = require("jsverify");\n\njsc.assert(jsc.forall("number", transformTransformIdempotent)); // OK, passed 100 tests\n\njsc.assert(jsc.forall("string", transformTransformIdempotent)); // OK, passed 100 tests\n
Run Code Online (Sandbox Code Playgroud)\n\n

您也可以致电jsc.forall"number | string"但我似乎无法让它工作。

\n\n
\n\n

所以来回答你的问题。

\n\n
\n

应该如何进行测试foo()

\n
\n\n

函数式编程鼓励基于属性的测试。例如,我测试了两次应用的negatereversetransform函数的幂等性。如果您遵循基于属性的测试,那么您的命题函数在结构上应该与您正在测试的函数类似。

\n\n
\n

fnForString()您是否应该将它委托给和 的事实fnForNumber()视为实现细节,并在为 编写测试时本质上重复每个测试的测试foo()?这种重复可以接受吗?

\n
\n\n

是的,可以接受吗?尽管如此,您可以完全放弃测试fnForStringfnForNumber因为这些测试包含在foo. 然而,为了完整性,我建议包括所有测试,即使它引入了冗余。

\n\n
\n

foo()您是否应该编写“知道”委托的测试,fnForString()例如fnForNumber()通过模拟它们并检查它是否委托给它们?

\n
\n\n

您在基于属性的测试中编写的命题遵循您正在测试的函数的结构。因此,他们通过使用正在测试的其他函数的命题来“了解”依赖关系。没必要嘲笑他们。您只需要模拟网络调用、文件系统调用等。

\n