使用代码库测试调用

测试使用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 :通过 TrueFalse 用退出代码代替其他为空的结果 01 分别为。

然后,示例测试如下所示::

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发行版上运行包管理命令,并且您正在尝试测试其错误处理。您可能希望设置一个上下文,该上下文伪装成任何任意的 aptyum 命令失败,并确保该函数在遇到问题时返回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")