Skip to content

Latest commit

 

History

History
1328 lines (947 loc) · 85 KB

File metadata and controls

1328 lines (947 loc) · 85 KB

十七、可测试性设计

高质量的程序有自动测试。我们需要使用一切可以使用的东西来确保我们的软件正常工作。黄金法则是:要可交付,特性必须具有自动单元测试。如果没有自动化的单元测试,就不能信任某个特性可以工作,也不应该使用它。根据 Kent Beck 的说法,在极限编程中解释了

"Any program feature without an automated test simply doesn't exist."

关于程序功能的自动测试,有以下两个要点:

  • 自动:这意味着不需要人工判断。测试涉及一个脚本,该脚本将实际响应与预期响应进行比较。
  • 功能:对软件的元素进行单独测试,以确保它们单独工作。在某些上下文中,功能是相当广泛的概念,涉及用户观察到的功能。当进行单元测试时,特性通常要小得多,并且是指单个软件组件的行为。每个单元都有足够的软件来实现给定的功能。理想情况下,它是一个 Python 类。但是,它也可以是更大的单元,例如模块或包。

Python 有两个内置的测试框架,使得编写自动单元测试变得容易。我们将考虑使用doctestunittest进行自动测试。doctest提供了一种非常简单的测试写作的方法。unittest套餐要复杂得多。

我们还将介绍流行的pytest框架,它比unittest更易于使用。在某些情况下,我们将使用unittest.TestCase类编写测试,但使用pytest复杂的测试发现执行测试。在其他情况下,我们将使用pytest完成几乎所有操作。

我们将研究一些使测试实用所需的设计考虑因素。为了可测试性,通常需要分解复杂的类。正如我们在第 15 章设计原则和模式中提到的,我们希望公开依赖项具有灵活的设计;依赖倒置原则也将有助于创建可测试类。

更多想法,请阅读Ottinger 和 Langr 的FIRST单元测试属性—FastIsolatedRepeatable自我验证Timed。在大多数情况下,可重复自验证需要一个自动化测试框架。及时意味着测试是在测试代码之前编写的。更多信息,请参考http://pragprog.com/magazines/2012-01/unit-tests-are-first

在本章中,我们将介绍以下主题:

  • 定义和隔离测试单元
  • 使用doctest定义测试用例
  • 使用安装和拆卸
  • TestCase类层次结构
  • 使用外部定义的预期结果
  • 使用pytestfixtures
  • 自动集成测试或自动性能测试

技术要求

本章的代码文件可在上找到 https://git.io/fj2UM

定义和隔离测试单元

当我们认为测试是必不可少的,可测试性是面向对象编程的一个重要设计考虑。我们的设计还必须支持测试和调试,因为只有出现工作的类是没有价值的。有证据表明它有效的类更有价值。

不同类型的测试形成一种层次结构。层次结构的基础是单元测试。在这里,我们单独测试每个类或函数,以确保它满足 API 的合同义务。每个类或函数都是一个独立的测试单元。在这一层之上是集成测试。一旦我们知道每个类和函数单独工作,我们就可以测试类的组和集群。我们也可以测试整个模块和整个软件包。一旦集成测试开始工作,下一层就是整个应用程序的自动化测试。这不是测试类型的详尽列表。我们还可以进行性能测试和安全漏洞测试。

在本章中,我们将重点讨论自动化单元测试,因为它是所有应用程序的核心。测试的层次结构揭示了重要的复杂性。单个类或一组类的测试用例可以定义得非常狭窄。随着我们在集成测试中引入更多的单元,输入领域也在增长。当我们尝试测试整个应用程序时,整个人类行为光谱都会成为潜在的输入。当我们更广泛地观察时,我们发现这可能包括关闭设备、拔出插头,甚至将设备从桌子上推下来,看看它们在被扔到硬木地板上三英尺后是否还能工作。人类行为领域的巨大性使得完全自动化所有可能的应用程序测试变得非常困难。我们将关注最容易自动测试的东西。一旦单元测试起作用,较大的聚合系统就更有可能起作用。

最小化依赖关系

当我们设计一个类时,我们还必须考虑该类周围的依赖网络:依赖于它的类和依赖于它的类。为了简化类定义的测试,我们需要将它与周围的类隔离开来。关于这方面的更多想法,请参考第 15 章设计原则和模式

一个例子是Deck类,它依赖于Card类。我们可以很容易地单独测试Card,但是当我们想要测试Deck类时,我们需要将其从Card的定义中剔除。

以下是我们已经研究过的Card的一个(众多)先前定义:

    import     enum

    class     Suit(enum.Enum):
    CLUB =     "♣"
                DIAMOND =     "♦"
                HEART =     "♥"
                SPADE =     "♠"        
        class     Card:
        def         __init__    (
            self    , rank:     int    , suit: Suit, hard:     int     =     None    , soft:     int     =     None
                ) ->     None    :
            self    .rank = rank
            self    .suit = suit
            self    .hard = hard     or         int    (rank)
            self    .soft = soft     or         int    (rank)
        def         __str__    (    self    ) ->     str    :
        return f"{self.rank!s}{self.suit.value!s}"
    class     AceCard(Card):
        def         __init__    (    self    , rank:     int    , suit: Suit) ->     None    :
            super    ().    __init__    (rank, suit,     1    ,     11    )
    class     FaceCard(Card):
        def         __init__    (    self    , rank:     int    , suit: Suit) ->     None    :
            super    ().    __init__    (rank, suit,     10    ,     10    )

我们可以看到,这些类中的每一个都有一个简单的继承依赖关系。每个类都可以单独测试,因为只有两个方法和四个属性。

我们可以(错误地)设计Deck类。以下是一个糟糕的示例,并且具有一些有问题的依赖关系:

    import random

        class     Deck1(    list    ):

        def         __init__    (    self    , size:     int    =    1    ) ->     None    :
            super    ().    __init__    ()
            self    .rng = random.Random()
            for     d     in         range    (size):
                for     s     in         iter    (Suit):
                cards: List[Card] = (
                    [cast(Card, AceCard(    1    , s))]
                    + [Card(r, s)     for     r     in         range    (    2    ,     12    )]
                    + [FaceCard(r, s)     for     r     in         range    (    12    ,     14    )]
                )
                    super    ().extend(cards)
            self    .rng.shuffle(    self    )

这种设计有两个缺陷。首先,它与Card类层次结构中的三个类密切相关。对于独立的单元测试,我们无法将DeckCard分离。其次,它依赖于随机数生成器,因此很难创建可重复的测试。当我们回顾第 15 章设计原则和模式时,我们可以将这些问题视为没有遵循依赖倒置原则。

一方面,Card是一个非常简单的类。我们可以在保留Card的情况下测试Deck的这个版本。另一方面,我们可能希望将Deck与扑克卡或皮诺切勒卡一起使用,这两种卡的行为与 21 点卡不同。

理想的情况是使Deck独立于任何特定的Card实现。如果我们做得好,那么我们不仅可以独立于任何Card实现来测试Deck,还可以使用CardDeck定义的任意组合。

这里是我们分离其中一个依赖项的首选方法。我们可以将这些依赖项放入工厂函数中,如下例所示:

    class     LogicError(    Exception    ):
        pass        

        def     card(rank:     int    , suit: Suit) -> Card:
        if     rank ==     1    :
            return     AceCard(rank, suit)
        elif         2     <= rank <     11    :
            return     Card(rank, suit)
        elif         11     <= rank <     14    :
            return     FaceCard(rank, suit)
        else    :
            raise     LogicError(    f"Rank         {    rank    }         invalid"    )

card()函数将根据请求的秩建立Card的适当子类。这允许Deck类使用此函数,而不是直接构建Card类的实例。我们通过插入一个中间函数来分隔这两个类定义。

还有其他技术可以将Card类与Deck类分开。我们可以将工厂函数重构为一种Deck方法。我们还可以通过类级属性甚至初始化方法参数使类名成为单独的绑定。以下示例通过在初始化方法中使用更复杂的绑定来避免工厂函数:

    class     Deck2(    list    ):

        def         __init__    (
            self    ,
        size:     int    =    1    ,
        random: random.Random=random.Random(),
        ace_class: Type[Card]=AceCard,
        card_class: Type[Card]=Card,
        face_class: Type[Card]=FaceCard,
    ) ->     None    :
            super    ().    __init__    ()
            self    .rng = random
            for     d     in         range    (size):
                for     s     in         iter    (Suit):
                cards = (
                    [ace_class(    1    , s)]
                    + [card_class(r, s)     for     r     in         range    (    2    ,     12    )]
                    + [face_class(r, s)     for     r     in         range    (    12    ,     14    )]
                )
                    super    ().extend(cards)
            self    .rng.shuffle(    self    )

虽然这个初始化很冗长,Deck类不再紧密地绑定到Card类层次结构或特定的随机数生成器。出于测试目的,我们可以提供一个具有已知种子的随机数生成器。我们还可以用其他类(如tuple)替换各种Card类定义,以简化我们的测试。

请注意为此类提供的默认值的类型提示。为random参数提供的对象应该是random.Random类型的实例,默认值是该类型的对象。类似地,对于三个卡类,每个卡类都必须是一个TypeCard类的子类。Type[Card]提示允许从Card派生的任何类。这允许mypy确认超控可能有效。在测试模块中检查类型提示可能很困难,因为从版本 3.8.2 开始,pytest 没有完整的类型提示存根。

在下一节中,我们将关注Deck类的另一个变体。这将使用card()工厂功能。该工厂函数将Card层次结构绑定和按等级划分卡类的规则封装到一个可测试的位置。

创建简单的单元测试

我们将为Card类层次结构和card()工厂函数创建一些简单的单元测试。

由于Card类非常简单,因此没有理由进行过于复杂的测试。总是有可能在不必要的复杂情况下出错。在一个测试驱动的开发过程中进行不加思考的可能会让我们觉得需要为一个只有少量属性和方法的类编写大量不太有趣的单元测试。

理解测试驱动开发是建议很重要,而不是像质量守恒那样的自然法则。这也不是一种必须不假思索地遵循的仪式。

关于命名测试方法,有几个学派。我们将重点介绍一种命名方式,其中包括描述测试条件和预期结果。以下是有关此主题的三个变体:

  • StateUnderTest_should_ExpectedBehavior
  • when_StateUnderTest_should_ExpectedBehavior
  • UnitOfWork_StateUnderTest_ExpectedBehavior

有关更多信息,请参阅:

http://osherove.com/blog/2005/4/3/naming-standards-for-unit-tests.html

名称的StateUnderTest部分通常在包含测试的类中很明显,并且可以从方法名称中省略。这意味着单个测试用例可以强调名称的should_ExpectedBehavior部分。为了符合unittest.TestCase类的工作方式,每个测试行为必须从test_开始。这导致我们建议在对unittest.TestCase子类使用单独的测试方法时,将test_should_*ExpectedBehavior*作为测试用例名称的模式。对于pytest函数,我们将使用不同的命名模式。

可以将unittest模块配置为使用不同的模式来命名测试方法。我们可以将其更改为查找when_而不是test_。名称的改进似乎不值得付出所需的努力。

例如,这是对Card类的测试:

    class     TestCard(unittest.TestCase):

        def     setUp(    self    ) ->     None    :
            self    .three_clubs = Card(    3    , Suit.CLUB)

        def     test_should_returnStr(    self    ) ->     None    :
            self    .assertEqual(    "3♣"    ,     str    (    self    .three_clubs))

        def     test_should_getAttrValues(    self    ) ->     None    :
            self    .assertEqual(    3    ,     self    .three_clubs.rank)
            self    .assertEqual(Suit.CLUB,     self    .three_clubs.suit)
            self    .assertEqual(    3    ,     self    .three_clubs.hard)
            self    .assertEqual(    3    ,     self    .three_clubs.soft)

我们定义了一个 testsetUp()方法,该方法创建被测试类的对象。我们还为此对象定义了两个测试。因为这里没有真正的交互,所以测试名称中没有被测试的状态;它们是简单的通用行为,应该始终有效。

这是一个非常小的类定义的大量测试代码。这就提出了一个问题,即测试代码的数量是否在某种程度上过多。答案是;这不是过多的测试代码。没有法律规定应用程序代码应该多于测试代码。实际上,将测试代码量与应用程序代码量进行比较是没有意义的。最重要的是,即使是一个很小的类定义也可能存在 bug,并且可能需要复杂的测试来确保 bug 不存在。

简单地测试属性的值似乎无法测试此类中的处理。关于测试属性值,有两种观点:

  • 黑盒透视图意味着我们忽略了实现。在这种情况下,我们需要测试所有属性。例如,属性可以是属性,必须对其进行测试。
  • 白框透视图意味着我们可以检查实现细节。在执行这种类型的测试时,我们可以更加谨慎地测试哪些属性。例如,suit属性不值得进行太多测试。然而,hardsoft属性确实需要测试。

有关更多信息,请参阅:

http://en.wikipedia.org/wiki/White-box_testinghttp://en.wikipedia.org/wiki/Black-box_testing

当然,我们需要测试Card类层次结构的其余部分。我们将向您展示AceCard测试用例。在本例之后,FaceCard测试用例应该是清晰的:

    class     TestAceCard(unittest.TestCase):

        def     setUp(    self    ) ->     None    :
            self    .ace_spades = AceCard(    1    , Suit.SPADE)

        @unittest.expectedFailure
                    def     test_should_returnStr(    self    ) ->     None    :
            self    .assertEqual(    "A♠"    ,     str    (    self    .ace_spades))

        def     test_should_getAttrValues(    self    ) ->     None    :
            self    .assertEqual(    1    ,     self    .ace_spades.rank)
            self    .assertEqual(Suit.SPADE,     self    .ace_spades.suit)
            self    .assertEqual(    1    ,     self    .ace_spades.hard)
            self    .assertEqual(    11    ,     self    .ace_spades.soft)

这个测试用例还设置了一个特定的Card实例,以便我们可以测试字符串输出。它检查此固定卡的各种属性。

注意,test_should_returnStr()测试将失败。AceCard类的定义不显示此测试定义中所示的值。测试不正确或类定义不正确。单元测试在类设计中发现了这个错误。

FaceCard类需要进行类似的测试。它将类似于AceCard类的测试。我们不会在这里介绍它,但会留给您作为练习。

当我们有许多测试时,将它们组合成一套测试可能会有所帮助。我们下一步将讨论这个问题。

创建测试套件

正式定义测试套件通常很有帮助。unittest包默认能够发现测试。当聚合来自多个测试模块的测试时,有时在每个测试模块中创建一个测试套件是有帮助的。如果每个模块都定义了一个suite()函数,我们可以将测试发现替换为从每个模块导入suite()函数。另外,如果我们定制TestRunner,我们必须使用套件。我们可以执行以下测试:

    def     suite2() -> unittest.TestSuite:
    s = unittest.TestSuite()
    load_from = unittest.defaultTestLoader.loadTestsFromTestCase
    s.addTests(load_from(TestCard))
    s.addTests(load_from(TestAceCard))
    s.addTests(load_from(TestFaceCard))
        return     s

我们根据三个TestCase类定义构建一个套件,然后将该套件提供给unittest.TextTestRunner()实例。我们在unittest中使用默认的TestLoader。此TestLoader检查TestCase类以定位所有测试方法。TestLoader.testMethodPrefix的值为test,这是在类中识别测试方法的方式。加载程序使用每个方法名称创建一个单独的测试对象。

使用TestLoaderTestCase的适当命名方法构建测试实例是使用TestCase的两种方法之一。在后面的部分中,我们将看到手动创建TestCase的实例;这些例子我们不会依赖TestLoader。我们可以使用以下代码运行此套件:

    if     __name__ ==     "__main__"    :
    t = unittest.TextTestRunner()
    t.run(suite2())

我们将看到如下代码所示的输出:

...F.F 
====================================================================== 
FAIL: test_should_returnStr (__main__.TestAceCard) 
---------------------------------------------------------------------- 
Traceback (most recent call last): 
  File "p3_c15.py", line 80, in test_should_returnStr 
    self.assertEqual("A♠", str(self.ace_spades)) 
AssertionError: 'A♠' != '1♠' 
- A+ 1====================================================================== 
FAIL: test_should_returnStr (__main__.TestFaceCard) 
---------------------------------------------------------------------- 
Traceback (most recent call last): 
  File "p3_c15.py", line 91, in test_should_returnStr 
    self.assertEqual("Q♥", str(self.queen_hearts)) 
AssertionError: 'Q♥' != '12♥' 
- Q+ 12---------------------------------------------------------------------- 
Ran 6 tests in 0.001s 

FAILED (failures=2) 

TestLoader类从每个TestCase类创建了两个测试。这给了我们总共六个测试。测试名称是方法名称,以test开头。

显然,我们有一个问题。我们的测试提供了我们的类定义不符合的预期结果。为了通过这套简单的单元测试,我们需要为Card类做更多的开发工作。我们将把修复作为练习留给您。

开始设计测试一开始似乎令人望而生畏。有一些指导方针可能会有所帮助。在下一节中,我们将讨论*边-*经常限制,*角-*经常是组件之间的接口,这可以帮助我们设计更好的测试。

包括边缘和角落案例

当我们将Deck类作为一个整体进行测试时,我们需要确认一些事情:它生成了所有必需的Cards类,并且它确实正确地洗牌。我们真的不需要测试它是否正确处理,因为我们依赖于listlist.pop()方法;由于它们是 Python 的一流部分,因此不需要额外的测试。

我们想独立于任何特定的Card类层次结构来测试Deck类构造和洗牌。如前所述,我们可以使用工厂函数使两个DeckCard定义相互独立。引入工厂功能将引入更多测试。考虑到之前在Card类层次结构中暴露的 bug,这不是一件坏事。

下面是对 factory 功能的测试:

    class     TestCardFactory(unittest.TestCase):

        def     test_rank1_should_createAceCard(    self    ) ->     None    :
        c = card(    1    , Suit.CLUB)
            self    .assertIsInstance(c, AceCard)

        def     test_rank2_should_createCard(    self    ) ->     None    :
        c = card(    2    , Suit.DIAMOND)
            self    .assertIsInstance(c, Card)

        def     test_rank10_should_createCard(    self    ) ->     None    :
        c = card(    10    , Suit.HEART)
            self    .assertIsInstance(c, Card)

        def     test_rank10_should_createFaceCard(    self    ) ->     None    :
        c = card(    11    , Suit.SPADE)
            self    .assertIsInstance(c, Card)

        def     test_rank13_should_createFaceCard(    self    ) ->     None    :
        c = card(    13    , Suit.CLUB)
            self    .assertIsInstance(c, Card)

        def     test_otherRank_should_exception(    self    ) ->     None    :
            with         self    .assertRaises(LogicError):
                c     = card(    14    , Suit.DIAMOND)
            with         self    .assertRaises(LogicError):
                c     = card(    0    , Suit.DIAMOND)

我们没有测试所有 13 个等级,因为 2 到 10 个等级应该都是相同的。相反,我们遵循了 Boris Beizer 在软件测试技术一书中的建议:

"Bugs lurk in corners and congregate at boundaries."

测试用例涉及每个卡范围的边缘值。因此,我们有值 1、2、10、11 和 13 以及非法值 0 和 14 的测试用例。我们用最小值、最大值、一个低于最小值、一个高于最大值将每个范围括起来。

我们已经修改了测试命名的模式。在本例中,我们测试了几个不同的状态。我们已经修改了这些更简单的名称,以遵循模式test_*StateUnderTest*_should_*ExpectedBehavior*。似乎没有令人信服的理由将这些测试分成单独的类来分解测试状态

在下一节中,我们将研究处理依赖对象的方法。这将允许我们单独测试每个单元。

使用模拟对象消除依赖关系

为了测试Deck,我们有以下两种选择来处理Card类层次结构中的依赖关系:

  • Mocking:我们可以为Card类创建一个 mock(或替身)类,并创建一个 mockcard()工厂函数来生成 mock 类的实例。使用模拟对象的优点是,我们可以真正确信被测试的单元在一个类中没有解决方法,这弥补了另一个类中的错误。一个罕见的潜在缺点是,我们可能必须调试超级复杂模拟类的行为,以确保它是真实类的有效替代。复杂的模拟对象表明真实对象太复杂,需要重构。
  • 集成:如果我们对Card类层次结构有效,card()工厂功能有效有一定程度的信任,我们可以利用它们来测试Deck。这偏离了纯单元测试的主要道路,在纯单元测试中,出于测试目的省略了所有依赖项。这样做的缺点是,一个损坏的基础类将导致依赖它的所有类中的大量测试失败。此外,很难对 API 与非模拟类的一致性进行详细测试。模拟类可以跟踪调用历史,从而可以跟踪对模拟对象的调用的详细信息。

unittest包包括unittest.mock模块,该模块可用于为测试目的对现有类进行修补。它还可以用于提供完整的模拟类定义。稍后,当我们看pytest时,我们将结合unittest.mock对象和pytest测试框架。

本节中的示例未在测试中使用广泛的类型暗示。在大多数情况下,测试应该顺利通过mypy。正如我们前面提到的,pytest版本 3.8.2。没有完整的类型存根集,因此在运行mypy时必须使用--ignore-missing-imports选项。大多数情况下,模拟对象提供类型提示,允许mypy确认它们被正确使用。

当我们设计一个类时,我们必须考虑对单元测试必须被嘲弄的依赖关系。在Deck的情况下,我们需要模拟以下三个依赖项:

  • Card类:这个类非常简单,我们可以为这个类创建一个 mock,而无需基于现有的实现。由于Deck类行为不依赖于Card的任何特定特性,因此我们的模拟对象可以很简单。 *** card()工厂:这个函数需要替换为一个 mock,我们可以用它来确定Deck是否正确调用了这个函数。* 方法random.Random.shuffle()方法:为了确定是否使用正确的参数值调用了该方法,我们可以提供一个模拟来跟踪使用情况,而不是实际执行任何洗牌。**

**以下是使用card()工厂功能的Deck版本:

    class     DeckEmpty(    Exception    ):
        pass

        class     Deck3(    list    ):

        def         __init__    (
                self    ,
            size:     int    =    1    ,
            random: random.Random=random.Random(),
            card_factory: Callable[[    int    , Suit], Card]=card
    ) ->     None    :
            super    ().    __init__    ()
            self    .rng = random
            for     d     in         range    (size):
                super    ().extend(
                [card_factory(r, s)
                     for     r     in         range    (    1    ,     14    )
                     for     s     in         iter    (Suit)])
            self    .rng.shuffle(    self    )

        def     deal(    self    ) -> Card:
            try    :
                return         self    .pop(    0    )
            except         IndexError    :
                raise     DeckEmpty()

这个定义有两个依赖项,它们被专门称为__init__()方法的参数。它需要一个随机数生成器random和一个卡片工厂card_factory。它具有合适的默认值,因此可以非常简单地在应用程序中使用。还可以通过提供模拟对象而不是默认对象来测试它。

我们包含了一个deal()方法,该方法通过使用pop()从集合中删除Card的实例来更改对象。如果甲板为空,deal()方法将引发DeckEmpty异常。

下面是一个测试用例,它向您展示了甲板是正确构建的:

    import     unittest
    import     unittest.mock

    class     TestDeckBuild(unittest.TestCase):

        def     setUp(    self    ) ->     None    :
            self    .mock_card = unittest.mock.Mock(    return_value    =unittest.mock.sentinel.card)
            self    .mock_rng = unittest.mock.Mock(    wraps    =random.Random())
            self    .mock_rng.shuffle = unittest.mock.Mock()

            def     test_Deck3_should_build(    self    ) ->     None    :
        d = Deck3(    size    =    1    ,     random    =    self    .mock_rng,     card_factory    =    self    .mock_card)
            self    .assertEqual(    52     * [unittest.mock.sentinel.card], d)
            self    .mock_rng.shuffle.assert_called_with(d)
            self    .assertEqual(    52    ,     len    (    self    .mock_card.mock_calls))
        expected = [
            unittest.mock.call(r, s)
                for     r     in         range    (    1    ,     14    )
                for     s     in     (Suit.CLUB, Suit.DIAMOND, Suit.HEART, Suit.SPADE)
        ]
            self    .assertEqual(expected,     self    .mock_card.mock_calls)

我们在这个测试用例的setUp()方法中创建了两个模拟。模拟卡工厂功能mock_card是一个Mock功能。定义的返回值是单个mock.sentinel,card对象,而不是一个不同的Card实例。当我们使用类似于mock.sentinel.card的表达式引用mock.sentinel对象的属性时,该表达式会在必要时创建一个新对象,或者检索现有对象。实现了单例设计模式;只有一个sentinel对象具有给定的名称。这将是一个独特的对象,允许我们确认创建了正确数量的实例。因为sentinel不同于所有其他 Python 对象,我们可以在没有返回None的正确return语句的情况下区分函数。

我们创建了一个模拟对象mock_rng,用于包装random.Random()生成器的一个实例。这个Mock对象将作为一个适当的随机对象,有一个区别。我们将shuffle()方法替换为一个Mock函数,该函数返回None。这为我们提供了一个适当的方法返回值,并允许我们确定使用适当的参数值调用了shuffle()方法。

我们的测试使用两个模拟对象创建一个Deck3实例。然后我们可以对Deck3实例d做出以下断言:

  • 创建了 52 个对象。预计将有 52 份mock.sentinel,向我们展示了只有 factory 函数用于创建对象;所有对象都是由模拟创建的哨兵。
  • Deck实例作为参数调用了shuffle()方法。这向我们展示了模拟对象如何跟踪其调用。我们可以使用assert_called_with()来确认在调用shuffle()时参数值是否符合要求。
  • 工厂函数被调用了 52 次。模拟对象的mock_calls属性是该对象的整个使用历史。从技术上讲,这个断言是多余的,因为下一次测试将暗示这种情况。
  • 使用预期等级和适合值的特定列表调用 factory 函数。

模拟对象将记录调用的方法序列。在下一节中,我们将研究检查模拟对象的方法,以确保其他单元正确使用模拟对象。

使用模拟观察行为

前面的模拟对象用于测试Deck类是如何构建的。有 52 名相同的哨兵,很难确认 aDeck交易是否正确。我们将定义一个不同的 mock 来测试 deal 特性。

下面是第二个测试用例,以确保Deck类正确处理:

    class     TestDeckDeal(unittest.TestCase):

        def     setUp(    self    ) ->     None    :
            self    .mock_deck = [
                getattr    (unittest.mock.sentinel,     str    (x))     for     x     in         range    (    52    )
        ]
            self    .mock_card = unittest.mock.Mock(
                side_effect    =    self    .mock_deck)
            self    .mock_rng = unittest.mock.Mock(
                wraps    =random.Random())
            self    .mock_rng.shuffle = unittest.mock.Mock()

        def     test_Deck3_should_deal(    self    ) ->     None    :
        d = Deck3(    size    =    1    ,     random    =    self    .mock_rng,     card_factory    =    self    .mock_card)
        dealt = []
            for     i     in         range    (    52    ):
            card = d.deal()
            dealt.append(card)
            self    .assertEqual(dealt,     self    .mock_deck)

        def     test_empty_deck_should_exception(    self    ) ->     None    :
        d = Deck3(    size    =    1    ,     random    =    self    .mock_rng,     card_factory    =    self    .mock_card)
            for     i     in         range    (    52    ):
                card     = d.deal()
            self    .assertRaises(DeckEmpty, d.deal)

此卡片工厂函数的模拟将side_effect参数用于Mock()。如果提供了 iterable,side_effect特性每次调用时都返回 iterable 的另一个值。

在本例中,我们使用sentinel对象构建了 52 个不同的sentinel对象;我们将使用这些对象而不是Card对象来将Deck3类与Card类层次结构隔离开来。getattr(unittest.mock.sentinel, str(x))表达式将使用数字的字符串版本x,并创建 52 个唯一的sentinel对象。

我们模仿了shuffle()方法,以确保这些卡片实际上没有重新排列。包装器意味着Random类的大部分功能都可以访问。然而,shuffle方法被取代。我们希望sentinel对象保持其原始顺序,以便我们的测试具有可预测的预期值。

第一个测试test_Deck3_should_deal将 52 张卡的交易结果累加到一个变量dealt。然后,它断言该变量具有来自原始模拟卡工厂的 52 个预期值。因为卡片工厂是一个模拟对象,所以它通过Mockside_effect特性返回各种sentinel对象。

第二个测试test_empty_deck_should_exception处理Deck实例中的所有卡片。然而,它又发出了一个 API 请求。断言是Deck.deal()方法将在处理所有卡后引发适当的异常。

因为Deck类相对简单,所以可以将TestDeckBuildTestDeckDeal组合成一个更复杂的模拟。虽然在这个例子中这是可能的,但重构测试用例以使其更简单既不是必要的,也不是必要的。事实上,过度简化测试可能无法正确测试 API 特性。

使用 doctest 定义测试用例

doctest模块为我们提供了验证文档字符串的方法。除了代码中的 docstring 之外,任何带有 Python REPL 风格响应的文档都可以通过doctest进行测试。这将把模块、类、函数和测试用例的文档合并到一个整洁的包中。

doctest案例被写入 docstring。一个doctest案例向我们展示了交互式 Python 提示符>>>;声明;以及预期的反应。doctest模块包含一个在 docstring 中查找这些示例的应用程序。它运行给定的示例,并将 docstring 中显示的预期结果与实际输出进行比较。

通过仔细设计 API,我们可以创建一个可以交互使用的类。如果可以交互使用,那么可以构建一个doctest示例来显示交互的预期结果。

事实上,设计良好的类有两个属性,即它可以交互使用,并且文档字符串中有doctest示例。许多内置模块包含 API 的doctest示例。我们可能选择下载的许多其他软件包也包括doctest示例。

通过一个简单的功能,我们可以提供以下文档:

    def     ackermann(m:     int    , n:     int    ) ->     int    :
        """Ackermann's Function
            ackermann(m, n) = $2 \\uparrow^{m-2} (n+3)-3$

            See http://en.wikipedia.org/wiki/Ackermann_function and
            http://en.wikipedia.org/wiki/Knuth%27s_up-arrow_notation.

            >>> from Chapter_17.ch17_ex1 import ackermann
            >>> ackermann(2,4)
            11
            >>> ackermann(0,4)
            5
            >>> ackermann(1,0)
            2
            >>> ackermann(1,1)
            3

            """
                    if     m ==     0    :
            return     n +     1
                    elif     m >     0         and     n ==     0    :
            return     ackermann(m -     1    ,     1    )
        elif     m >     0         and     n >     0    :
            return     ackermann(m -     1    , ackermann(m, n -     1    ))
        else    :
            raise     LogicError()

我们已经定义了 Ackermann 函数的一个版本。这个函数相当复杂,定义中包含了一些奇怪的数学符号。形式定义以以下两种方式显示:

  • 。此定义使用扩展的向上箭头表示法进行递归求幂。
  • 。这是以前实现的递归定义。

该定义包括 docstring 注释,其中包括来自交互式 Python 的五个示例响应。第一个示例输出是import语句,它不应该产生任何输出。其他四个示例输出向我们显示函数的不同值。

我们可以使用doctest模块运行这些测试。当doctest模块作为程序运行时,命令行参数是应该测试的文件。doctest程序定位所有 docstring,并在这些字符串中查找交互式 Python 示例。需要注意的是,doctest文档提供了用于定位字符串的正则表达式的详细信息。在我们的示例中,我们在最后一个doctest示例之后添加了一个难以看到的空行,以帮助doctest解析器。

我们可以从命令行运行doctest,如下所示:

$ python3 -m doctest Chapter_17/ch17_ex1.py

如果一切正常,这就是沉默。我们可以通过添加-v选项使其显示一些细节,如下所示:

$ python3 -m doctest -v Chapter_17/ch17_ex1.py

这将为我们提供解析的每个 docstring 以及从 docstring 收集的每个测试用例的详细信息。

输出将包括以下内容:

Trying:
 from Chapter_17.ch17_ex1 import ackermann
Expecting nothing
ok
Trying:
 ackermann(2,4)
Expecting:
 11
ok
Trying:
 ackermann(0,4)
Expecting:
 5
ok
Trying:
 ackermann(1,0)
Expecting:
 2
ok
Trying:
 ackermann(1,1)
Expecting:
 3
ok

Trying:子句显示了>>>示例中的语句。Expecting:显示以下结果行。最后一个ok告诉我们这个例子按照预期工作。详细的输出将向我们显示所有未经任何测试的类、函数和方法,以及具有测试的组件。这就证实了我们的测试在 docstring 中的格式是正确的。

在某些情况下,我们的输出将不容易与交互式 Python 匹配。在这些情况下,我们可能需要用一些注释来补充 docstring,这些注释修改测试用例和预期结果的解析方式。

有一个特殊的注释字符串,我们可以用于更复杂的输出。我们可以附加以下两个命令中的任意一个来启用(或禁用)各种可用的指令。以下是第一个命令:

# doctest: +DIRECTIVE

以下是第二个命令:

# doctest: -DIRECTIVE

我们可以对预期结果的处理方式进行十几次修改。其中大多数都是关于间距以及如何比较实际值和预期值的罕见情况。

doctest文件强调精确匹配原则

"doctest is serious about requiring exact matches in expected output." If even a single character doesn't match, the test fails. You'll need to build flexibility into some of the expected outputs. If building in flexibility gets too complex, it suggests that unitest might be a better choice.

以下是doctest的预期值和实际值不容易匹配的一些具体情况:

  • 对象的id()和默认repr()涉及物理内存地址;Python 不能保证它们是一致的。如果显示id()repr(),请使用#doctest: +ELLIPSIS指令,并在样本输出中将 ID 或地址替换为...
  • 浮点结果在不同平台之间可能不一致。始终显示带有格式或舍入的浮点数,以将位数减少为有意义的位数。使用"{:.4f}".format(value)round(value,4)确保忽略不重要的数字。
  • 虽然字典键现在已排序,但 Python 不保证集合排序。使用诸如sorted(some_set)之类的构造,而不是some_set
  • 当然,不能使用当前日期或时间,因为这将不一致。涉及日期或时间的测试通常需要通过模拟timedatetime强制特定日期或时间。
  • 操作系统详细信息(如文件大小或时间戳)可能会有所不同,不应在没有省略号的情况下使用。有时,可以在doctest脚本中包含有用的设置或拆卸,以管理操作系统资源。在其他情况下,模拟os模块是有帮助的。

这些考虑意味着我们的doctest模块可能包含一些额外的处理,而不仅仅是 API 的一部分。我们可能在交互式 Python 提示符下做了如下操作:

>>> sum(values)/len(values) 
3.142857142857143 

这向我们展示了特定实现的完整输出。我们不能简单地复制并粘贴到 docstring 中。浮点结果可能不同。我们需要执行类似以下代码的操作:

>>> round(sum(values)/len(values), 4) 
3.1429 

这四舍五入到一个值,该值在不同的实现之间不应该有所不同。

有时,将doctest和单元测试结合在一个综合测试包中是有帮助的。稍后我们将研究如何将doctest案例与unittest测试案例结合起来。

结合 doctest 和 unittest

doctest模块中有一个钩子,它将根据 docstring 注释创建一个适当的unittest.TestSuite。这允许我们在大型应用中同时使用doctestunittest

我们要做的是创建一个doctest.DocTestSuite()的实例。这将从模块的 docstring 构建一个套件。如果我们没有指定模块,那么当前正在运行的模块将用于构建套件。我们可以使用如下模块:

    import doctest 
    suite5 = doctest.DocTestSuite() 
    t = unittest.TextTestRunner(verbosity=2) 
    t.run(suite5) 

我们根据当前模块中的doctest字符串构建了一个套件suite5。我们在这个套房里用了unittest``TextTestRunner。作为替代方案,我们可以将doctest套件与其他TestCase套件结合起来,创建一个更大、更完整的套件。

随着我们的测试变得越来越复杂,我们需要组织我们的测试模块。在下一节中,我们将了解在 Python 项目中创建tests文件夹的总体情况。

创建更完整的测试包

对于较大的应用程序,每个应用程序模块可以有一个并行模块,其中包括该模块的TestCase。这可以形成两个并行封装结构:一个带有应用模块的src结构和一个带有测试模块的test结构。以下是两个并行目录树,它们向我们展示了模块集合:

src 
    __init__.py 
    __main__.py 
    module1.py 
    module2.py 
    setup.py 
tests 
    __init__.py 
    module1.py 
    module2.py 
    all.py 

显然,平行性并不精确。我们通常不会对setup.py进行自动单元测试。一个设计良好的__main__.py可能不需要单独的单元测试,因为它不应该包含太多的代码。我们将在第 18 章中介绍一些设计__main__.py的方法,处理命令行

我们可以创建一个顶层test/all.py模块,其主体将所有测试构建到单个套件中,如下所示:

import module1 
import module2 
import unittest 
import doctest 
all_tests = unittest.TestSuite() 
for mod in module1, module2: 
    all_tests.addTests(mod.suite()) 
    all_tests.addTests(doctest.DocTestSuite(mod)) 
t = unittest.TextTestRunner() 
t.run(all_tests) 

我们从其他测试模块中的套件构建了一个套件all_tests。这为我们提供了一个方便的脚本,它将运行作为发行版一部分提供的所有测试。

也可以使用unittest模块的测试发现功能来实现这一点。我们从命令行执行包范围的测试,代码如下:

python3 -m unittest tests/*.py

这将使用unittest的默认测试发现功能在给定文件中定位TestCase。这样做的缺点是依赖 shell 脚本特性而不是纯 Python 特性。通配符文件规范有时会使开发更加复杂,因为不完整的模块可能会被测试。

使用pytest比使用unittest发现和运行整个测试套件有一些优势。我们将在后面看到,pytest稍微简单一些。更重要的是,它可以定位更广泛的测试用例,包括函数以及unittest.TestCase的子类。这使得新软件功能的开发更加灵活和快速。

使用安装和拆卸

unittest模块有三个设置和拆卸级别。这些定义了不同类型的测试范围:方法、类和模块,如下所示:

  • 测试用例setUp()tearDown()方法:这些方法确保TestCase类中的每个单独测试方法都有适当的上下文。通常,我们会使用setUp()方法来创建被测试的单元和所需的任何模拟对象
  • 测试用例setUpClass()tearDownClass()方法:这些方法对TestCase类中的所有测试执行一次性设置(和拆卸)。这些方法包括每种方法的setUp()-testMethod()-tearDown()顺序。这是在数据库中插入和删除测试所需数据的好地方。
  • 模块setUpModule()tearDownModule()功能:这两个独立的功能为我们提供了在模块中所有TestCase类之前的一次性设置。这是一个在运行一系列TestCase类之前创建和销毁整个测试数据库的好地方。

我们很少需要定义所有这些setUp()tearDown()方法。有几个测试场景将成为我们可测试性设计的一部分。这些场景之间的本质区别在于所涉及的集成程度。如前所述,我们的测试层次结构中有三层:独立单元测试、集成测试和总体应用程序测试。这些测试层通过多种方式使用各种设置和拆卸功能,包括:

  • **独立单元,无集成,无依赖。**某些类或函数没有外部依赖关系;它们不依赖于文件、设备、其他进程或其他主机。其他类有一些可以模拟的外部资源。当TestCase.setUp()方法的成本和复杂性很小时,我们可以在那里创建必要的对象。如果模拟对象特别复杂,则类级别TestCase.setUpClass()可能更适合将重新创建模拟对象的成本分摊到多个测试方法中。
  • **内部集成,一些依赖性:**类或模块之间的自动集成测试通常涉及更复杂的设置情况。我们可能有一个复杂的setUpClass()甚至模块级setUpModule()来为集成测试准备上下文。在使用第 11 章通过 Shelve存储和检索对象、第 12 章中的数据库访问层时,通过 SQLite存储和检索对象,我们经常执行集成测试,包括我们的类定义和访问层。这可能涉及在测试数据库或架子上植入适当的测试数据。
  • 外部集成:我们可以对更大、更复杂的应用程序进行自动集成测试。在这些情况下,我们可能需要生成外部进程或创建数据库,并为它们添加数据种子。在这种情况下,我们可能需要setUpModule()准备一个空数据库,供模块中的所有TestCase类使用。当使用第 13 章中的 RESTful web 服务、传输和共享对象时,或者在第 19 章模块和包设计中的(PITL)中测试编程时,这种方法可能会有所帮助。

请注意,单元测试的概念并没有定义被测试的单元是什么。单元可以是类、模块、包,甚至是软件组件的集成集合。该单元需要与其环境隔离,以创建一个集中的测试。

在设计自动化集成测试时,选择要测试的组件非常重要。例如,我们不需要测试 Python 库;他们有自己的测试。类似地,我们不需要测试操作系统。例如,如果我们的软件删除了一个文件,我们不需要在删除文件后检查文件系统的完整性。我们不需要确定空间是否被回收了。我们通常需要相信库和操作系统能够正常工作。如果我们对基础设施有疑问,我们可以为操作系统或库运行测试套件;我们不需要重新发明它。集成测试必须重点测试我们编写的代码,而不是我们下载和安装的代码

使用操作系统资源的安装和拆卸

在许多情况下,测试用例可能需要特定的操作系统环境。当使用外部资源(如文件、目录或进程)时,我们可能需要在测试之前创建或初始化它们。我们可能还需要在测试之前删除资源,或者可能需要在测试结束时删除资源。

假设我们有一个函数rounds_final(),它应该处理给定的文件。我们需要在文件不存在的罕见情况下测试函数的行为。通常可以看到TestCase具有如下结构:

    import     os

    class     Test_Missing(unittest.TestCase):

        def     setUp(    self    ) ->     None    :
            try    :
            (Path.cwd() /     "data"     /     "ch17_sample.csv"    ).unlink()
                    except         OSError         as         e    :
                pass  # OK if file didn't exist

                    def     test_missingFile_should_returnDefault(    self    ) ->     None    :
            self    .assertRaises(
                FileNotFoundError    , rounds_final, (Path.cwd() /     "data"     /     "ch17_sample.csv"    )
        )

我们必须处理一个可能的异常,即试图删除一个首先不存在的文件。这个测试用例有一个setUp()方法来确保所需的文件丢失。一旦setuUp()确保文件确实消失,我们就可以使用丢失文件的参数p3_c15_sample.csv执行rounds_final()函数。我们预计这会引起一个FileNotFoundError错误。

注意,提升FileNotFoundError是 Pythonopen()方法的默认行为。这可能根本不需要测试。这引出了一个重要的问题:为什么要测试内置功能?如果我们正在执行黑盒测试,我们需要练习外部接口的所有功能,包括预期的默认行为。如果我们正在执行白盒测试,我们可能需要测试rounds_final()函数体中的异常处理try:语句。

ch17_sample.csv文件名在测试主体内重复。有些人认为枯燥的规则甚至应该适用于测试代码。在编写测试时,这种优化的价值是有限的:

It's okay for test code to be brittle. If a small change to the application leads to test failures, this really is a good thing. Tests should value simplicity and clarity, not robustness and reliability.

在下一节中,我们将看一些其他使用setUp()tearDown()作为数据库的示例。

对数据库使用安装和拆卸

当使用数据库和 ORM 层时,我们通常必须创建测试数据库、文件、目录或服务器进程。我们可能需要在测试通过后拆除测试数据库,以确保其他测试可以运行。我们可能不想在测试失败后破坏数据库;我们可能需要让数据库单独运行,以便检查结果行以诊断测试失败。

在复杂的多层体系结构中管理测试范围是很重要的。回顾第 12 章通过 SQLite存储和检索对象,我们不需要专门测试 SQLAlchemy ORM 层或 SQLite 数据库。这些组件在我们的应用程序测试之外有自己的测试程序。然而,由于 ORM 层从我们的代码中创建数据库定义、SQL 语句和 Python 对象的方式,我们不能轻易地模仿 SQLAlchemy,希望我们已经正确地使用了它。我们需要测试我们的应用程序使用 ORM 层的方式,而不必脱离测试 ORM 层本身。

一个更复杂的测试用例设置情况将涉及创建一个数据库,然后用给定测试的适当样本数据填充它。在使用 SQL 时,可能需要运行相当复杂的 SQL DDL 脚本来创建必要的表,然后运行另一个 SQL DML 脚本来填充这些表。关联的拆卸将是另一个复杂的 SQL DDL 脚本。

这种测试用例可能会变得冗长,因此我们将其分为三个部分:创建数据库和模式的有用函数、setUpClass()方法和单元测试的其余部分。

以下是创建数据库功能:

    from     Chapter_12.ch12_ex4     import     Base, Blog, Post, Tag, assoc_post_tag
    import     datetime
    import     sqlalchemy.exc
    from     sqlalchemy     import     create_engine

    def     build_test_db(name=    "sqlite:///./data/ch17_blog.db"    ):
        """
            Create Test Database and Schema
            """
                engine = create_engine(name,     echo    =    True    )
    Base.metadata.drop_all(engine)
    Base.metadata.create_all(engine)
        return     engine

这将通过删除与 ORM 类关联的所有表并重新创建表来构建一个新的数据库。这样做的目的是确保一个新的、空的数据库符合当前的设计,无论自上次运行单元测试以来该设计发生了多大的变化。

在本例中,我们使用一个文件构建了一个 SQLite 数据库。我们可以使用内存中SQLite 数据库特性使测试运行更快一些。使用内存中数据库的缺点是我们没有可以用来调试失败测试的数据库。

下面是我们在TestCase子类中如何使用它:

    from     sqlalchemy.orm     import     sessionmaker

    class     Test_Blog_Queries(unittest.TestCase):

        @staticmethod
                    def     setUpClass():
        engine = build_test_db()
        Test_Blog_Queries.Session = sessionmaker(    bind    =engine)
        session = Test_Blog_Queries.Session()

        tag_rr = Tag(    phrase    =    "#RedRanger"    )
        session.add(tag_rr)
        tag_w42 = Tag(    phrase    =    "#Whitby42"    )
        session.add(tag_w42)
        tag_icw = Tag(    phrase    =    "#ICW"    )
        session.add(tag_icw)
        tag_mis = Tag(    phrase    =    "#Mistakes"    )
        session.add(tag_mis)

        blog1 = Blog(    title    =    "Travel 2013"    )
        session.add(blog1)
        b1p1 = Post(
                date    =datetime.datetime(    2013    ,     11    ,     14    ,     17    ,     25    ),
                title    =    "Hard Aground"    ,
                rst_text    =    """Some embarrassing revelation. Including ☹ and         ⚓        """    ,
                blog    =blog1,
                tags    =[tag_rr, tag_w42, tag_icw],
        )
        session.add(b1p1)
        b1p2 = Post(
                date    =datetime.datetime(    2013    ,     11    ,     18    ,     15    ,     30    ),
                title    =    "Anchor Follies"    ,
                rst_text    =    """Some witty epigram. Including ☺ and         ☀        """    ,
                blog    =blog1,
                tags    =[tag_rr, tag_w42, tag_mis],
        )
        session.add(b1p2)

        blog2 = Blog(    title    =    "Travel 2014"    )
        session.add(blog2)
        session.commit()

我们定义了setUpClass()以便在运行此类中的测试之前创建一个数据库。这允许我们定义一些共享公共数据库配置的测试方法。一旦建立了数据库,我们就可以创建一个会话并添加数据。

我们已经将 session maker 对象作为类级属性Test_Blog_Queries.Session = sessionmaker(bind=engine)放入类中。这个类级对象可以用于setUp()和单独的测试方法。

以下是setUp()和两种单独的试验方法:

    def     setUp(    self    ):
        self    .session = Test_Blog_Queries.Session()

    def     test_query_eqTitle_should_return1Blog(    self    ):
            results =     self    .session.query(Blog).filter(Blog.title ==     "Travel 2013"    ).all()
        self    .assertEqual(    1    ,     len    (results))
        self    .assertEqual(    2    ,     len    (results[    0    ].entries))

    def     test_query_likeTitle_should_return2Blog(    self    ):
            results =     self    .session.query(Blog).filter(Blog.title.like(    "Travel %"    )).all()
        self    .assertEqual(    2    ,     len    (results))

setUp()方法从类级sessionmaker实例创建一个新的空会话对象。这将确保每个查询都能够正确地生成 SQL 并使用 SQLAlchemy 会话从数据库中获取数据。

query_eqTitle_should_return1Blog()测试将找到请求的Blog实例,并通过entries关系导航到Post实例。请求的filter()部分并没有真正测试我们的应用程序定义;它练习炼金术和 SQLite。最后一个断言中的results[0].entries测试是对我们的类定义的有意义的测试。

query_likeTitle_should_return2Blog()测试几乎完全是对 SQLAlchemy 和 SQLite 的测试。除了在Blog中存在一个名为title的属性之外,它在我们的应用程序中并没有真正意义上的使用。这些类型的测试通常是创建初始技术峰值时留下的。它们可以帮助澄清应用程序 API,即使它们不能作为测试用例提供太多的价值。

以下是另外两种测试方法:

    def     test_query_eqW42_tag_should_return2Post(    self    ):
    results =     self    .session.query(Post).join(assoc_post_tag).join(Tag).filter(
        Tag.phrase ==     "#Whitby42"
                ).all()
        self    .assertEqual(    2    ,     len    (results))

    def     test_query_eqICW_tag_should_return1Post(    self    ):
    results =     self    .session.query(Post).join(assoc_post_tag).join(Tag).filter(
        Tag.phrase ==     "#ICW"
                ).all()
                self    .assertEqual(    1    ,     len    (results))
        self    .assertEqual(    "Hard Aground"    , results[    0    ].title)
        self    .assertEqual(    "Travel 2013"    , results[    0    ].blog.title)
        self    .assertEqual(
            set    ([    "#RedRanger"    ,     "#Whitby42"    ,     "#ICW"    ]),
            set    (t.phrase     for     t     in     results[    0    ].tags),
    )

query_eqW42_tag_should_return2Post()测试执行更复杂的查询来定位具有给定标记的帖子。这将练习类中定义的许多关系。当找到两个相关的博客条目时,此测试已通过。

类似地,query_eqICW_tag_should_return1Post()测试执行一个复杂的查询。通过results[0].blog.title测试从Post到包含PostBlog实例的导航。它还通过set(t.phrase for t in results[0].tags)测试从PostTags相关集合的导航。我们必须使用显式的set(),因为 SQL 中的结果顺序不能保证。

关于TestCaseTest_Blog_Queries子类,重要的是它通过setUpClass()方法创建了一个数据库模式和一组特定的定义行。这种测试设置对数据库应用程序很有帮助。它可能变得相当复杂,通常通过从文件或 JSON 文档加载示例行来补充,而不是用 Python 编码行。

TestCase 类层次结构

继承在TestCase类中起作用。理想情况下,每个TestCase都是唯一的。从实用角度讲,案例之间可能有共同的特点。TestCase类有以下三种常见的重叠方式:

  • 常用setUp():我们可能有一些数据在多个TestCase中使用。没有理由重复这些数据。一个只定义了setUp()tearDown()而没有测试方法的TestCase类是合法的,但它可能会导致一个混乱的日志,因为其中涉及零个测试。
  • 通用tearDown():对涉及操作系统资源的测试进行通用清理是很常见的。我们可能需要删除文件和目录或终止子进程。
  • 常见结果检查:对于算法复杂的测试,我们可能会有一种结果检查方法来验证结果的某些属性。

回首于 Tyl T1 第 4 章,To2 T2,AuthT3 属性访问,属性和描述符 To.T4^,例如,考虑 AutoT0p 类。此类基于其他两个值在字典中填充缺少的值,如下所示:

    @dataclass
        class     RateTimeDistance:

    rate: Optional[    float    ] =     None
                time: Optional[    float    ] =     None
                distance: Optional[    float    ] =     None

            def     __post_init__(    self    ) ->     None    :
            if         self    .rate     is not None and         self    .time     is not None    :
                self    .distance =     self    .rate *     self    .time
            elif         self    .rate     is not None and         self    .distance     is not None    :
                self    .time =     self    .distance /     self    .rate
            elif         self    .time     is not None and         self    .distance     is not None    :
                self    .rate =     self    .distance /     self    .time

RateTimeDistance类的每个单元测试方法可包括以下代码:

    self    .assertAlmostEqual(
        self    .rtd.distance,     self    .rtd.rate *     self    .rtd.time,     places    =    2
    )

如果我们使用许多TestCase子类,我们可以将此有效性检查作为单独的方法继承,如下所示:

def validate(self, object):
    self.assertAlmostEqual(
        self.rtd.distance, self.rtd.rate * self.rtd.time, places=2
    )

这样,每个测试只需要包含self.validate(object),以确保所有测试都提供了一致的正确性定义。

unittest模块定义的一个重要特征是,测试用例是具有适当继承的适当类。我们可以设计TestCase类层次结构,就像我们应用于应用程序类一样,对细节非常关注。

使用外部定义的预期结果

对于某些应用程序,用户可以明确描述软件行为的处理规则。在其他情况下,分析师或设计师的工作会将用户的需求转化为软件的过程描述。

通常最容易提供预期结果的具体示例。最终用户或中介分析师可能会发现,创建一个显示样本输入和预期结果的电子表格很有帮助。使用用户提供的具体样本数据可以简化正在开发的软件。

只要有可能,让真实用户提供正确结果的具体示例。创建过程描述或软件规范非常困难。创建具体的示例,并将示例概括为软件规范,这样就不那么复杂和混乱了。此外,它还融入了一种开发风格,在这种风格中,测试用例驱动开发工作。一套测试用例为开发人员提供了done的具体定义。跟踪软件开发项目状态会询问我们现在有多少个测试用例,其中有多少通过了测试。

给出一个包含具体示例的电子表格,我们需要将每一行转换为一个TestCase实例。然后,我们可以从这些对象构建一个套件。

对于本章前面的示例,我们从基于TestCase的类加载测试用例。我们使用unittest.defaultTestLoader.loadTestsFromTestCase来定位名称以test开头的所有方法。加载程序从每个方法中创建一个带有正确名称前缀的测试对象,并将它们组合到一个测试套件中。

然而,还有另一种方法。对于这个例子,我们将分别构建测试用例实例。这是通过使用一个runTest()方法定义一个类来实现的。我们可以将这个类的多个实例加载到一个套件中。要使其工作,TestCase类必须只定义一个名为runTest()的测试。我们不会使用加载程序来创建测试对象;我们将直接从外部提供的数据行创建它们。

让我们来看看我们需要测试的具体功能。这来自第 4 章属性访问、属性和描述符

    from     Chapter_4.ch04_ex3     import     RateTimeDistance

这是一个在初始化时急切地计算许多属性的类。这个简单函数的用户向我们提供了一些作为电子表格的测试用例,我们从中提取了 CSV 文件。有关 CSV 文件的更多信息,请参见第 10 章序列化和保存-JSON、YAML、Pickle、CSV 和 XML。我们需要将每一行转换为一个TestCase。以下是 CSV 文件中的数据:

rate_in,time_in,distance_in,rate_out,time_out,distance_out 
2,3,,2,3,6 
5,,7,5,1.4,7 
,11,13,1.18,11,13 

我们不打算使用以test开头的名称定义一个类,因为该类不会被加载程序简单地发现。相反,该类用于将实例构建到一套更大的测试中。下面是我们可以用来从 CSV 文件的每一行创建测试实例的测试用例模板:

    class     Test_RTD(unittest.TestCase):

        def     runTest(    self    ) ->     None    :
            with     (Path.cwd() /     "data"     /     "ch17_data.csv"    ).open()     as     source:
            rdr = csv.DictReader(source)
                for     row     in     rdr:
                    self    .example(**row)

        def     example(
            self    ,
        rate_in:     str    ,
        time_in:     str    ,
        distance_in:     str    ,
        rate_out:     str    ,
        time_out:     str    ,
        distance_out:     str    ,
    ) ->     None    :
        args =     dict    (
                rate    =float_or_none(rate_in),
                time    =float_or_none(time_in),
                distance    =float_or_none(distance_in),
        )
        expected =     dict    (
                rate    =    float    (rate_out),     time    =    float    (time_out),     distance    =    float    (distance_out)
        )
        rtd = RateTimeDistance(**args)
            assert     rtd.distance     and     rtd.rate     and     rtd.time
            self    .assertAlmostEqual(rtd.distance, rtd.rate * rtd.time,     places    =    2    )
            self    .assertAlmostEqual(rtd.rate, expected[    "rate"    ],     places    =    2    )
            self    .assertAlmostEqual(rtd.time, expected[    "time"    ],     places    =    2    )
            self    .assertAlmostEqual(rtd.distance, expected[    "distance"    ],     places    =    2    )

测试体现在该类的runTest()方法中。在前面的示例中,我们使用以test_开头的方法名来提供测试用例行为。可以提供一个runTest()方法,而不是多个test_方法名称。这也将改变测试套件的构建方式,我们将在下面看到。

此方法将电子表格的一行解析为字典。要使其正常工作,示例数据列标题必须与example()方法所需的参数名称相匹配。输入值放在名为args的字典中;类似地,预期结果值被放入名为expected的字典中。

float_or_none()函数有助于处理 CSV 源数据,None值将由空字符串表示。它将单元格的文本转换为float值或None。该函数定义如下:

    def     float_or_none(text:     str    ) -> Optional[    float    ]:
        if         len    (text) ==     0    :
            return None
            return         float    (text)

电子表格的每一行都通过example()方法进行处理。这为我们提供了一种相对灵活的测试方法。我们可以允许用户或业务分析师创建澄清正确操作所需的所有示例。

我们可以从该测试对象构建一个套件,如下所示:

    def     suite9():
    suite = unittest.TestSuite()
    suite.addTest(Test_RTD())
        return     suite

注意,我们不使用loadTestsFromTestCase方法来发现具有test_名称的方法。相反,我们创建一个测试用例的实例,该实例可以简单地添加到测试套件中。

该套件是使用我们前面看到的那种脚本执行的。下面是一个例子:

if __name__ == "__main__":
    t = unittest.TextTestRunner() 
    t.run(suite9()) 

输出如下所示:

..F 
====================================================================== 
FAIL: runTest (__main__.Test_RTD) 
{'rate': None, 'distance': 13.0, 'time': 11.0} -> {'rate': 1.18, 'distance': 13.0, 'time': 11.0} 
---------------------------------------------------------------------- 
Traceback (most recent call last): 
  File "p3_c15.py", line 504, in runTest 
    self.assertAlmostEqual( self.rtd.rate, self.result['rate'] ) 
AssertionError: 1.1818181818181819 != 1.18 within 7 places 

---------------------------------------------------------------------- 
Ran 3 tests in 0.000s 

FAILED (failures=1) 

用户提供的数据有一个小问题。用户提供的值仅四舍五入到两位。要么样本数据需要提供更多的数字,要么我们的测试断言需要处理舍入。

这也可以使用unittest测试发现从命令行运行。我们可以运行以下命令来使用unittest模块的内置测试发现功能:

python3 -m unittest Chapter_17/ch17_ex1.py

这将生成一个简短的输出,其中包含本章中的许多测试示例。看起来是这样的:

.x............x..Run time 0.931446542
..
----------------------------------------------------------------------
Ran 19 tests in 0.939s

每个.都是通过的测试。x标记是预期会失败的测试。正如我们前面提到的,一些测试揭示了定义类的问题,这些测试将失败,直到类被修复。

Run time 0.931446542输出来自测试中的print()。这不是输出的标准部分。由于输出的结构方式,在测试用例中打印其他调试或性能数据输出可能会很困难。这个例子展示了在显示测试执行进度的简单周期的中间有额外的输出是如何混淆的。

使用 pytest 和 fixture

unittest转轮的替代品是pytest试验转轮。pytest框架有一个出色的测试发现功能,它超越了unittest工具所能发现的功能。

unittest转轮可通过以下两种方式使用。

  • 使用测试套件对象。前面的例子都集中在这一点上。
  • 要搜索属于unittest.TestCase扩展的类,请从这些类构建一个测试套件,然后运行该套件。这提供了相当大的灵活性。我们可以添加测试用例,而无需更新代码来构建测试套件。

pytest工具还可以定位unittest.TestCase类定义,构建一套测试,并执行测试。除此之外,它还可以在名称以test_开头的模块中定位名称以test_开头的函数。与更复杂的unittest.TestCase类定义相比,使用简单函数具有一些优势。

使用单独功能的主要优点是简化了测试模块。特别是,当我们回顾TestCardFactory示例时,我们发现类内的测试不需要setUp()。因为所有的方法都是独立的,所以不需要将这些方法绑定到单个类定义中。尽管这是一本关于面向对象 Python 的书,但如果类定义不能改进代码,就没有理由使用它们。在许多情况下,面向类的测试用例定义没有帮助,pytest执行的单独函数具有优势。

pytest方法会导致以下两种其他后果:

  • self.assert...()方法不可用。使用pytest时,Pythonassert语句用于比较预期结果和实际结果。
  • setUp()tearDown()使用的有状态类变量不可用。为了设置和分解测试上下文,我们将使用pytest @fixture函数。

作为可能的简化的具体示例,我们将回顾前面的一些示例,从Card类的测试开始。pytest版本如下:

    def     test_card():
    three_clubs = Card(    3    , Suit.CLUB)
        assert         "3♣"     ==     str    (three_clubs)
        assert         3     == three_clubs.rank
        assert Suit.CLUB             == three_clubs.suit
        assert         3     == three_clubs.hard
        assert         3     == three_clubs.soft

函数名必须以test_开头,以确保pytest可以发现它。测试设置创建了一个card实例和许多assert语句,以确认它具有预期的行为。在使用带有assert语句的函数时,我们没有使用unitest.TestCase子类时那么多的开销。

断言检查

为了确认测试引发的异常,unittest.TestCase类有类似assertRaises()的方法。在使用pytest时,我们有一种独特的方法来测试此功能。pytest包提供了一个名为raises的上下文管理器来帮助检测引发的异常。raises上下文如本例所示:

    from     pytest     import     raises

    def     test_card_factory():

    c1 = card(    1    , Suit.CLUB)
        assert         isinstance    (c1, AceCard)

        c2     = card(    2    , Suit.DIAMOND)
        assert         isinstance    (c1, Card)

    c10 = card(    10    , Suit.HEART)
        assert         isinstance    (c10, Card)

    cj = card(    11    , Suit.SPADE)
        assert         isinstance    (cj, FaceCard)

    ck = card(    13    , Suit.CLUB)
        assert         isinstance    (ck, FaceCard)

        with     raises(LogicError):
            c14     = card(    14    , Suit.DIAMOND)

        with     raises(LogicError):
            c0     = card(    0    , Suit.DIAMOND)

我们使用pytest.raises作为上下文管理器。当随类定义一起提供时,上下文中的语句将引发命名异常。如果引发异常,则测试通过;如果未引发异常,则这是测试失败。

使用夹具进行测试设置

pytest的测试设置和拆卸功能通常由@fixture功能处理。这些功能构成了一个装置连接到其中进行测试的夹具。在硬件测试领域,它也可以称为测试线束或测试台。

夹具可用于执行与测试相关的任何类型的设置或拆卸。由于pytest调用测试的方式,它隐式调用 fixture 函数,大大简化了测试代码。这些装置可以引用其他装置,使我们能够创建复合对象,从而有助于隔离正在测试的单元。

之前,我们研究了两个复杂的测试用例子类:TestDeckBuildTestDeckDeal。这两个测试用例涵盖了Deck3类定义的不同特性。我们可以使用公共夹具构建类似的测试用例。以下是设备定义:

    import     unittest.mock
    from     types     import     SimpleNamespace
    from     pytest     import     fixture

    @fixture
        def     deck_context():
    mock_deck = [
            getattr    (unittest.mock.sentinel,     str    (x))
            for     x     in         range    (    52    )
    ]
    mock_card = unittest.mock.Mock(    side_effect    =mock_deck)
    mock_rng = unittest.mock.Mock(
            wraps    =random.Random,
            shuffle    =unittest.mock.Mock(    return_value    =    None    )
    )
        return     SimpleNamespace(**    locals    ())

deck_context()函数创建以下三个模拟对象:

  • mock_deck对象是 52 个独立mock.sentinel对象的列表。每个sentinel对象通过获取sentinel的唯一属性进行定制。属性名称是根据range(52)中的整数值构建的字符串。将有名称为mock.sentinel.0的对象。对于源代码中的简单 Python 属性引用,这不是有效的语法,但我们只需要确保sentinel是唯一的。
  • mock_card对象是带有side_effect的模拟对象。这将表现为一个函数。每次调用时,它都会从提供给side_effect参数的列表中返回另一个值。这可用于模拟从文件或网络连接读取值的函数。
  • mock_rng对象是random.Random类的包装版本。除了两个特征外,它的行为类似于随机对象。首先,shuffle()方法没有任何作用。第二,Mock包装器将跟踪对该对象的方法的单个调用,以便我们可以确定被测试单元是否正确使用了它。

return步骤将所有局部变量打包到一个SimpleNamespace对象中。此对象允许我们使用诸如deck_context.mock_card之类的语法来引用Mock函数。我们可以在test函数中使用此夹具。以下是一个例子:

    def     test_deck_build(deck_context):
    d = Deck3(
            size    =    1    ,
            random    =deck_context.mock_rng,
            card_factory    =deck_context.mock_card
    )
    deck_context.mock_rng.shuffle.assert_called_once_with(d)
        assert         52     ==     len    (deck_context.mock_card.mock_calls)
    expected = [
        unittest.mock.call(r, s)     for     r     in         range    (    1    ,     14    )     for     s     in         iter    (Suit)
    ]
        assert     expected == deck_context.mock_card.mock_calls

本试验参考deck_context夹具。代码中没有做任何特殊的操作;pytest将隐式计算函数,生成的SimpleNamespace对象将是deck_context参数的值。参数和夹具之间的映射非常简单:所有参数名称必须是夹具函数的名称,并且这些函数会自动计算。

测试使用random参数和card_factory参数的模拟对象构建Deck3实例。一旦构建了Deck3实例,我们就可以检查 mock 对象,看看它们是否有适当数量的调用,以及预期的参数值。

使用夹具进行安装和拆卸

夹具功能还可以提供拆卸功能和设置功能。这依赖于生成器函数的惰性工作方式。进行设置和拆卸的夹具的一些示例代码如下:

    @fixture
        def     damaged_file_path():
    file_path = Path.cwd() /     "data"     /     "ch17_sample.csv"
                    with     file_path.open(    "w"    ,     newline    =    ""    )     as     target:
            print    (    "not_player,bet,rounds,final"    ,     file    =target)
            print    (    "data,1,1,1"    ,     file    =target)
        yield     file_path
    file_path.unlink()

damaged_file_path()函数创建一个相对路径为data/ch17_sample.csv的文件。将几行数据写入该文件。

yield语句提供初始结果值。这是由测试函数使用的。测试完成后,将从夹具中检索第二个值。当请求第二个结果时,fixture 函数可以执行所需的任何拆卸工作。在本例中,拆卸工作将删除已创建的测试文件。

测试运行时将隐式调用 fixture 函数。以下是使用此夹具的示例测试:

    def     test_damaged(    damaged_file_path    ):
        with     raises(    AssertionError    ):
            stats     = rounds_final(Path.cwd()/    "data"    /    "ch17_sample.csv"    )

此测试确认rounds_final()函数在给出示例文件时将引发AssertionError。因为damaged_file_pathfixture 使用yield,所以它可以删除测试上下文,删除文件。

建筑参数化装置

对于一个测试用例,通常会有大量类似的示例。在前面的部分中,我们讨论了让用户或分析师创建带有输入和输出示例的电子表格。这有助于允许直接输入软件开发过程。我们需要我们的测试工具来尽可能直接地处理 CSV 示例文件。

使用pytest,我们可以将参数应用于夹具。pytest运行程序将使用参数集合中的每个对象重复运行测试函数。要构建参数化夹具,我们可以使用如下示例中的代码:

    import     csv

    with     (Path.cwd() /     "Chapter_17"     /     "ch17_data.csv"    ).open()     as     source:
    rdr = csv.DictReader(source)
    rtd_cases =     list    (rdr)

    @fixture    (    params    =rtd_cases)
    def     rtd_example(request):
                yield     request.param

我们已经在上下文管理器中打开了 CSV 文件。该文件用于构建读取器,将每行数据转换为字典。键是列标题,值是给定行中每个单元格的字符串。最后一个rtd_cases变量是字典列表;一个类型提示List[Dict[str, str]]将捕获该结构。

rtd_example夹具是使用params=参数构建的。params集合中的每一项都将作为测试功能的上下文提供。这意味着测试将运行几次,每次都将使用与params序列不同的值。要使用此夹具,我们将有一个类似以下示例的测试用例:

    from pytest import approx

def     test_rtd(rtd_example):
    args =     dict    (
            rate    =float_or_none(rtd_example[    'rate_in'    ]),
            time    =float_or_none(rtd_example[    'time_in'    ]),
            distance    =float_or_none(rtd_example[    'distance_in'    ]),
    )
    result =     dict    (
            rate    =float_or_none(rtd_example[    'rate_out'    ]),
            time    =float_or_none(rtd_example[    'time_out'    ]),
            distance    =float_or_none(rtd_example[    'distance_out'    ]),
    )

                rtd = RateTimeDistance(**args)

        assert     rtd.distance == approx(rtd.rate * rtd.time)
        assert     rtd.rate == approx(result[    "rate"    ],     abs    =    1E-2    )
        assert     rtd.time == approx(result[    "time"    ])
        assert     rtd.distance == approx(result[    "distance"    ])

这种情况取决于rtd_example夹具。由于 fixture 有一个参数值列表,因此将多次调用此案例;每次,rtd_example的值都将与值序列不同。这使得为各种输入值编写通用测试变得非常方便

本测试还使用了pytest.approx对象。此对象用于包装浮点值,因此__eq__()方法是近似相等的算法,而不是简单的精确相等测试。这是一种非常方便的方法,可以忽略由于截断数字的二进制表示形式而产生的微小浮点差异。

自动化集成或性能测试

我们可以使用unittest包来执行测试,而不是专注于单个孤立的类定义。如前所述,我们可以使用unittest自动化测试多个组件集成的单元。这种测试只能在通过了独立组件单元测试的软件上执行。当组件的单元测试无法正常工作时,尝试调试失败的集成测试是没有意义的。

性能测试可以在多个集成级别上进行。对于大型应用程序,对整个构建进行性能测试可能不会完全有帮助。一种传统的观点是,程序 90%的时间只执行可用代码的 10%。因此,我们通常不需要优化整个应用程序;我们只需要找到代表真正性能瓶颈的一小部分程序。

在某些情况下,很明显我们有一个涉及搜索的数据结构。我们知道,删除搜索将大大提高性能。正如我们在第 6 章中所看到的,使用可调用对象和上下文,通过避免重新计算,实现记忆可以显著提高性能。

为了执行适当的性能测试,我们需要遵循以下三步工作循环:

  1. 结合使用设计审查和代码分析来定位应用程序中可能存在性能问题的部分。Python 在标准库中有两个分析模块。除非有更复杂的需求,cProfile将定位应用程序中需要焦点的部分。
  2. 使用unittest创建自动测试场景,以演示任何实际性能问题。使用timeittime.perf_counter()采集性能数据。
  3. 优化所选测试用例的代码,直到性能可接受为止。

关键是尽可能地自动化,避免为了提高性能而对事情进行模糊的调整。大多数情况下,必须替换中心数据结构或算法(或两者兼而有之),从而导致大规模重构。拥有自动化的单元测试使大规模重构变得切实可行。

当性能测试缺乏特定的通过-失败标准时,可能会出现尴尬的情况。有一种驱动力可以使软件更快,而无需具体定义足够快。当有可测量的绩效目标时,它总是更简单。给定一个具体的目标,然后可以使用正式的自动化测试来断言结果是正确的,并且获得这些结果所花费的时间是可以接受的。

对于性能测试,我们可以使用如下代码:

    import     unittest
    import     timeit

    class     Test_Performance(unittest.TestCase):

        def     test_simpleCalc_shouldbe_fastEnough(    self    ):
        t = timeit.timeit(
                stmt    =    """RateTimeDistance(rate=1, time=2)"""    ,
                setup    =    """from Chapter_4.ch04_ex3 import RateTimeDistance"""    ,
        )
            print    (    "Run time"    , t)
            self    .assertLess(t,     10    ,     f"run time         {    t    }         >= 10"    )

使用unittest可以创建自动性能测试。当timeit模块执行给定语句 1000000 次时,这将使执行测试的计算机上的后台工作产生的测量变化最小化。

在前面的示例中,RTD 构造函数的每次执行所需时间少于 1/100000 秒。100 万次执行应该不到 10 秒。

总结

我们研究了使用unittestdoctest来创建自动化单元测试。我们还研究了如何创建一个测试套件,以便在不依赖自动测试发现过程的情况下,将测试集合打包以供重用和聚合到具有更大范围的套件中。

我们研究了如何创建模拟对象,以便能够单独测试软件单元。我们还研究了各种设置和拆卸功能。这些允许我们编写具有复杂初始状态或持久结果的测试。

第一个单元测试属性与doctestunittest都非常吻合。第一个属性如下所示:

  • :除非我们编写了非常糟糕的测试,doctestunitest的性能应该非常快。
  • 隔离unittest包为我们提供了一个模拟模块,我们可以使用它隔离我们的类定义。此外,我们可以在设计中谨慎一些,以确保我们的组件彼此隔离。
  • 可重复:使用doctestunittest进行自动测试,确保重复性。
  • 自验证doctestunittest都将测试结果与测试用例条件绑定,确保测试不涉及主观判断。
  • 及时:一旦我们有了类、函数或模块的框架,我们就可以编写和运行测试用例。一个类的主体只有pass就足以运行测试脚本。

出于项目管理的目的,书面测试和通过测试的计数有时是非常有用的状态报告。

设计考虑和权衡

在创建软件时,测试用例必须是可交付的。没有自动测试的任何特性也可能不存在。如果没有测试,就不能相信特性是正确的。如果它不可信,就不应该使用它。

唯一真正的权衡问题是是否使用doctestunittest或两者兼用。对于简单的编程,doctest可能非常适合。对于更复杂的情况,unittest将是必要的。对于 API 文档需要包含示例的框架,组合可以很好地工作。

在某些情况下,只需创建一个充满TestCase类定义的模块就足够了。TestLoader类和测试发现功能可能完全足以定位所有测试。

更一般而言,unittest涉及使用TestLoader从每个TestCase子类中提取多个测试方法。我们将测试方法打包到一个单独的类中,基于他们可以与谁共享类级别setUp()setUpClass()的方法。

我们也可以创建没有TestLoaderTestCase实例。在这种情况下,runTest()的默认方法被定义为具有测试用例断言。我们可以从此类类的实例创建一个套件。

最困难的部分可能是设计可测试性。删除依赖项以便可以独立测试单元有时会感觉增加了软件设计的复杂性。在大多数情况下,暴露依赖关系所花费的时间就是在创建更具可维护性和灵活性的软件上投入的时间。

一般规则是:类之间的隐式依赖是糟糕的设计

可测试设计具有明确的依赖性;可以很容易地用模拟对象替换这些对象。pytest框架提供monkeypatch夹具。这允许我们编写测试,通过修补依赖项来隔离正在测试的单元。虽然这很方便,但提供简单、可见的依赖项注入通常更简单、更可靠。

期待

第 18 章处理命令行中,我们将研究如何编写从命令行启动的完整应用程序。我们将研究在 Python 应用程序中处理启动选项、环境变量和配置文件的方法。

第 19 章模块和封装设计中,我们将对应用程序设计进行扩展。我们将添加在更大的应用程序中组合应用程序的能力,以及将应用程序分解为更小的模块或包的能力。**