依赖倒置、猴子修补是最适合单元测试还是两者都不适合?

Is dependency inversion, monkey patching, both or neither most appropriate for unit testing?

这些都是人为的例子,大部分是 JavaScript,但问题是与语言无关的,一般侧重于单元测试。

代码库

function func1() {                                                               
  return func2(7, 4);                                                            
}                                                                                

function func2(param1, param2) {                                                 
  return param1 + param2 + func3(11) + func4(14, 2, 8);                          
}                                                                                

function func3(param1) {                                                         
  return param1 + 5;                                                             
}                                                                                

function func4(param1, param2, param3) {                                         
  return func5(6, 1) + param1 + param2 + param3;                                 
}                                                                                

function func5(param1, param2) {                                                 
  return param1 + param2;                                                        
}

单元测试(猴子补丁样式)

function func2_stub(param1, param2) {
  return 5;
}

monkey_patch(func2, func2_stub);
assert(func1() == 5);

问题

单元测试(依赖性 Inversion/Injection 样式)

我了解依赖 inversion/injection、存根、伪造、模拟等概念,但尚未在现实​​世界的多级函数调用中实践过。 IE。到目前为止我看到的例子只显示了一个调用者和一个被调用者。

这是我推断的超过两个级别的内容:

// Refactored code

function func1() {                                                               
  return func2(func3, func4, func5, 7, 4);                                       
}                                                                                

function func2(dependent1, dependent2, dependent3, param1, param2) {             
  return param1 + param2 + dependent1(11) + dependent2(dependent3, 14, 2, 8);    
}                                                                                

function func3(param1) {                                                         
  return param1 + 5;                                                             
}                                                                                

function func4(dependent1, param1, param2, param3) {                             
  return dependent1(6, 1) + param1 + param2 + param3;                            
}                                                                                

function func5(param1, param2) {                                                 
  return param1 + param2;                                                        
}

// Tests

function func5_stub(param1, param2) {
  return 5;
}

assert(func4(func5_stub, 1, 2, 3) == 11);

问题

问题

在现实世界中进行单元测试(即深层函数调用)时,处理消除依赖项的最佳方法或策略是什么?

Functional programming 有很多优点,与这里相关的一个是它使测试超级 easy/clean,因为它很容易实现依赖性 inversion/injection.

您不需要使用像 Haskell 这样的函数式编程语言来编写依赖倒置函数,所以不要 运行 离开。 您的编程语言只需要函数和间接引用函数的能力 (pointers/references)。

我认为解释该策略的最佳方式是从一些例子开始:

动态类型示例(Java脚本)

/*
 * This function is now trivial to unit test.
 */
function depInvFunc(param1, param2, depFunc1, depFunc2) {
  // do some stuff

  var result1 = depFunc1(param1);
  var result2 = depFunc2(param2);

  if (result1 % 15 === 0) {
    result1 *= 4;
  }

  return result1 + result2;
}

/*
 * This function can be used everywhere, as opposed to using the above function
 * and having to specify the dependent param functions all the time.
 * 
 * This function does not need to be tested (nor should it be), because it has
 * no logic, it's just a simple function call.
 *
 * Think of these kinds of wrapper dependent-defining functions as configuration
 * functions (like config files). You don't have unit tests for your configs,
 * you just manually check them yourself.
 */
function wrappedDepInvFunc(param1, param2) {
  return depInvFunc(param1, param2, importedFunc1, importedFunc2);
}

静态类型示例 (Java)

DepInvFunc.java:

public class DepInvFunc {

   public int doDepInvStuff(String param1, String param2, Dep1 dep1, 
                            Dep2 dep2) {
      // do some stuff

      int result1 = dep1.doDepStuff(param1);
      int result2 = dep2.doDepStuff(param2);

      if (result % 15 == 0) {
         result1 *= 4;
      }

      return result1 + result2;
   }

}

已包装DepInvFunc.java:

public class WrappedDepInvFunc {

   public int wrappedDoDepInvStuff(String param1, String param2) {
      Dep1 dep1 = new Dep1();
      Dep2 dep2 = new Dep2();

      return DepInvFunc().doDepInvStuff(param1, param2, dep1, dep2);
   }

}

Dep1.java:

public class Dep1 {

   public int doDepStuff(String param1) {
      // do stuff
      return 5;
   }

}

Dep2.java:

public class Dep2 {

   public int doDepStuff(String param1) {
      // do stuff
      return 7;
   }

}

所以这种方法的唯一缺点(当使用动态类型语言时)是因为您可能间接调用函数,您(and/or 您的 IDE)可能无法检测提供的无效参数对于那些间接函数调用。

当使用静态类型语言的编译时类型检查时,这个问题在很大程度上得到了克服。

这种方法避免了对脆弱且可能不可用的猴子修补的需要,并且不会带来必须将依赖函数的参数从高级函数向下传递到低级函数的问题。


Tldr:将所有(或尽可能多的)逻辑放入依赖倒置函数(通过依赖注入很容易测试)和将它们包装在 logic-free/minimal 函数中(不需要测试)。


我刚才想到了这个策略,是从这两个来源中汲取了灵感: