pytest8.x版本 中文使用文档-------4-1.如何使用夹具(Fixtures)

目录

“Requesting” fixtures-“请求”夹具

简单的例子

fixture可以请求(或依赖)其他fixture

夹具(Fixtures)是可重用的

一个测试/夹具可以同时请求多个夹具

在同一个测试中,夹具可以被请求多次(返回值会被缓存)

自动使用夹具(无需请求即可使用的夹具)

作用域(Scope):跨类、模块、包或会话共享夹具

夹具作用域

动态作用域

拆解/清理(也称为Fixture终结)

使用yield的Fixture(推荐)

直接添加终结器


“Requesting” fixtures-“请求”夹具

在基本层面上,测试函数通过将它们声明为参数来请求所需的夹具。

当pytest运行一个测试时,它会查看该测试函数签名中的参数,然后搜索与这些参数同名的夹具。pytest找到这些夹具后,会运行它们,捕获它们返回的内容(如果有的话),并将这些对象作为参数传递给测试函数。

简单的例子

import pytest


class Fruit:
    def __init__(self, name):
        self.name = name
        self.cubed = False

    def cube(self):
        self.cubed = True


class FruitSalad:
    def __init__(self, *fruit_bowl):
        self.fruit = fruit_bowl
        self._cube_fruit()

    def _cube_fruit(self):
        for fruit in self.fruit:
            fruit.cube()


# Arrange
@pytest.fixture
def fruit_bowl():
    return [Fruit("apple"), Fruit("banana")]


def test_fruit_salad(fruit_bowl):
    # Act
    fruit_salad = FruitSalad(*fruit_bowl)

    # Assert
    assert all(fruit.cubed for fruit in fruit_salad.fruit)

在这个例子中,首先,我们定义了两个类:FruitFruitSalad

  • Fruit 类表示一种水果,它有一个 name 属性来存储水果的名称,以及一个 cubed 属性来标记水果是否已被“切块”(在这个上下文中,我们将其视为一个布尔值,但实际上它可能表示水果的某种状态变化)。Fruit 类还有一个 cube 方法,用于将 cubed 属性设置为 True

  • FruitSalad 类表示一份水果沙拉,它接受任意数量的 Fruit 对象作为输入(通过 *fruit_bowl 参数),并在初始化时调用 _cube_fruit 方法来处理这些水果。_cube_fruit 方法遍历所有的水果,并对每个水果调用 cube 方法。

然后,我们定义了一个 pytest 夹具 fruit_bowl。这个夹具函数创建了一个包含两个 Fruit 对象(苹果和香蕉)的列表,并返回这个列表。

最后,我们编写了一个测试函数 test_fruit_salad,它请求了 fruit_bowl 夹具。在测试函数内部,我们使用从 fruit_bowl 夹具返回的水果列表来创建一个 FruitSalad 对象。然后,我们使用一个断言来检查 FruitSalad 对象中的所有水果是否都被“切块”了(即它们的 cubed 属性是否都为 True)。

当 pytest 运行这个测试时,它会自动识别 test_fruit_salad 函数中请求的 fruit_bowl 夹具,并执行该夹具函数来获取所需的水果列表。然后,pytest 将这个列表作为参数传递给 test_fruit_salad 函数,使得测试能够使用这些水果来验证 FruitSalad 类的行为。

如果我们打算手动执行这个测试,大致上会发生以下事情:

def fruit_bowl():
    return [Fruit("apple"), Fruit("banana")]


def test_fruit_salad(fruit_bowl):
    # Act
    fruit_salad = FruitSalad(*fruit_bowl)

    # Assert
    assert all(fruit.cubed for fruit in fruit_salad.fruit)


# Arrange
bowl = fruit_bowl()
test_fruit_salad(fruit_bowl=bowl)

fixture可以请求(或依赖)其他fixture

pytest 最大的优点之一是其极其灵活的夹具(fixture)系统。该系统允许我们将复杂的测试需求简化为更简单、更有组织的函数,我们只需要让每个函数描述它所依赖的事物。稍后我们将更深入地探讨这一点,但现在,让我们通过一个快速示例来演示夹具如何使用其他夹具:

# contents of test_append.py
import pytest


# Arrange
@pytest.fixture
def first_entry():
    return "a"


# Arrange
@pytest.fixture
def order(first_entry):
    return [first_entry]


def test_string(order):
    # Act
    order.append("b")

    # Assert
    assert order == ["a", "b"]

请注意,这个例子与上面的例子相同,但几乎没有改变。在pytest中,夹具(fixtures)请求其他夹具的方式与测试请求夹具的方式相同。适用于测试的请求规则同样适用于夹具。如果我们手动执行这个示例,它将按以下方式工作:

def first_entry():
    return "a"


def order(first_entry):
    return [first_entry]


def test_string(order):
    # Act
    order.append("b")

    # Assert
    assert order == ["a", "b"]


entry = first_entry()
the_list = order(first_entry=entry)
test_string(order=the_list)

夹具(Fixtures)是可重用的

pytest 的夹具系统之所以如此强大,原因之一在于它允许我们定义一个通用的设置步骤,该步骤可以像使用普通函数一样被反复重用。两个不同的测试可以请求相同的夹具,并让pytest从该fixture为每个测试提供各自的结果。

这对于确保测试之间互不影响非常有用。我们可以使用这个系统来确保每个测试都获得其自己的一组新数据,并从干净的状态开始,从而提供一致且可重复的结果。

这里有一个例子可以说明这是如何派上用场的:

# contents of test_append.py
import pytest


# Arrange
@pytest.fixture
def first_entry():
    return "a"


# Arrange
@pytest.fixture
def order(first_entry):
    return [first_entry]


def test_string(order):
    # Act
    order.append("b")

    # Assert
    assert order == ["a", "b"]


def test_int(order):
    # Act
    order.append(2)

    # Assert
    assert order == ["a", 2]

在这里,每个测试都被赋予了该列表对象的一个自己的副本,这意味着顺序夹具(order fixture)被执行了两次(对于第一个条目夹具(first_entry fixture)也是如此)。如果我们手动来做这件事,它看起来可能会像这样:

def first_entry():
    return "a"


def order(first_entry):
    return [first_entry]


def test_string(order):
    # Act
    order.append("b")

    # Assert
    assert order == ["a", "b"]


def test_int(order):
    # Act
    order.append(2)

    # Assert
    assert order == ["a", 2]


entry = first_entry()
the_list = order(first_entry=entry)
test_string(order=the_list)

entry = first_entry()
the_list = order(first_entry=entry)
test_int(order=the_list)

一个测试/夹具可以同时请求多个夹具

测试和夹具并不限于一次只请求一个夹具。它们可以请求任意数量的夹具。这些夹具可以是相互独立的,也可以是存在依赖关系的。pytest的夹具系统会负责解析这些依赖关系,并按照正确的顺序来设置和清理这些夹具。

下面是另一个快速演示的例子:

# contents of test_append.py
import pytest


# Arrange
@pytest.fixture
def first_entry():
    return "a"


# Arrange
@pytest.fixture
def second_entry():
    return 2


# Arrange
@pytest.fixture
def order(first_entry, second_entry):
    return [first_entry, second_entry]


# Arrange
@pytest.fixture
def expected_list():
    return ["a", 2, 3.0]


def test_string(order, expected_list):
    # Act
    order.append(3.0)

    # Assert
    assert order == expected_list

在同一个测试中,夹具可以被请求多次(返回值会被缓存)

在pytest中,同一个测试过程中也可以多次请求同一个夹具,但pytest不会为那个测试再次执行它。这意味着我们可以在多个依赖于它的夹具中请求同一个夹具(甚至在测试本身中再次请求),而这些夹具不会被执行多次。

pytest通过缓存夹具的返回值来实现这一点。当夹具第一次被请求时,pytest会执行它并缓存其返回值。随后的请求将直接返回缓存的值,而不是重新执行夹具。

这种机制对于需要执行代价高昂的初始化操作的夹具特别有用,因为它可以确保这些操作只执行一次,同时仍然允许在测试的不同部分中使用这些夹具的返回值。

需要注意的是,虽然夹具的返回值被缓存了,但如果夹具本身具有副作用(比如修改全局状态),那么这些副作用仍然会在每次请求夹具时发生。因此,在设计夹具时,需要特别注意避免引入不必要的副作用。

# contents of test_append.py
import pytest


# Arrange
@pytest.fixture
def first_entry():
    return "a"


# Arrange
@pytest.fixture
def order():
    return []


# Act
@pytest.fixture
def append_first(order, first_entry):
    return order.append(first_entry)


def test_string_only(append_first, order, first_entry):
    # Assert
    assert order == [first_entry]

如果每次在测试过程中请求一个夹具时都执行它一次,那么这个测试将会失败,因为 append_firsttest_string_only 都会将 order 视为一个空列表(即 [])。但是,由于 order 的返回值(以及执行它可能产生的任何副作用)在第一次被调用后被缓存了,所以测试和 append_first 实际上都在引用同一个对象。因此,测试能够看到 append_first 对该对象所产生的影响。

具体来说,在这个例子中,append_first 夹具修改了 order 列表,将其变为包含 "a" 的列表。由于 order 列表的引用(而不是其内容的副本)被缓存了,所以当 test_string_only 访问 order 时,它看到的是已经被 append_first 修改过的列表,而不是原始的空列表。这就是为什么断言 assert order == [first_entry] 能够成功的原因。

自动使用夹具(无需请求即可使用的夹具)

有时,您可能希望有一个或多个夹具,您知道所有测试都将依赖于它们。“自动使用”夹具是一种让所有测试自动请求它们的便捷方式。这可以省去许多冗余的请求,甚至还可以提供更高级的夹具使用方法(稍后会详细介绍)。

我们可以通过将autouse=True传递给夹具的装饰器来使夹具成为自动使用夹具。以下是一个简单的示例,展示了如何使用它们:

# contents of test_append.py
import pytest


@pytest.fixture
def first_entry():
    return "a"


@pytest.fixture
def order(first_entry):
    return []


@pytest.fixture(autouse=True)
def append_first(order, first_entry):
    return order.append(first_entry)


def test_string_only(order, first_entry):
    assert order == [first_entry]


def test_string_and_int(order, first_entry):
    order.append(2)
    assert order == [first_entry, 2]

在这个示例中,append_first夹具是一个自动使用夹具。因为它是自动发生的,所以两个测试都受到了它的影响,尽管这两个测试都没有请求它。但这并不意味着它们不能请求它;只是这样做不是必要的。

作用域(Scope):跨类、模块、包或会话共享夹具

需要网络访问的夹具依赖于网络连接,并且通常创建起来比较耗时。以前面的示例为基础进行扩展,我们可以在@pytest.fixture调用中添加scope="module"参数,这样smtp_connection夹具函数(负责创建一个到已存在的SMTP服务器的连接)就只会在每个测试模块中被调用一次(默认是每个测试函数被调用一次)。因此,在同一个测试模块中的多个测试函数将接收相同的smtp_connection夹具实例,从而节省时间。scope的可能值包括:functionclassmodulepackagesession

下面的示例将fixture函数放入一个单独的conftest.py文件中,以便该目录中来自多个测试模块的测试可以访问fixture函数:

# content of conftest.py
import smtplib

import pytest


@pytest.fixture(scope="module")
def smtp_connection():
    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
# content of test_module.py


def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250
    assert b"smtp.gmail.com" in msg
    assert 0  # for demo purposes


def test_noop(smtp_connection):
    response, msg = smtp_connection.noop()
    assert response == 250
    assert 0  # for demo purposes

在这里,test_ehlo 需要 smtp_connection 夹具的值。pytest 会发现并调用标记有 @pytest.fixturesmtp_connection 夹具函数。运行测试看起来像这样:

$ pytest test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items

test_module.py FF                                                    [100%]

================================= FAILURES =================================
________________________________ test_ehlo _________________________________

smtp_connection = <smtplib.SMTP object at 0xdeadbeef0001>

    def test_ehlo(smtp_connection):
        response, msg = smtp_connection.ehlo()
        assert response == 250
        assert b"smtp.gmail.com" in msg
>       assert 0  # for demo purposes
E       assert 0

test_module.py:7: AssertionError
________________________________ test_noop _________________________________

smtp_connection = <smtplib.SMTP object at 0xdeadbeef0001>

    def test_noop(smtp_connection):
        response, msg = smtp_connection.noop()
        assert response == 250
>       assert 0  # for demo purposes
E       assert 0

test_module.py:13: AssertionError
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo - assert 0
FAILED test_module.py::test_noop - assert 0
============================ 2 failed in 0.12s =============================

你看到两个测试函数中的 assert 0 失败了,但更重要的是,你还可以看到完全相同的 smtp_connection 对象被传递给了这两个测试函数,因为 pytest 在回溯中显示了传入的参数值。因此,使用 smtp_connection 的两个测试函数运行得就像单个测试函数一样快,因为它们重用了相同的实例。

如果你决定你更希望有一个会话级作用域的 smtp_connection 实例,你可以简单地声明它:

@pytest.fixture(scope="session")  
def smtp_connection():  
    # 返回的夹具值将被所有请求它的测试共享  
    ...

夹具作用域

夹具是在首次被测试请求时创建的,并且根据它们的作用域被销毁:

  • function:默认作用域,夹具在测试结束时被销毁。

  • class:夹具在类中最后一个测试的清理阶段被销毁。

  • module:夹具在模块中最后一个测试的清理阶段被销毁。

  • package(注意:pytest本身不直接支持包级作用域,但这里的描述可能是在假设或模拟该行为):如果假设支持包级作用域,则夹具在定义它的包(包括其子包和子目录)中最后一个测试的清理阶段被销毁。然而,重要的是要注意,这不是pytest官方直接支持的功能,可能需要通过其他方式(如使用pytest_configurepytest_unconfigure钩子)来模拟。

  • session:夹具在测试会话结束时被销毁。

注意

pytest 一次只缓存一个夹具的实例,这意味着当使用参数化夹具时,pytest 可能会在给定作用域内多次调用该夹具。

动态作用域

从版本5.2开始增加。

在某些情况下,您可能想要在不更改代码的情况下更改夹具的作用域。为了做到这一点,您可以将一个可调用的对象传递给作用域(scope)参数。这个可调用的对象必须返回一个包含有效作用域名称的字符串,并且它只会在夹具定义期间执行一次。它将被调用时带有两个关键字参数:fixture_name(一个字符串,表示夹具的名称)和config(一个配置对象)。

您可以编写一个可调用对象,它根据传递给pytest的命令行参数或环境变量来决定夹具的作用域。这样,您就可以在不修改夹具代码的情况下,通过简单地更改命令行参数或环境变量来调整夹具的作用域,从而适应不同的测试环境或需求。

这在处理需要时间来设置的夹具时特别有用,比如启动一个Docker容器。您可以使用命令行参数来控制不同环境中启动的容器的作用域。请看下面的例子:

def determine_scope(fixture_name, config):
    if config.getoption("--keep-containers", None):
        return "session"
    return "function"


@pytest.fixture(scope=determine_scope)
def docker_container():
    yield spawn_container()
  1. determine_scope 函数
    • 这个函数接收两个参数:fixture_nameconfig。尽管在这个具体的实现中 fixture_name 没有被使用,但在更复杂的情况下,你可以根据fixture的名称来做出不同的决策。
    • config 是Pytest的配置对象,它允许你访问命令行选项、插件配置等信息。
    • 函数内部,它检查是否通过命令行选项 --keep-containers 传递了值(在这个例子中,不关心值的具体内容,只关心是否传递了此选项)。如果传递了 --keep-containers 选项,则函数返回 "session",意味着 docker_container fixture 将具有会话作用域(即在整个测试会话期间只创建和销毁一次容器)。
    • 如果没有传递 --keep-containers 选项,则函数返回 "function",表示 docker_container fixture 将具有函数作用域(即在每个测试函数之前创建容器,在每个测试函数之后销毁容器)。
  2. docker_container fixture
    • 使用 @pytest.fixture 装饰器来定义fixture。
    • scope=determine_scope:这里将fixture的作用域设置为 determine_scope 函数的返回值。这意味着 docker_container fixture 的作用域将基于测试运行时的配置动态确定。
    • 在fixture体内,yield spawn_container() 被使用。spawn_container() 应该是一个函数,负责启动并返回一个Docker容器实例(尽管这个函数没有在代码片段中定义)。使用 yield 语句使得fixture成为一个生成器,允许在测试前执行 spawn_container() 来启动容器,并在测试后自动执行 yield 之后的清理代码(尽管在这个例子中,并没有直接显示清理代码)。

通过这种方式,你可以灵活地控制Docker容器的创建和销毁行为,以适应不同的测试需求。例如,在开发过程中,你可能希望每个测试函数都使用一个新的容器来避免潜在的副作用,而在CI/CD流程中,为了节省时间和资源,你可能希望整个测试会话只使用一个容器。

拆解/清理(也称为Fixture终结)

当我们运行测试时,我们希望确保它们能够自我清理,以免干扰其他测试(同时也避免在系统中留下大量测试数据导致系统膨胀)。pytest中的Fixture提供了一个非常有用的拆解系统,允许我们为每个Fixture定义必要的具体步骤,以便在其自身之后进行清理。

这个系统可以通过两种方式加以利用。

  1. 使用yield的Fixture(推荐)

使用yield的Fixture通过yield而不是return来返回。这些Fixture可以运行一些代码,并将一个对象传回给请求它的Fixture/测试,就像其他Fixture一样。唯一的不同之处在于:

  • 使用yield代替了return
  • 该Fixture的任何拆解代码都放在yield之后。

一旦pytest确定了Fixture的线性顺序,它就会依次运行每个Fixture,直到它returnyield,然后移动到列表中的下一个Fixture进行相同的操作。

测试完成后,pytest会按照相反的顺序回溯Fixture列表,对于每个使用了yield的Fixture,它会运行yield语句之后的代码。

作为一个简单的例子,考虑这个基本的电子邮件模块:

# content of emaillib.py
class MailAdminClient:
    def create_user(self):
        return MailUser()

    def delete_user(self, user):
        # do some cleanup
        pass


class MailUser:
    def __init__(self):
        self.inbox = []

    def send_email(self, email, other):
        other.inbox.append(email)

    def clear_mailbox(self):
        self.inbox.clear()


class Email:
    def __init__(self, subject, body):
        self.subject = subject
        self.body = body

假设我们想测试从一个用户向另一个用户发送邮件的功能。我们首先需要创建两个用户,然后从一个用户向另一个用户发送邮件,最后断言另一个用户在其收件箱中收到了该邮件。如果我们想在测试运行后进行清理,我们可能需要确保在删除用户之前清空该用户的邮箱,否则系统可能会报错。

这可能是这样的:

# content of test_emaillib.py
from emaillib import Email, MailAdminClient  
  
import pytest  
  
  
@pytest.fixture  
def mail_admin():  
    return MailAdminClient()  
  
  
@pytest.fixture  
def sending_user(mail_admin):  
    user = mail_admin.create_user()  
    yield user  # 使用yield在这里可以让我们在测试完成后执行清理代码  
    mail_admin.delete_user(user)  # 测试完成后清理用户  
  
  
@pytest.fixture  
def receiving_user(mail_admin):  
    user = mail_admin.create_user()  
    yield user  # 同样使用yield来支持测试后的清理  
    user.clear_mailbox()  # 先清空邮箱  
    mail_admin.delete_user(user)  # 然后删除用户  
  
  
def test_email_received(sending_user, receiving_user):  
    email = Email(subject="Hey!", body="How's it going?")  
    sending_user.send_email(email, receiving_user)  
    assert email in receiving_user.inbox  # 断言邮件已发送到接收者的收件箱  
  

因为receiving_user是在设置过程中最后运行的fixture,所以在拆解过程中它会首先运行。需要注意的是,即使拆解的顺序是正确的,也不能保证清理过程是绝对安全的。关于安全拆解的更多细节将在后文介绍。

处理带有 yield 的 fixture 的错误

如果在使用 yield 的 fixture 中,在 yield 语句之前抛出了异常,pytest 将不会尝试运行该 yield fixture 的 yield 语句之后的拆解(teardown)代码。但是,对于该测试已经成功运行的每一个其他 fixture,pytest 仍然会尝试像平常一样拆解它们。

这意味着,如果某个 yield fixture 在其 yield 语句之前失败(比如,在创建资源时出错),那么该 fixture 的拆解代码将不会被执行,但 pytest 会尽力拆解那些已经成功运行的 fixture,以确保测试环境的清理。然而,这也可能导致一些资源(如数据库连接、文件句柄等)在测试失败后被遗漏,因此编写健壮的拆解代码和错误处理逻辑是很重要的。

直接添加终结器

虽然使用 yield 的 fixture 被认为是更干净、更直接的选择,但还有另一种方法,那就是直接将“终结器”函数添加到测试的请求上下文对象中。这种方法与 yield fixture 产生类似的结果,但需要更多的代码量。

为了使用这种方法,我们需要在需要添加拆解代码的 fixture 中请求请求上下文对象(就像我们请求另一个 fixture 一样),然后将包含拆解代码的可调用对象传递给其 addfinalizer 方法。

但我们必须小心,因为 pytest 会在添加终结器后立即运行它,即使在该 fixture 添加终结器后抛出了异常也是如此。因此,为了确保我们不会在不需要时运行终结器代码,我们只有在 fixture 完成了需要拆解的操作后才添加终结器。

下面是前面的例子使用addfinalizer方法的样子:

# content of test_emaillib.py
from emaillib import Email, MailAdminClient

import pytest


@pytest.fixture
def mail_admin():
    return MailAdminClient()


@pytest.fixture
def sending_user(mail_admin):
    user = mail_admin.create_user()
    yield user
    mail_admin.delete_user(user)


@pytest.fixture
def receiving_user(mail_admin, request):
    user = mail_admin.create_user()

    def delete_user():
        mail_admin.delete_user(user)

    request.addfinalizer(delete_user)
    return user


@pytest.fixture
def email(sending_user, receiving_user, request):
    _email = Email(subject="Hey!", body="How's it going?")
    sending_user.send_email(_email, receiving_user)

    def empty_mailbox():
        receiving_user.clear_mailbox()

    request.addfinalizer(empty_mailbox)
    return _email


def test_email_received(receiving_user, email):
    assert email in receiving_user.inbox

它比使用 yield 的 fixture 要长一些,也稍微复杂一些,但在你遇到棘手问题时,它确实提供了一些细微的差别和解决方案。

关于终结器顺序的说明

终结器(finalizers)是按照后进先出(LIFO,Last In First Out)的顺序执行的。对于使用 yield 的 fixture,首先运行的拆解代码来自最右侧的 fixture,即最后一个测试参数。

# content of test_finalizers.py
import pytest


def test_bar(fix_w_yield1, fix_w_yield2):
    print("test_bar")


@pytest.fixture
def fix_w_yield1():
    yield
    print("after_yield_1")


@pytest.fixture
def fix_w_yield2():
    yield
    print("after_yield_2")

$ pytest -s test_finalizers.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item

test_finalizers.py test_bar
.after_yield_2
after_yield_1

对于终结器,首先运行的 fixture 是对 request.addfinalizer 的最后一次调用。

# content of test_finalizers.py
from functools import partial
import pytest


@pytest.fixture
def fix_w_finalizers(request):
    request.addfinalizer(partial(print, "finalizer_2"))
    request.addfinalizer(partial(print, "finalizer_1"))


def test_bar(fix_w_finalizers):
    print("test_bar")

$ pytest -s test_finalizers.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item

test_finalizers.py test_bar
.finalizer_1
finalizer_2

这是因为 yield fixtures 在幕后使用了 addfinalizer:当 fixture 执行时,addfinalizer 会注册一个函数来恢复生成器,该生成器进而调用拆解代码。这样确保了即使在发生异常时,拆解代码也能被执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值