使用代码库测试调用¶
测试使用invoke的代码库的策略;一些适用于专注于cli任务的代码,另一些适用于更通用/重构的设置。
子类和修改调用“internals”¶
一个简短的前言:大多数用户会发现后面的方法是合适的,但是高级用户应该注意到invoke的设计使其本身易于测试。这意味着,在许多情况下,即使调用的“内部”也被暴露为低/不共享的责任,公开文档类,可以被分类和修改,以注入测试友好的值或模拟。一定要检查一下 API documentation 你说什么?
使用 MockContext
¶
为测试目的对invoke的公共api进行子类化的实例是我们自己的 MockContext
. 代码基,它围绕 Context
对象及其方法(大多数面向任务的代码)将发现通过注入 MockContext
已实例化以产生部分 Result
物体。
例如,执行以下任务:
from invoke import task
@task
def get_platform(c):
uname = c.run("uname -s").stdout.strip()
if uname == 'Darwin':
return "You paid the Apple tax!"
elif uname == 'Linux':
return "Year of Linux on the desktop!"
用来测试它的示例 MockContext
可能是以下情况:
from invoke import MockContext, Result
from mytasks import get_platform
def test_get_platform_on_mac():
c = MockContext(run=Result("Darwin\n"))
assert "Apple" in get_platform(c)
def test_get_platform_on_linux():
c = MockContext(run=Result("Linux\n"))
assert "desktop" in get_platform(c)
把这个 Mock
在……里面 MockContext
¶
从Invoke 1.5开始, MockContext
将尝试导入 mock
库,并将其方法包装在 Mock
对象。这使您不仅可以向代码提供真实的返回值,还可以对代码正在运行的命令进行测试断言。
这是另一项“平台敏感”任务:
from invoke import task
@task
def replace(c, path, search, replacement):
# Assume systems have sed, and that some (eg macOS w/ Homebrew) may
# have gsed, implying regular sed is BSD style.
has_gsed = c.run("which gsed", warn=True, hide=True)
# Set command to run accordingly
binary = "gsed" if has_gsed else "sed"
c.run(f"{binary} -e 's/{search}/{replacement}/g' {path}")
测试代码(同样,它假定例如 MockContext.run
现在是一个 Mock
包装器)主要依赖于“最后一次调用”断言 (Mock.assert_called_with
)但是您当然可以使用任何 Mock
你需要的方法。它还显示了如何使用dict值设置模拟上下文以响应多个可能的命令:
from invoke import MockContext, Result
from mytasks import replace
def test_regular_sed():
expected_sed = "sed -e s/foo/bar/g file.txt"
c = MockContext(run={
"which gsed": Result(exited=1),
expected_sed: Result(),
})
replace(c, 'file.txt', 'foo', 'bar')
c.run.assert_called_with(expected_sed)
def test_homebrew_gsed():
expected_sed = "gsed -e s/foo/bar/g file.txt"
c = MockContext(run={
"which gsed": Result(exited=0),
expected_sed: Result(),
})
replace(c, 'file.txt', 'foo', 'bar')
c.run.assert_called_with(expected_sed)
布尔模拟结果¶
您可能已经注意到,上面的示例使用了少量的“Empty” Result
对象;这些对象代表“成功,但没有有用的属性”命令执行(AS Result
默认为退出代码为 0
以及用于标准输出/标准错误的空字符串)。
这是相对常见的-想想调用者只关心布尔结果的“疑问性”命令,或者纯粹出于副作用而调用命令的情况。为了支持这一点,在 MockContext
:通过 True
或 False
用退出代码代替其他为空的结果 0
或 1
分别为。
然后,示例测试如下所示::
from invoke import MockContext, Result
from mytasks import replace
def test_regular_sed():
expected_sed = "sed -e s/foo/bar/g file.txt"
c = MockContext(run={
"which gsed": False,
expected_sed: True,
})
replace(c, 'file.txt', 'foo', 'bar')
c.run.assert_called_with(expected_sed)
def test_homebrew_gsed():
expected_sed = "gsed -e s/foo/bar/g file.txt"
c = MockContext(run={
"which gsed": True,
expected_sed: True,
})
replace(c, 'file.txt', 'foo', 'bar')
c.run.assert_called_with(expected_sed)
字符串模拟结果¶
另一个方便的捷径是使用字符串值,它被解释为结果 Result
。这只会真正使您不必写出类本身(因为 stdout
是的第一个位置参数 Result
!)但是“命令X导致标准输出Y”是一个非常常见的用例,我们还是实现了它。
通过示例,让我们修改前面的一个示例,其中我们关心stdout::
from invoke import MockContext
from mytasks import get_platform
def test_get_platform_on_mac():
c = MockContext(run="Darwin\n")
assert "Apple" in get_platform(c)
def test_get_platform_on_linux():
c = MockContext(run="Linux\n")
assert "desktop" in get_platform(c)
与本文档中的其他内容一样,此策略可以应用于迭代器或映射以及单个值。
正则表达式命令匹配¶
的词典形式 MockContext
除了字符串之外,kwarg还可以接受正则表达式对象作为键;这非常适合您不知道要调用的确切命令,或者根本不需要或不想写出整个内容的情况。
假设您正在编写一个函数,以便在几个不同的Linux发行版上运行包管理命令,并且您正在尝试测试其错误处理。您可能希望设置一个上下文,该上下文伪装成任何任意的 apt
或 yum
命令失败,并确保该函数在遇到问题时返回stderr::
import re
from invoke import MockContext
from mypackage.tasks import install
package_manager = re.compile(r"^(apt(-get)?|yum) .*")
def test_package_success_returns_True():
c = MockContext(run={package_manager: True})
assert install(c, package="somepackage") is True
def test_package_explosions_return_stderr():
c = MockContext(run={
package_manager: Result(stderr="oh no!", exited=1),
})
assert install(c, package="otherpackage") == "oh no!"
有点做作-有很多其他方法可以组织这一测试代码,这样您就不真正需要正则表达式了-但希望很清楚,当您 do 需要这种灵活性,这就是你可以做的。
重复结果¶
缺省情况下,这些模拟结构中的值被使用,从而导致 MockContext
筹集资金 NotImplementedError
之后(对于任何意外的命令执行都是如此)。这是在假设大多数测试代码只运行一次给定命令的情况下设计的。
如果你的情况与此不符,就给 repeat=True
传递给构造函数,您将看到值无限期重复(对于可迭代,则为循环重复)。
期待 Results
¶
核心调用子流程方法,如 run
全部返回 Result
对象(如上所述)可以仅用部分数据(例如标准输出,但不存在退出代码或标准错误)容易地实例化。
这意味着组织良好的代码可以更容易地进行测试,并且不需要大量使用 MockContext
。
关于初始值的迭代 MockContext
-使用上面的示例::
from invoke import task
@task
def get_platform(c):
print(platform_response(c.run("uname -s")))
def platform_response(result):
uname = result.stdout.strip()
if uname == 'Darwin':
return "You paid the Apple tax!"
elif uname == 'Linux':
return "Year of Linux on the desktop!"
使用封装在子例程中的逻辑,您可以只对该函数本身进行单元测试,从而推迟任务或其上下文的测试:
from invoke import Result
from mytasks import platform_response
def test_platform_response_on_mac():
assert "Apple" in platform_response(Result("Darwin\n"))
def test_platform_response_on_linux():
assert "desktop" in platform_response(Result("Linux\n"))
避免完全模拟依赖代码路径¶
这更像是一种通用的软件工程策略,但上述代码示例的自然终点是您的主逻辑根本不关心调用——只关心基本的python(或本地定义的)数据类型。这允许您单独测试逻辑,或者忽略对调用端的测试,或者只为代码与invoke接口的地方编写目标测试。
任务代码的另一个小调整:
from invoke import task
@task
def show_platform(c):
uname = c.run("uname -s").stdout.strip()
print(platform_response(uname))
def platform_response(uname):
if uname == 'Darwin':
return "You paid the Apple tax!"
elif uname == 'Linux':
return "Year of Linux on the desktop!"
还有测试:
from mytasks import platform_response
def test_platform_response_on_mac():
assert "Apple" in platform_response("Darwin\n")
def test_platform_response_on_linux():
assert "desktop" in platform_response("Linux\n")