1

这些都是人为的例子,主要是 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);

问题

  • 测试与实现紧密耦合。
  • 在某些语言中可能无法进行猴子修补。
  • 未经测试的副作用依赖项更改不会破坏现有测试(即静默和未修补的依赖项)。

单元测试(依赖倒置/注入风格)

我了解依赖反转/注入、存根、伪造、模拟等的概念,但尚未在现实​​世界的多级函数调用中遇到它。即到目前为止,我所看到的示例仅显示了调用者和被调用者。

这就是我将其推断为两个以上的级别:

// 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);

问题

  • 测试与实现紧密耦合。
  • 顶级函数因未使用的参数而臃肿(只是传递下来)。
  • 您如何测试最高级别的函数(在这种情况下为 func1)?每次反转依赖关系时,都会无意中创建另一个级别。

问题

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

4

1 回答 1

0

函数式编程有很多优点,与此相关的是它使测试变得超级容易/干净,因为它很容易实现依赖倒置/注入。

你不需要使用像Haskell这样的函数式编程语言来编写依赖倒置函数,所以不要跑路。您的编程语言只需要函数和间接引用函数的能力(指针/引用)。

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

动态类型示例 (JavaScript)

/*
 * 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;
   }

}

WrappedDepInvFunc.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;
   }

}

因此,这种方法的唯一缺点(当使用动态类型语言时)是因为您可能间接调用函数,您(和/或您的 IDE)可能无法检测到提供给这些间接函数调用的无效参数。

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

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


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


在从以下两个来源中汲取灵感之后,我才想到了这个策略:

于 2017-01-10T10:20:30.337 回答