跳转到主要内容

Python期望库

项目描述

pyexpect: 最小但非常灵活的实现expect模式

expect模式的全貌是允许简洁的断言,生成可预测且良好的错误消息。

最佳示例请参考(此处来自pytest)

___ test_equals ______________________________________________________

    def test_equals():
>       expect(foo).to_equal(bar) # many variant spellings, see source
E       AssertionError: Expect 3 to equal 4

___ test_equals_shorthand ____________________________________________

    def test_equals_shorthand():
>       expect(foo) == bar # if you like the pyexpect way better
E       AssertionError: Expect 3 to equal 4

=== 2 failed in 0.06 seconds =========================================

尽可能减少行噪声,因此错误消息将尽可能接近问题代码显示。无需挖掘堆栈跟踪,清晰的错误消息会告诉您出了什么问题。这正是断言应该工作的方式。

为什么我应该使用expect()而不是self.assert*?

让我们从一个示例开始

self.assertEquals('foo', 'bar')

在这个断言中,无法看到哪个参数是期望值,哪个是实际值。虽然这种排序在unittest包中的不同断言之间大多是内部一致的,但它肯定不是人们使用此包时的一致性。如果您在单元测试包、团队和语言之间切换,这会变得更加令人不安。

这里的问题是API没有方法知道两个参数中哪个是期望值,因此无法在错误消息中使用该信息。

考虑这个 unittest.TestCase 示例

======================================================================
FAIL: test_equals (__main__.ExpectedActualConfusionTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-4-a19c8d7db4a9>", line 6, in test_equals
    self.assertEqual(sorted(unsorted), [1,2,3,5])
AssertionError: Lists differ: [1, 2, 3, 5, 8] != [1, 2, 3, 5]

First list contains 1 additional elements.
First extra element 4:
8

- [1, 2, 3, 5, 8]
?            ---
+ [1, 2, 3, 5]

这个错误信息相当不错,但是您必须深入阅读和理解单元测试,才能确定哪个值是预期的,哪个是实际值。(更糟糕的是,有些测试框架会首先打印第二个参数,这甚至让您更难阅读输出。)

如果您和我一样对此感到烦恼,请允许我向您介绍“期望模式”。像这样的断言

expect('foo').to.equal('bar')
expect('foo').equals('bar')
expect('foo') == 'bar'

清楚地表明了预期值和实际值。不可能产生混淆。此外,错误信息的设计与源代码完全对应

Expect 'foo' to equal 'bar'.

因此,从错误信息到映射的映射立即且完全清晰,每次都能为您节省几分钟,提高您的专注力、生产力和——最重要的是——与单元测试一起工作时带来的乐趣。

作为额外的好处,所有这些异常都与任何TestCase类无关,因此您可以在代码的任何地方轻松重用它们来形式化代码对某些内部状态的要求。有时也称为“设计规范”或“快速失败”编程。哦,而且这些期望通常更短,所以您在测试中获得更清晰、更简洁的断言时甚至需要输入更少。几乎就像是既能享受蛋糕又能吃掉它!

但我已经放弃了 unittest.TestCase 并转向了 PyTest

pytest可以仅仅通过普通的Python断言生成各种错误信息,这相当令人惊讶。但是有一个问题。因为API(assert somethingThatResolvesToTrueOrFalse())并不知道测试实际上是什么。这意味着必要的错误信息很容易缺乏上下文。

考虑这个试图确保框架输出特定类型的测试

import numpy as np
obj = np.int8(3)
def test_bad_error_message():
    assert isinstance(obj, int)

# Testing output
____ test_bad_error_message ________________

    def test_bad_error_message():
>       assert isinstance(obj, int)
E       assert False
E        +  where False = isinstance(3, int)

查看输出——你能弄清楚发生了什么吗?

np.int8(3)repr()3,这与int(3)的相同,这使得这个错误信息……很糟糕。

问题是pytest无法知道这个断言实际上是什么。因此,它不知道参数的类型(np.int8)是这里有用的信息。

因此,pyexpect包含了一套丰富的匹配器,每次都能生成清晰可读的错误信息。

当然,如果您尝试在pytes测试之外使用常规断言,您会发现Python默认的异常输出相当糟糕,并且没有自定义错误信息就完全没有用处,这需要输入很多。

有趣!那么它能做什么呢?

很高兴您问!这就是

  1. 许多内置匹配器:查看源代码以查看您开始所需的全部断言。从equalsbeis_raises再到matches——我们都有。不仅如此,每个匹配器还有一些别名,因此您可以使用最适合您的断言的变体,或者当在跨语言边界使用多个单元测试框架时,您更习惯的变体(例如python/js)。

    一些示例

    expect(True).is_true()
    expect(True).is_.true()
    expect(True).equals(True)
    expect(True).is_.equal(True)
    expect(True) == True
    expect(raising_calable).raises()
    expect(raising_calable).to_raise()
    

    如果缺少重要的别名,请提交拉取请求。

  2. 易用性:expect()可以与您能想到的任何东西(只要它是有效的Python标识符)任意链式调用,以给出您可能的最干净的断言描述

    expect(23).to.equal(23)
    expect(23).is_.equal(23)
    # or go all out - but just because it works doesn't mean it's sensible
    expect(23).to_be_chaned.with_something.that_makes_sense_in\
        .your_context.before_it.calls_the.matcher.as_the_last.segment(23)
    # Here .segment(23) would be the matcher that is called
    

    选择对您的特定测试来说有意义的任何内容,以便在阅读测试时感觉自然,并尽可能最好地传达代码的含义。

  3. 扩展的简单性:在所有我看过除了Python expect实现之外的其他Python实现中,至少它们的一些方面要复杂得多。每个匹配器都是一个类或需要通过相对复杂的过程进行注册的东西,参数不仅仅是直接的方法参数,not不支持作为本地框架概念...

    相比之下,在pyexpect中,如果您想注册一个新的匹配器,它就像定义一个方法然后将其分配给您想要的任何实例方法名一样简单,明确断言您想要断言的内容,并清楚地定义将要抛出的错误消息。

    def falseish(self):
        # See expect() source for availeable helpers
        self._assert(self._actual == False, "to be falseish")
    expect.is_falsish = expect.is_falseish = expect.falsish = expect.falseish = falseish
    

    或者类expect(original_expect): def falseish(self): # 查看 expect() 源代码以获取可用的辅助函数 self._assert(self._actual == False, "to be falseish")

    完成了!

    请注意,匹配器清楚地传达了哪些是重要的:它断言了什么,以及它生成的错误消息。没有冗余!

    将此与您添加到像sureensure等更成熟的包中的匹配器进行比较 - 我认为pyexpect更简单 - 您可以仅将属性分配给expect或创建一个带有更多方法的本地子类。

  4. 原生not支持:如果您定义了一个匹配器,您不需要定义它的逆或执行任何特殊操作来获得它。这意味着对于每个equals这样的匹配器,您会自动获得它的逆,即not_equals。这个逆可以通过多种方式调用。

    您只需像这样在匹配器前加上not_前缀:

    expect(foo).not_equals(bar)
    expect(some_function).not_to_raise()
    

    或者您可以在匹配器之前将not作为路径的一部分包含,如下所示:

    expect(foo).not_.to_equal(bar)
    expect(foo).not_to.equal(bar)
    expect(foo).to_not.equal(bar)
    expect(foo).to_not_be.equal(bar)
    # or go all out - but just because it works doesn't mean it's sensible
    expect(foo).is.just_a_little.not_quite_the_same_as_it.equals(bar)
    

    也就是说,您可以在标识符的开始、中间或末尾包含单词not - 只确保通过蛇形命名法将其与标识符分开。

    这对于每个匹配器的所有别名都适用,因此无需额外的工作。

    有关更多示例,请参阅匹配器的测试套件。

    如果您想添加自己的匹配器,有时如果您将期望实现为多个连续的检查,则逆操作可能不会自动工作。在这种情况下,逆匹配器可能会断言错误的事情,因为检查的顺序在反转情况下没有意义。如果发生这种情况,请查看expect._assert_if_positive()expect._assert_if_negative()expect._is_negative()。但是请注意,好的匹配器应该只需要很少使用。

  5. 出色的错误消息:pyexpect非常注意确保体验错误的每个方面都尽可能简洁和有用。所有错误消息都具有相同的格式,始终从预期内容开始,然后由匹配器自定义以尽可能包含更多信息。

    expect(23).not_.to_equal(23)
    Expect 23 not to equal 23
    

    如果您编写自己的断言方法来增强单元测试,则很容易得到长的堆栈跟踪,因为实际的断言发生在调用匹配器之一的某些堆栈帧中。

    考虑以下这样的断言(有点编造,好吧。但我相信您在您的项目中见过这种情况)

    from unittest import TestCase, main
    class Test(TestCase):
        def assert_equals(self, actual, expected):
            self.assertEquals(actual, expected)
        def assert_something(self, something):
            self.assert_equals(something, 'something')
        def test_something(self):
            self.assert_something('fnord')
    main()
    

    这将给出如下输出

    FAIL: test_something (__main__.Test)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      method test_something in test.py at line 11
        self.assert_something('fnord')
      method assert_something in test.py at line 8
        self.assert_equals(something, 'something')
      method assert_equals in test.py at line 5
        self.assertEquals(actual, expected)
    AssertionError: 'fnord' != 'something'
    

    即使在这样简单的情况下,实际错误也距离实际错误有4行之远。

    但是使用pyexpect,这样的测试

    from pyexpect import expect
    def something(self):
        self._assert(self._actual == 'something', "to be 'something'")
    expect.something = something
    
    from unittest import TestCase, main
    class Test(TestCase):
        def test_something(self):
            expect('fnord').to_be.something()
    main()
    

    给出如下输出(标准unittest.main()

    FAIL: test_something (__main__.Test)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "test_example.py", line 11, in test_something
        expect('fnord').to_be.something()
      File "/Users/dwt/Code/Projekte/pyexpect/internals.py  # pyexpect_internals_hidden_in_backtraces
        raise assertion
    AssertionError: Expect 'fnord' to be something
    

    nosetests

    FAIL: test_example:Test.test_something
      mate +11  test_example.py  # test_something
        expect('fnord').to_be.something()
      mate +150 internals.py  # pyexpect_internals_hidden_in_backtraces
        raise assertion
    AssertionError: Expect 'fnord' to be something
    

    py.test中这甚至更漂亮

    self = <test_example.Test testMethod=test_something>
    def test_something(self):
    >       expect('fnord').to_be.something()
    E       AssertionError: Expect 'fnord' to be something
    

    请注意,这里的实际匹配器someting()调用另一个方法_assert来进行实际的断言并构建错误消息,但这一切都不会在堆栈跟踪中显示?这是任何您在匹配器中调用的方法的真实情况,所以调用您的api或您需要触发断言的任何东西,并享受生成的错误消息的可读性。

    真正难以阅读的错误消息的一个常见原因是消息太长。你有没有遇到过类似的情况?

    FAIL: test_example (__main__.DemoTest)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "untitled", line 15, in test_example
        self.assertEquals(long_actual, long_expected)
    AssertionError: ['foo', 'bar', 'baz', 'quoox', 'quaax', 'quuuxfoo', 'bar', 'baz', 'quoox', 'quaax', 'quuux'] != ['foo', 'bar', 'baz', 'quoox', 'quaax', 'quuux', 'foo', 'bar', 'baz', 'quoox', 'quaax', 'quuux', 'foo', 'bar', 'baz', 'quoox', 'quaax', 'quuux']
    

    找到预期和实际对象输出甚至在哪里被分隔开的地方都很困难,更不用说它们之间有什么不同了。

    pyexpect将长错误消息格式化为多行,这样你就可以总是看到投诉开始的地方,并且更容易在心理上对比两个对象

    FAIL: test_example (__main__.DemoTest)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "test.py", line 16, in test_example
        expect(long_actual) == long_expected
      File "pyexpect/internals.py", line 17, in pyexpect_internals_hidden_in_backtraces
        raise exception
    AssertionError: Expect ['foo', 'bar', 'baz', 'quoox', 'quaax', 'quuuxfoo', 'bar', 'baz', 'quoox', 'quaax', 'quuux']
    
    to equal ['foo', 'bar', 'baz', 'quoox', 'quaax', 'quuux', 'foo', 'bar', 'baz', 'quoox', 'quaax', 'quuux', 'foo', 'bar', 'baz', 'quoox', 'quaax', 'quuux']
    

    tl;dr:错误消息更容易阅读,并且在错误和原因之间没有太多冗余内容来分散你的注意力。应该是这样。

  6. 单元测试之外的用法:你可以将这个包用作独立的断言包,它提供的断言比仅仅使用assert更具有表现力,并且还提供改进的错误消息。

    只需在需要的地方断言,通过快速失败来获得健壮的代码

    from pyexpect import expect
    def some_method(some_argument):
        expect(some_argument).is_.between(3,20)
        something(some_argument)
    

    如果你需要的话,你可以将断言从抛出转换为返回一个(bool, string)元组,这样你就可以在你的API中重用它。

    from pyexpect import expect
    def some_api(something):
        was_success, explanation = expect.returning(something) == 23
        if not was_success: register_error(explanation)
    

    如果你需要的话,你可以覆盖生成的错误消息,而无需更改匹配器。有关详细信息,请参阅expect.with_message()

  7. 测试覆盖率:当然,pyexpect具有完整的测试覆盖率,确保它确实做了你所期望的事情。

  8. 认为这份文档中有可以改进的地方?发送一个pull request吧。 :)

项目详情


下载文件

下载适合你平台的文件。如果你不确定选择哪一个,了解更多关于安装包的信息。

源分布

pyexpect-1.0.22.tar.gz (31.3 kB 查看散列)

上传时间

构建分布

pyexpect-1.0.22-py3-none-any.whl (27.4 kB 查看散列)

上传时间 Python 3

由以下机构支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误记录 StatusPage StatusPage 状态页面