如何在Bash测试中使用'bats mock'断言对模拟脚本的调用

如何在Bash测试中使用'bats mock'断言对模拟脚本的调用,bash,unit-testing,mocking,bats-core,Bash,Unit Testing,Mocking,Bats Core,我正试图在我正在进行的项目中测试一些关键的shell脚本。我希望能够模拟脚本,以便断言在给定情况下,一个脚本调用另一个具有正确参数的脚本。图书馆似乎应该做到这一点,但它根本没有被记录在案 我已经尝试查看了和其他人创建的几个测试助手脚本(如),但不幸的是,我对bash不太熟悉,无法推断如何正确使用bats模拟库 如何使用bats模拟库模拟脚本并对模拟调用进行断言?简要建议: 有一个更新的、更积极的开发人员bats mock,它使用了一种稍微不同的方法,值得探索 稍后我会带着…更多回来 返回更多内

我正试图在我正在进行的项目中测试一些关键的shell脚本。我希望能够模拟脚本,以便断言在给定情况下,一个脚本调用另一个具有正确参数的脚本。图书馆似乎应该做到这一点,但它根本没有被记录在案

我已经尝试查看了和其他人创建的几个测试助手脚本(如),但不幸的是,我对bash不太熟悉,无法推断如何正确使用bats模拟库

如何使用bats模拟库模拟脚本并对模拟调用进行断言?

简要建议:

有一个更新的、更积极的开发人员bats mock,它使用了一种稍微不同的方法,值得探索

稍后我会带着…更多回来


返回更多内容:

它们之间的主要区别在于它们实现了哪种“双重测试”样式。Martin Fowler在其文章中引用了一本涵盖许多测试策略的书,简要解释了一些风格

Meszaros使用术语Test Double作为任何类型测试的通用术语 为了测试的目的,用假装对象代替真实对象。 这个名字来源于电影中特技替身的概念。(其中一个 他的目的是避免使用任何已经广泛使用的名称。) 梅萨罗斯随后定义了四种特殊的双重身份:

  • 虚拟对象被传递,但从未实际使用过。通常它们只是用来填充参数列表
  • 伪对象实际上有工作实现,但通常会采取一些使其不适合生产的快捷方式(例如 内存数据库就是一个很好的例子)
  • 存根为测试过程中的呼叫提供了固定的答案,通常对测试程序之外的任何内容都没有响应 测试
  • 间谍是一种存根,根据他们被称为间谍的方式记录一些信息。其中的一种形式可能是记录 发送了多少条消息
  • 模拟就是我们在这里所讨论的:预先编程的对象,带有期望值,这些期望值形成了它们所属调用的规范 预计将收到
在这类替身中,只有嘲笑者坚持自己的行为 验证。另一个替身可以,而且通常使用状态 验证。mock在比赛中的表现确实和其他替身一样 锻炼阶段,因为他们需要让SUT相信他们正在与他们交谈 它真正的合作者——但模拟在设置和操作上有所不同 核查阶段

'似乎主要是围绕启用存根而设计的,在存根中,它为脚本或二进制文件返回N次调用的伪数据,并保留对存根调用次数的内部计数,如果调用与N行伪数据不匹配,则返回错误代码

的版本允许您创建spy对象及其应生成的输出、返回代码以及运行模拟时应触发的任何“副作用”(如下面示例中的PID)。然后,您可以运行一个脚本或命令来调用mock正在隐藏的命令,并查看调用该命令的次数、脚本的返回代码以及调用mock命令时的环境。总的来说,它似乎更容易断言脚本/二进制文件的调用内容以及调用次数

顺便说一句,如果你浪费了很多时间想知道
$)*#@$
中的代码在示例中是什么样子的,或者
${u DATE\u ARGS}
是什么,下面是我根据另一个答案中的示例得出的关于从纪元开始以毫秒为单位的时间的最佳猜测:

您可以将其复制并粘贴到Bash/POSIX shell中,以查看第一个输出与第一个存根数据行将提供给
get_timestamp
的内容相匹配,第二个输出与示例中第一个断言的输出相匹配

get_timestamp () {
  # This should really be named get timestamp in milliseconds

  # In truth it wouldn't accept input ie the ${1} below,
  # but it is easier to show and test how it works with a fixed date (which is why we want to stub!)
  GIVEN_DATE="$1"
  # Pass in a human readable date and get back the epoch `%s` (seconds since 1-1-1970) and %N nanoseconds
  # date +%s.%N -d'Mon Apr 18 03:19:58.184561556 CDT 2016'
  EPOCH_NANO=$(date +%s.%N -d"$GIVEN_DATE")
  echo "This reflects the data the date stub would return: $EPOCH_NANO"
  # Accepts input in seconds.nanoseconds ie %s.%N and
  # sets the output format to milliseconds,
  # by combining the epoch `%s` (seconds since 1-1-1970) and
  # first 3 digits of the nanoseconds with %3N
  _DATE_ARGS='+%s%3N -d'
  echo $(date ${_DATE_ARGS}"@${EPOCH_NANO}")
}
get_timestamp 'Mon Apr 18 03:19:58.184561556 CDT 2016' # The quotes make it a *single* argument $1 to the function
文档中的示例,
左侧的注释:
是存根匹配所需的传入参数,如果您使用不同的参数调用date,它可能会通过并命中真实对象,但是我还没有测试这个,因为我已经花了太多的时间来计算原始函数,以便与其他bats模拟实现进行更好的比较

# In bats you can declare globals outside your tests if you want them to apply
# to all tests in a file, or in a `fixture` or `vars` file and `load`or `source` it
declare -g _DATE_ARGS='+%s.%N -d'

# The interesting thing about the order of the mocked call returns is they are actually moving backwards in time,
# very interesting behavior and possibly needs another test that should throw a really big exception if this is encountered in the real world

# Original example below
@test "get_timestamp" {
  stub date \
      "${_DATE_ARGS} : echo 1460967598.184561556" \
      "${_DATE_ARGS} : echo 1460967598.084561556" \
      "${_DATE_ARGS} : echo 1460967598.004561556" \
      "${_DATE_ARGS} : echo 1460967598.000561556" \
      "${_DATE_ARGS} : echo 1460967598.000061556"

  run get_timestamp
  assert_success
  assert_output 1460967598184

  run get_timestamp
  assert_success
  assert_output 1460967598084

  run get_timestamp
  assert_success
  assert_output 1460967598004

  run get_timestamp
  assert_success
  assert_output 1460967598000

  run get_timestamp
  assert_success
  assert_output 1460967598000

  unstub date
}
在的自述文件中的示例中,请注意酷的
mock\u集-*
mock\u get.*
选项

@test "postgres.sh starts Postgres" {
  mock="$(mock_create)"
  mock_set_side_effect "${mock}" "echo $$ > /tmp/postgres_started"

  # Assuming postgres.sh expects the `_POSTGRES` variable to define a
  # path to the `postgres` executable
  _POSTGRES="${mock}" run postgres.sh

  [[ "${status}" -eq 0 ]]
  [[ "$(mock_get_call_num ${mock})" -eq 1 ]]
  [[ "$(mock_get_call_user ${mock})" = 'postgres' ]]
  [[ "$(mock_get_call_args ${mock})" =~ -D\ /var/lib/postgresql ]]
  [[ "$(mock_get_call_env ${mock} PGPORT)" -eq 5432 ]]
  [[ "$(cat /tmp/postgres_started)" -eq "$$" ]]
}
要获得与之非常相似的行为,您需要在调用函数之前将存根(也称为${mock}的符号链接)注入到路径中。如果在
setup()
方法中执行此操作,则每次测试都会发生此操作,这可能不是您想要的,并且您还需要确保在
teardown()
中删除符号链接,否则您可以在测试中执行存根操作,并在测试结束时进行清理(类似于存根/取消存根版本),但是如果你经常这样做,你会想让它成为一个测试助手(基本上是重新实现的
stub
内部),并把助手和你的测试放在一起,这样你就可以在很多测试中加载或源代码并重用这些函数。或者你也可以提交一份PR来包含存根功能(DigitalOcean Hackertoberfest的名气和耻辱竞赛正在进行中,别忘了还有swag参与其中!)


如果有人需要更多的说明或希望有一个可用的存储库,请告诉我,我可以把我的代码推上去。

自述文件中的文档似乎已经更新,因为您发布了这个问题以添加用法和示例,但用法文档似乎仍然不完整。现在我给一些人打电话,请回复。谢谢它真的帮助了我进一步模仿蝙蝠。实际上,我发现
ln-s“${externalApp}”/usr/local/bin/externalApp
对于在容器中进行测试非常有用。只是提醒一下:S
@test "get_timestamp" {
  mocked_command="date"
  mock="$(mock_create)"
  mock_path="${mock%/*}" # Parameter expansion to get the folder portion of the temp mock's path
  mock_file="${mock##*/}" # Parameter expansion to get the filename portion of the temp mock's path
  ln -sf "${mock_path}/${mock_file}" "${mock_path}/${mocked_command}"
  PATH="${mock_path}:$PATH" # Putting the stub at the beginning of the PATH so it gets picked up first
  mock_set_output "${mock}" "1460967598.184561556" 1
  mock_set_output "${mock}" "1460967598.084561556" 2
  mock_set_output "${mock}" "1460967598.004561556" 3
  mock_set_output "${mock}" "1460967598.000561556" 4
  mock_set_output "${mock}" "1460967598.000061556" 5
  mock_set_status "${mock}" 1 6

  run get_timestamp
  [[ "${status}" -eq 0 ]]
  run get_timestamp
  run get_timestamp
  run get_timestamp
  run get_timestamp
  [[ "${status}" -eq 0 ]]
  # Status is just of the previous invocation of `run`, so you can test every time or just once
  # note that calling the mock more times than you set the output for does NOT change the exit status...
  # unless you override it with `mock_set_status "${mock}" 1 6` 
  # Last bits are the exit code/status and index of call to return the status for
  # This is a test to assert that mocked_command stub is in the path and points the right place
  [[ "$(readlink -e $(which date))" == "$(readlink -e ${mock})" ]]
  # This is a direct call to the stubbed command to show that it returns the `mock_set_status` defined code and shows up in the call_num 
  run ${mocked_command}
  [[ "$status" -eq 1 ]]
  [[ "$(mock_get_call_num ${mock})" -eq 6 ]]
  # Check if your function exported something to the environment, the example get_timestamp function above does NOT
  # [[ "$(mock_get_call_env ${mock} _DATE_ARGS 1)" -eq '~%s%3N' ]]

  # Use the below line if you actually want to see all the arguments the function used to call the `date` 'stub'
  # echo "# call_args: " $(mock_get_call_args ${mock} 1) >&3

  # The actual args don't have the \ but the regex operator =~ treats + specially if it isn't escaped
  date_args="\+%s%3N"
  [[ "$(mock_get_call_args ${mock} 1)" =~ $date_args ]]

  # Cleanup our stub and fixup the PATH
  unlink "${mock_path}/${mocked_command}"
  PATH="${PATH/${mock_path}:/}"
}