测试类

在本章的第一部分,您为单个函数编写了测试。 现在你将为一个类编写测试。 您将在自己的许多程序中使用类,因此能够证明您的类可以正常工作会很有帮助。 如果你已经通过了你正在处理的类的测试,你可以确信你对类所做的改进不会意外地破坏它的当前行为。

各种断言

到目前为止,您只看到了一种断言:断言字符串具有特定值。 编写测试时,您可以提出任何可以表示为条件语句的声明。 如果条件如预期的那样为 True,那么您对程序该部分行为方式的假设将得到确认; 您可以确信不存在任何错误。 如果您假设为 True 的条件实际上为 False,则测试将失败并且您将知道有问题需要解决。 表 11-1 显示了您可以在初始测试中包含的一些最有用的断言类型。

Table 1. Commonly Used Assertion Statements in Tests
断言 详解

assert a == b

断言两个值相等

assert a != b

断言两个值不相等

assert a

断言 a 的计算结果为 True

assert not a

断言 a 的计算结果为 False

assert element in list

断言元素位于列表中

assert element not in list

断言元素不位于列表中

这些只是几个例子; 任何可以表示为条件语句的内容都可以包含在测试中。

一个要测试的类

测试类类似于测试函数,因为大部分工作涉及测试类中方法的行为。 然而,有一些不同,所以让我们写一个类来测试。 考虑一个帮助管理匿名调查的类:

class AnonymousSurvey:
    """Collect anonymous answers to a survey question."""

    def __init__(self, question): (1)
        """Store a question, and prepare to store responses."""
        self.question = question
        self.responses = []

    def show_question(self): (2)
        """Show the survey question."""
        print(self.question)

    def store_response(self, new_response): (3)
        """Store a single response to the survey."""
        self.responses.append(new_response)
        
    def show_results(self): (4)
        """Show all the responses that have been given."""
        print("Survey results:")
        for response in self.responses:
            print(f"- {response}")

该课程以您提供的调查问题开始 ❶ 并包含一个空列表来存储响应。 该类具有打印调查问题 ❷ 的方法,将新的响应添加到响应列表 ❸,并打印存储在列表中的所有响应 ❹。 要从此类创建实例,您只需提供一个问题。 一旦您有一个代表特定调查的实例,您就可以使用 show_question() 显示调查问题,使用 store_response() 存储响应,并使用 show_results() 显示结果。

为了证明 AnonymousSurvey 类有效,让我们编写一个使用该类的程序:

from survey import AnonymousSurvey


# Define a question, and make a survey.
question = "What language did you first learn to speak?"
language_survey = AnonymousSurvey(question)

# Show the question, and store responses to the question.
language_survey.show_question()
print("Enter 'q' at any time to quit.\n")
while True:
    response = input("Language: ")
    if response == 'q':
        break
    language_survey.store_response(response)

# Show the survey results.
print("\nThank you to everyone who participated in the survey!")
language_survey.show_results()

该程序定义了一个问题("What language did you first learn to speak?")并使用该问题创建了一个 AnonymousSurvey 对象。 该程序调用 show_question() 来显示问题,然后提示回答。 每个响应都在收到时存储。 输入所有响应后(用户输入 q 退出),show_results() 打印调查结果:

What language did you first learn to speak?
Enter 'q' at any time to quit.

Language: English
Language: Spanish
Language: English
Language: Mandarin
Language: q

Thank you to everyone who participated in the survey!
Survey results:
- English
- Spanish
- English
- Mandarin

此类适用于简单的匿名调查,但假设我们要改进 AnonymousSurvey 及其所在的模块调查。 我们可以允许每个用户输入多个响应,我们可以编写一个方法来仅列出唯一的响应并报告每个响应给出的次数,或者我们甚至可以编写另一个类来管理非匿名调查。

实施此类更改可能会影响 AnonymousSurvey 类的当前行为。 例如,在尝试允许每个用户输入多个响应时,我们可能会不小心更改处理单个响应的方式。 为确保我们在开发此模块时不会破坏现有行为,我们可以为该类编写测试。

测试AnonymousSurvey类

让我们编写一个测试来验证 AnonymousSurvey 行为方式的一个方面。 我们将编写一个测试来验证对调查问题的单个回答是否正确存储:

test_survey.py
from survey import AnonymousSurvey

def test_store_single_response(): (1)
    """Test that a single response is stored properly."""
    question = "What language did you first learn to speak?"
    language_survey = AnonymousSurvey(question) (2)
    language_survey.store_response('English')
    assert 'English' in language_survey.responses (3)

我们首先导入要测试的类 AnonymousSurvey。 第一个测试函数将验证当我们存储对调查问题的回复时,该回复将最终出现在调查的回复列表中。 这个函数的一个很好的描述性名称是 test_store_single_response() ❶。 如果此测试失败,我们将从测试摘要中的函数名称得知存储对调查的单个响应存在问题。

为了测试一个类的行为,我们需要创建一个类的实例。 我们创建一个名为 language_survey 的实例,问题是 "What language did you first learn to speak?" 我们使用 store_response() 方法存储一个单一的回复,English 。 然后我们通过断言英语在列表 language_survey.responses ❸中来验证响应是否正确存储。

默认情况下,运行不带参数的命令 pytest 将运行 pytest 在当前目录中发现的所有测试。 要专注于一个文件中的测试,请传递要运行的测试文件的名称。 在这里,我们将只运行我们为 AnonymousSurvey 编写的一个测试:

$ pytest test_survey.py
=================== test session starts =========================
--snip--
test_survey.py .
[100%]
==================== 1 passed in 0.01s ==========================

这是一个良好的开端,但调查只有在产生多个响应时才有用。 让我们验证三个响应是否可以正确存储。 为此,我们向 TestAnonymousSurvey 添加另一个方法:

from survey import AnonymousSurvey

def test_store_single_response():
--snip--

def test_store_three_responses():
    """Test that three individual responses are stored proper
    ly."""
    question = "What language did you first learn to speak?"
    language_survey = AnonymousSurvey(question)
    responses = ['English', 'Spanish', 'Mandarin'] (1)
    for response in responses:
        language_survey.store_response(response)

    for response in responses: (2)
        assert response in language_survey.responses

我们将新函数称为 test_store_three_responses()。 我们创建一个调查对象,就像我们在 test_store_single_response() 中所做的那样。 我们定义了一个包含三个不同响应的列表 ❶,然后我们为每个响应调用 store_response()。 存储响应后,我们编写另一个循环并断言每个响应现在都在 language_survey.responses ❷ 中。

当我们再次运行测试文件时,两个测试(单个响应和三个响应)都通过了:

$ pytest test_survey.py
================== test session starts =========================
--snip--
test_survey.py ..
[100%]
=================== 2 passed in 0.01s ==========================

这非常有效。 然而,这些测试有点重复,所以我们将使用 pytest 的另一个功能来提高它们的效率。

使用夹具

在 test_survey.py 中,我们在每个测试函数中创建了一个新的 AnonymousSurvey 实例。 这在我们正在处理的简短示例中很好,但在具有数十或数百个测试的真实项目中,这将是有问题的。

在测试中,fixture 有助于建立测试环境。 通常,这意味着创建一个供多个测试使用的资源。 我们通过使用装饰器 @pytest.fixture 编写一个函数来在 pytest 中创建一个夹具。 装饰器是放置在函数定义之前的指令; Python 在运行之前将此指令应用于函数,以改变函数代码的行为方式。 如果这听起来很复杂,请不要担心; 在学习自己编写装饰器之前,您可以开始使用第三方包中的装饰器。

让我们使用一个夹具来创建一个可以在 test_survey.py 中的两个测试函数中使用的调查实例:

import pytest
from survey import AnonymousSurvey


@pytest.fixture (1)
def language_survey(): (2)
    """A survey that will be available to all test functions."""
    question = "What language did you first learn to speak?"
    language_survey = AnonymousSurvey(question)
    return language_survey

def test_store_single_response(language_survey): (3)
    """Test that a single response is stored properly."""
    language_survey.store_response('English') (4)
    assert 'English' in language_survey.responses

def test_store_three_responses(language_survey): (5)
    """Test that three individual responses are stored properly."""
    responses = ['English', 'Spanish', 'Mandarin']
    for response in responses:
        language_survey.store_response(response) (6)

    for response in responses:
        assert response in language_survey.responses

我们现在需要导入 pytest,因为我们正在使用 pytest 中定义的装饰器。 我们将 @pytest.fixture 装饰器 ❶ 应用于新函数 language_survey() ❷。 此函数构建一个 AnonymousSurvey 对象并返回新调查。

请注意,两个测试函数的定义都已更改 ❸ ❺; 每个测试函数现在都有一个名为 language_survey 的参数。 当测试函数中的参数与带有 @pytest.fixture 装饰器的函数名称匹配时,fixture 将自动运行并将返回值传递给测试函数。 在此示例中,函数 language_survey() 为 test_store_single_response() 和 test_store_three_responses() 提供了一个 language_survey 实例。

两个测试函数中都没有新代码,但请注意,每个函数中都删除了两行 ❹ ❻:定义问题的行和创建 AnonymousSurvey 对象的行。

当我们再次运行测试文件时,两个测试仍然通过。 在尝试扩展 AnonymousSurvey 以处理每个人的多个响应时,这些测试将特别有用。 修改代码以接受多个响应后,您可以运行这些测试并确保您没有影响存储单个响应或一系列单独响应的能力。

上面的结构几乎肯定会看起来很复杂; 它包含一些您迄今为止看到的最抽象的代码。 您不需要立即使用固定装置; 编写包含大量重复代码的测试总比根本不编写测试要好。 只要知道,当您编写了足够多的测试,重复就会妨碍您时,就会有一种行之有效的方法来处理重复。 此外,像这样的简单示例中的固定装置并不能真正使代码更短或更易于遵循。 但是在有很多测试的项目中,或者在需要很多行来构建用于多个测试的资源的情况下,fixtures 可以极大地改进您的测试代码。

当你想写一个夹具时,写一个函数来生成被多个测试函数使用的资源。 将 @pytest.fixture 装饰器添加到新函数,并将此函数的名称添加为使用此资源的每个测试函数的参数。 从那时起,您的测试将更短且更易于编写和维护。

自己试试
11-3.Employee

编写一个名为Employee的类。 __init__() 方法应该接收名字、姓氏和年薪,并将它们分别存储为属性。 编写一个名为 give_raise() 的方法,默认情况下将年薪增加 5,000 美元,但也接受不同的加薪金额。

使用两个测试函数 test_give_default_raise() 和 test_give_custom_raise() 为 Employee 编写一个测试文件。 在不使用夹具的情况下编写一次测试,并确保它们都通过。 然后编写一个夹具,这样你就不必在每个测试函数中创建一个新的员工实例。 再次运行测试,并确保两个测试仍然通过。

总结

在本章中,您学习了使用 pytest 模块中的工具为函数和类编写测试。 您学习了编写测试函数来验证您的函数和类应表现出的特定行为。 您了解了如何使用固定装置高效地创建可在测试文件中的多个测试函数中使用的资源。

测试是许多新程序员没有接触过的重要主题。 作为新程序员,您不必为尝试的所有简单项目编写测试。 但是,一旦您开始从事涉及大量开发工作的项目,就应该测试函数和类的关键行为。 您会更加确信项目中的新工作不会破坏正常工作的部分,这将使您可以自由地改进代码。 如果您不小心破坏了现有功能,您会立即知道,因此您仍然可以轻松解决问题。 响应您运行的失败测试比响应不满意用户的错误报告要容易得多。

如果您包含一些初始测试,其他程序员会更加尊重您的项目。 他们会更愿意尝试您的代码,并且更愿意与您合作开展项目。 如果你想为其他程序员正在从事的项目做出贡献,你将被期望证明你的代码通过了现有测试,并且通常你将被期望为你引入项目的任何新行为编写测试。

尝试测试以熟悉测试代码的过程。 为你的函数和类的最关键行为编写测试,但不要以在早期项目中完全覆盖为目标,除非你有特定的理由这样做。