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

Nat*_*hur 2 bash unit-testing mocking bats-core

我正在尝试使用蝙蝠在我正在研究的项目中测试一些关键的shell脚本。我希望能够模拟脚本,以便断言一个脚本在给定情况下使用正确的参数调用了另一个脚本。该蝙蝠-模拟图书馆好像它应该做的伎俩,但它尚未在所有记录在案。

我曾尝试查看bats-mock代码和其他人创建的一些测试帮助程序脚本(如本示例),但是不幸的是,我对bash不够满意,无法推断出如何正确使用bats-mock库。

如何使用bats-mock库来模拟脚本并断言对模拟的调用?

dra*_*788 5

简要建议:

有一种更新的,更活跃的开发人员bats-mock使用了稍有不同的方法,值得探讨。https://github.com/grayhemp/bats-mock

我稍后再回来.....更多。


返回更多:

它们之间的主要区别在于它们实现了哪种“测试两倍”样式。马丁·福勒(Martin Fowler)在他的文章模拟中引用了涵盖许多测试策略的书来简要解释了某些样式

Meszaros使用术语Test Double作为测试对象代替真实对象使用的任何假装对象的通用术语。该名称源于电影中的特技替身。(他的目的之一是避免使用已经被广泛使用的任何名称。)然后,梅萨罗斯定义了四种特殊的双精度类型:

  • 虚拟对象会传递,但从未实际使用过。通常它们仅用于填充参数列表。
  • 伪对象实际上具有有效的实现,但是通常采取一些捷径,这使它们不适合生产(内存数据库是一个很好的例子)。
  • 存根提供对测试过程中进行的呼叫的固定答复,通常通常根本不响应为测试编程的内容。
  • 间谍是存根,它们还会根据调用方式记录一些信息。其中一种形式可能是电子邮件服务,它记录发送了多少消息。
  • 嘲讽是我们在这里谈论的:带有期望的预编程对象,这些对象形成了期望接收的呼叫的规范。

在这些类型的双打中,只有嘲笑者坚持进行行为验证。其他双打通常可以使用状态验证。在练习阶段,模拟程序实际上的行为确实像其他双打游戏一样,因为他们需要让SUT相信它正在与其真正的合作者交谈-但是模拟程序在设置和验证阶段有所不同。

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

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

顺便说一句,如果你太浪费时间在想什么$)*#@$了将代码get_timestamp看起来像在jasonkarns例子或哪些${_DATE_ARGS},这里的基础上在这个其他答案示例用于获取时间,因为以毫秒为单位的时代,我最好的猜测HTTPS:/ /serverfault.com/a/588705/266525

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

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
Run Code Online (Sandbox Code Playgroud)

来自jasonkarns / bats-mock文档的示例,请注意的左侧是:存根匹配所需的传入参数,如果您使用不同的参数调用date,它可能会通过并击中真实对象,但我尚未对此进行测试因为我已经花了很多时间找出原始功能,以便与其他bats-mock实现更好地进行比较。

# 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
}
Run Code Online (Sandbox Code Playgroud)

grayhemp / bats-mock的自述文件中获取示例,请注意其功能mock_set-*mock_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 "$$" ]]
}
Run Code Online (Sandbox Code Playgroud)

要获得与jasonkarns版本非常相似的行为,您需要在调用函数之前自己将stub(也称为$ {mock}的符号链接)注入PATH。如果您在setup()方法中执行此操作,那么它会在每次测试中发生,可能不是您想要的,并且还需要确保删除中的符号链接teardown(),否则可以在测试中进行存根并直接在试验结束时(类似存根/ unstub的jasonkarns版本),但如果你经常这样做,你会希望把它的测试助手(基本上是重新实现jasonkarns /蝙蝠-模拟stub内部grayhemp /蝙蝠,模拟),并在测试中保留该助手,以便您可以加载或获取该助手,并在许多测试中重复使用这些功能。或者,您也可以向Grayhemp / bats-mock提交PR,以包含存根功能(DigitalOcean Hacktoberfest声名狼藉的竞赛正在进行中,别忘了还有赃物!)。

@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}:/}"
}
Run Code Online (Sandbox Code Playgroud)

如果有人需要进一步澄清或想要有一个可以使用的存储库,请告诉我,我可以将代码推高。