函数可测试性问题浅析

不论是面向过程编程、面向对象编程亦或是函数式编程, 函数都是一个基本的单位,或者说是最小的功能单位。而在成千上万代码中,可维护和可测试是项目成败的关键。好的代码规范和全民皆兵式的Review是提升代码质量的利器,但无论怎么做代码单元测试完备性已然是行业公认的代码质量的量化的准则。本文仅针对函数的可测试性描述个人理解。

数学中,对而y = f(x)而言,一个输入值会有固定是输出值。比如说 sin(x)当输入为0,结果也固定是0。在函数式编程中,经常提到引用透明性,就是说函数结果即使用变量替换,程序运行结果依然是期望结果。我们期望的函数可测试性能够尽量做到数学上以及函数式编程里引用透明性特点。下面就例子进行说明。

我们在编写测试用例时,函数测试用例"最容易"编写,特别是对单个函数。实际上函数有外部依赖、外部服务的注入以及内部变量引入,远达不到引用透明性原则。因此在骨感的现实面前,函数测试远不是原本期望的样子。下面就函数可测试性浅谈下我们的尝试。

我们使用的方法:

  • 内部依赖配置、变量通过参数传入

  • 高阶函数作为入参

  • 外部依赖作为变量保存

    def readFileFromServer(fileType: String): List[String] = {
    val conn : FileServer = FileUtil.getConnect // server信息

    val cont = fileType match {
    case "mysql" => parseMysqlFile(conn)
    case "spark" => parseSparkFile(conn)
    case _ => Nil
    }

    conn.close() //

    ...... //cont解析 资源关闭等操作
    }

为了便于测试,我们需要规避函数内部的依赖,服务器连接信息从参数传入。

 def readFileFromServer(fileType: String, server: FileServer): List[String] = {
 
 val cont = fileType match {
     case "mysql" => parseMysqlFile(server)
     case "spark" => parseSparkFile(server)
     case  _ => Nil  
   }

...... .//cont解析 其它处理
}

上面函数parseMysqlFile,parseSparkFile的问题是 服务资源获取到之后的关闭问题。 面对此种情况通过借贷模式解决。即:

def using[A <: {def close() : Unit}, B](param: A)(f: A => B): B =
  try {
    f(param)
  } finally {
    if (param != null) { // scalastyle:ignore
      param.close()
    }
  }
  
 def readFileFromServer(fileType: String, server: FileServer): List[String] = {
 
 val cont = fileType match {
     case "mysql" => using(server)(parseMysqlFile(server))
     case "spark" => using(server)(parseSparkFile(server))
     case  _ => Nil  
   }

上例修改的副作用增加代码的复杂度,降低了代码覆盖率。
程序运行都有过程和边界控制的,对于模式匹配中的每一个pattern,在我们看来是没有必要覆盖的。即 using(server)(parseMysqlFile(server)) using(server)(parseSparkFile(server)) 代码覆盖率可以不完备。之前项目中代码覆盖率靓丽数据背后很多是反智操作。

def checkIn(product: List[String]) : Double = {
  val hk2cnyRatio = 0.9
  val sum =    ...... //扫描所有商品信息
   
   ratio * sum  //转换价格

}

上例涉及到汇率转换 hk2cnyRatio变量 是可变且可复用的。如果作为入参传入,不合理。hk2cnyRatio 需要作为类成员方法。

 val hk2cnyRatio = ??? // 从配置读取获取在线获取
 def checkIn(product: List[String]) : Double = {
  val sum =    ...... //扫描所有商品信息
   
   hk2cnyRatio * sum  //转换价格

}

按照可测试性而言,作为入参传入汇率解决测试问题。考虑它是函数成员变量,不作为入参。
以上方法:作为参数传递会造成函数入参过多;作为类成员在用例编写需要mock,才能让函数输出稳定。

因此,函数测试方法不一而足,隔离是基本原则。隔离外部依赖、内部实现使得代码精简易读,降低测试成本。

你可能感兴趣的:(函数可测试性问题浅析)