Shell 脚本单元测试:如何模拟复杂的实用程序

Shell Script unit testing: How to mockup a complex utility program

我喜欢 unit testing of some legacy shell 脚本。

在现实世界中,脚本通常用于调用实用程序 像 findtarcpiogrepsedrsyncdate 等等一些相当复杂的命令包含很多选项的行。有时构造和使用正则表达式或通配符模式。

一个示例:通常由cron定期调用的shell脚本的任务是将一些巨大的目录树从一台计算机镜像到另一台计算机,使用效用 rsync。 应从中排除几种类型的文件和目录 镜像进程:

  #!/usr/bin/env bash
  ...
  function mirror() {
      ...
      COMMAND="rsync -aH$VERBOSE$DRY $PROGRESS $DELETE $OTHER_OPTIONS \
                   $EXCLUDE_OPTIONS $SOURCE_HOST:$DIRECTORY $TARGET"
      ...
      if eval $COMMAND
      then ...
      else ...
      fi
      ...
  }
  ...

正如 Michael Feathers 在他的名著 Working Effectively with Legacy Code 中所写,一个好的单元测试运行得非常快,并且不会接触网络、文件系统或打开任何数据库。

根据 Michael Feathers 的建议,此处使用的技术是:dependency injection。这里要替换的对象是实用程序rsync.

我的第一个想法:在我的shell脚本测试框架中(我使用bats) I manipulate $PATH in a way that a mockup找到rsync而不是 真正的 rsync 实用程序。这个mockup object could check the supplied command line parameters and options. Similar with other utilities used in this part of the script under test.

我过去在这个脚本领域遇到的实际问题通常是由文件或目录名称中的特殊字符引起的错误、引用或编码问题、缺少 ssh 密钥、错误权限等。这些类型的错误会逃脱这种单元测试技术。 (我知道:对于其中一些问题,单元测试根本不是治愈方法)。

另一个缺点是,为像 rsyncfind 这样的复杂实用程序编写 mockup 很容易出错,而且它本身就是一项乏味的工程任务。

我相信上面描述的情况很普遍,其他人可能也遇到过类似的问题。谁有什么好主意愿意在这里与我分享?

嘉吉的窘境:

" Any design problem can be solved by adding an additional level of indirection, except for too many levels of indirection."

为什么要模拟系统命令?毕竟,如果您正在编程 Bash,系统就是您的目标,您应该使用系统来评估您的脚本。

顾名思义,单元测试会让您对正在设计的系统的单一部分充满信心。因此,在 bash 脚本的情况下,您将必须 define 您的 unit 是什么。一个功能?脚本文件?命令 ?

如果您想将单位定义为函数,那么我会建议您编写上面列出的众所周知的错误列表:

  • 文件名或目录名中的特殊字符
  • 引号或编码问题
  • 缺少 ssh 密钥
  • 错误的权限等等。

并为它写一个测试用例。并尽量不要偏离系统命令,因为它们是您要交付的系统的 不可或缺的 部分。

您可以使用函数模拟任何命令,如下所示:

function rsync() {
    # mock things here if necessary
}

然后导出函数和运行单元测试:

export -f rsync
unittest