GoogleMock mocking框架:C++接口测试的艺术

GoogleMock mocking框架:C++接口测试的艺术

【免费下载链接】googletest GoogleTest - Google Testing and Mocking Framework 【免费下载链接】googletest 项目地址: https://gitcode.com/gh_mirrors/googl/googletest

本文深入解析GoogleMock框架,全面介绍C++接口测试中的Mocking技术。从Mocking基础概念和GoogleMock架构设计入手,详细讲解了MOCK_METHOD宏的使用方法、mock类创建技巧、期望设置与行为验证的最佳实践,以及Nice/Strict Mock的区别与应用场景。文章通过丰富的代码示例、架构图和对比表格,系统性地阐述了GoogleMock的核心组件、工作原理和使用技巧,为C++开发者提供了一套完整的接口测试解决方案。

Mocking概念与GoogleMock架构解析

在C++测试领域,Mocking(模拟)是一种强大的测试技术,它允许开发者创建虚拟对象来模拟真实依赖的行为。GoogleMock作为GoogleTest框架的重要组成部分,提供了一个完整的Mocking解决方案,让开发者能够编写更加灵活和可靠的单元测试。

Mocking基础概念

Mocking的核心思想是隔离测试行为验证。通过创建模拟对象,我们可以:

  • 隔离被测代码:将测试目标与外部依赖分离,避免测试受到外部系统状态的影响
  • 控制依赖行为:精确控制模拟对象的方法调用和返回值
  • 验证交互行为:确认被测代码是否正确调用了依赖对象的方法

mermaid

GoogleMock架构设计

GoogleMock采用分层架构设计,核心组件包括:

1. 核心架构层

mermaid

2. 严格性控制层

GoogleMock提供了三种不同严格程度的Mock类型:

Mock类型行为描述适用场景
NiceMock允许无预期的调用大型测试套件,关注主要功能
NaggyMock警告无预期的调用(默认)一般测试场景
StrictMock将无预期调用视为错误严格的行为验证
// 严格性控制实现示例
template <typename Base>
class StrictMockImpl {
public:
    StrictMockImpl() {
        ::testing::Mock::FailUninterestingCalls(
            reinterpret_cast<uintptr_t>(this));
    }
    ~StrictMockImpl() {
        ::testing::Mock::UnregisterCallReaction(
            reinterpret_cast<uintptr_t>(this));
    }
};
3. 匹配器系统架构

GoogleMock的匹配器系统采用组合模式设计,支持复杂的参数验证:

mermaid

4. 动作系统设计

动作系统负责定义Mock方法的返回值和行为:

// 动作系统核心接口
class ActionInterface {
public:
    virtual ~ActionInterface() {}
    virtual Result Perform(const ArgumentTuple& args) = 0;
};

// 预定义动作示例
ACTION(Return5) { return 5; }
ACTION(ThrowException) { throw std::runtime_error("error"); }

核心组件详细解析

FunctionMocker:方法模拟的核心

FunctionMocker是GoogleMock的核心组件,负责:

  1. 方法调用拦截:捕获对Mock方法的调用
  2. 期望匹配:查找匹配的期望规则
  3. 动作执行:执行预定义的动作(返回值、抛出异常等)
  4. 调用记录:记录方法调用信息用于后续验证
template <typename R, typename... Args>
class FunctionMocker<R(Args...)> : public UntypedFunctionMockerBase {
public:
    // 查找匹配的期望
    const ExpectationBase* UntypedFindMatchingExpectation(
        const void* untyped_args, const void** untyped_action, 
        bool* is_excessive, ::std::ostream* what, ::std::ostream* why);
    
    // 执行动作
    R PerformDefaultAction(const ArgumentTuple& args, const char* call_description);
};
Expectation:期望管理

期望系统管理所有的预期行为规则:

mermaid

线程安全机制

GoogleMock通过全局互斥锁确保线程安全:

// 全局互斥锁保护所有Mock操作
GTEST_API_ GTEST_DECLARE_STATIC_MUTEX_(g_gmock_mutex);

bool UntypedFunctionMockerBase::VerifyAndClearExpectationsLocked() {
    MutexLock l(&g_gmock_mutex);
    // 验证逻辑...
}

架构优势与设计理念

GoogleMock的架构设计体现了以下几个重要理念:

  1. 类型安全:通过模板元编程确保类型安全
  2. 可扩展性:支持自定义匹配器和动作
  3. 表达力强:流畅的接口语法,代码即文档
  4. 性能优化:尽量减少运行时开销

这种架构设计使得GoogleMock不仅功能强大,而且易于使用和扩展,成为C++单元测试领域的事实标准。

MOCK_METHOD宏与mock类创建详解

在现代C++测试驱动开发中,GoogleMock的MOCK_METHOD宏是构建mock类的核心工具。它通过简洁的语法和强大的类型推导能力,让开发者能够轻松创建复杂的接口模拟对象。本文将深入解析MOCK_METHOD宏的使用方法、参数机制以及mock类的创建技巧。

MOCK_METHOD宏的基本语法

MOCK_METHOD宏提供了两种主要的使用形式:现代语法和传统语法。现代语法更加直观且类型安全:

// 现代语法(推荐)
MOCK_METHOD(ReturnType, MethodName, (Arguments), (Specifiers));

// 示例
MOCK_METHOD(int, GetValue, (), (const, override));
MOCK_METHOD(std::string, ProcessData, (const std::string& input), (noexcept));

传统语法使用数字后缀表示参数个数,虽然仍然可用,但已不推荐在新项目中使用:

// 传统语法(不推荐)
MOCK_METHOD0(GetValue, int());
MOCK_METHOD1(ProcessData, std::string(const std::string&));

宏参数详解

MOCK_METHOD宏接受3-4个参数,每个参数都有特定的作用:

参数位置参数类型描述示例
1返回类型方法的返回类型int, std::string, void
2方法名要mock的方法名称GetValue, ProcessData
3参数列表方法的参数类型列表,用括号包围(int), (const std::string&, bool)
4修饰符可选的修饰符列表,用括号包围(const, override), (noexcept)

修饰符系统

GoogleMock提供了丰富的修饰符来控制mock方法的行为:

mermaid

常用修饰符组合示例
// 常量方法,重写基类虚函数
MOCK_METHOD(int, GetValue, (), (const, override));

// 无异常抛出的方法
MOCK_METHOD(void, Process, (int data), (noexcept));

// Windows COM接口调用约定
MOCK_METHOD(HRESULT, QueryInterface, (REFIID riid, void** ppv), 
            (Calltype(STDMETHODCALLTYPE)));

// 引用限定的方法
MOCK_METHOD(int, ProcessLValue, (), (ref(&)));
MOCK_METHOD(int, ProcessRValue, (), (ref(&&)));

复杂类型处理

MOCK_METHOD宏能够正确处理各种复杂类型,包括模板类型、函数指针和包含逗号的类型:

// 包含逗号的返回类型需要使用额外括号
MOCK_METHOD((std::map<int, std::string>), GetMapping, (), ());

// 函数指针类型
using Callback = void(*)(int, const std::string&);
MOCK_METHOD(void, SetCallback, (Callback cb), ());

// 模板类型参数
MOCK_METHOD(bool, Validate, (const std::vector<int>& data), ());

Mock类创建最佳实践

创建完整的mock类需要遵循一定的模式和最佳实践:

// 接口定义
class DataProcessor {
public:
    virtual ~DataProcessor() = default;
    virtual bool Process(const std::string& input) = 0;
    virtual std::string GetResult() const = 0;
    virtual void Reset() noexcept = 0;
};

// Mock类实现
class MockDataProcessor : public DataProcessor {
public:
    MOCK_METHOD(bool, Process, (const std::string& input), (override));
    MOCK_METHOD(std::string, GetResult, (), (const, override));
    MOCK_METHOD(void, Reset, (), (noexcept, override));
    
    // 可选:默认行为设置
    MockDataProcessor() {
        ON_CALL(*this, Process)
            .WillByDefault(Return(true));
        ON_CALL(*this, GetResult)
            .WillByDefault(Return("default_result"));
    }
};

高级用法:参数匹配和返回值控制

MOCK_METHOD宏生成的mock方法会自动创建对应的gmock_方法用于设置期望:

TEST(DataProcessorTest, ProcessDataSuccess) {
    MockDataProcessor processor;
    
    // 设置期望:Process方法被调用一次,参数为"test_data",返回true
    EXPECT_CALL(processor, Process("test_data"))
        .WillOnce(Return(true));
    
    // 设置期望:GetResult方法被调用任意次数,返回"success"
    EXPECT_CALL(processor, GetResult())
        .WillRepeatedly(Return("success"));
    
    // 执行测试
    bool result = processor.Process("test_data");
    std::string output = processor.GetResult();
    
    EXPECT_TRUE(result);
    EXPECT_EQ("success", output);
}

错误处理和调试技巧

在使用MOCK_METHOD时,常见的错误包括:

  1. 参数类型不匹配:确保mock方法的签名与接口完全一致
  2. 修饰符顺序错误:修饰符应该按照特定顺序排列
  3. 复杂类型处理:包含逗号的类型需要使用额外括号

调试技巧:

  • 使用GMOCK_INTERNAL_STRICT_SPEC_ASSERT开启严格的修饰符检查
  • 检查编译器错误信息,通常会有详细的错误提示
  • 使用静态断言确保类型正确性

性能考虑

虽然MOCK_METHOD宏提供了强大的功能,但在性能敏感的场景中需要注意:

  • 虚函数调用有一定的性能开销
  • 复杂的参数匹配和验证逻辑会增加运行时成本
  • 在release构建中考虑禁用mock相关代码

通过深入理解MOCK_METHOD宏的工作原理和使用技巧,开发者可以创建出既强大又易于维护的mock类,为C++项目的单元测试提供坚实的基础。

期望设置与行为验证最佳实践

GoogleMock框架提供了强大而灵活的期望设置机制,让开发者能够精确控制mock对象的行为并验证测试的执行路径。正确的期望设置是编写高质量单元测试的关键,本文将深入探讨期望设置与行为验证的最佳实践。

期望设置的基本语法

GoogleMock使用EXPECT_CALL宏来设置方法调用的期望,其基本语法结构如下:

EXPECT_CALL(mock_object, MethodName(argument_matchers))
    .Times(cardinality)
    .WillOnce(action)
    .WillRepeatedly(action);

这个语法链允许我们精确指定:

  • 哪个mock对象的哪个方法会被调用
  • 使用什么参数匹配器来验证调用参数
  • 方法会被调用多少次(基数约束)
  • 每次调用时执行什么动作

基数约束的精确控制

基数约束是期望设置中最关键的部分之一,它定义了方法期望被调用的次数范围。GoogleMock提供了丰富的基数约束选项:

// 精确次数约束
EXPECT_CALL(mock, Method()).Times(3);           // 恰好3次
EXPECT_CALL(mock, Method()).Times(AtLeast(2));  // 至少2次
EXPECT_CALL(mock, Method()).Times(AtMost(5));   // 最多5次
EXPECT_CALL(mock, Method()).Times(Between(1, 4)); // 1到4次之间

// 特殊基数约束
EXPECT_CALL(mock, Method()).Times(AnyNumber()); // 任意次数(包括0次)
EXPECT_CALL(mock, Method()).Times(0);           // 永远不被调用

动作链的合理组合

动作链定义了方法被调用时的行为响应。合理组合WillOnce()WillRepeatedly()是编写清晰测试的关键:

// 单次动作序列
EXPECT_CALL(mock, GetValue())
    .WillOnce(Return(1))
    .WillOnce(Return(2))
    .WillOnce(Return(3))
    .WillRepeatedly(Return(0));  // 后续所有调用返回0

// 混合动作模式
EXPECT_CALL(mock, ProcessItem(_))
    .Times(AtLeast(1))
    .WillOnce(Invoke([](const Item& item) {
        return item.IsValid();
    }))
    .WillRepeatedly(Return(true));

参数匹配器的精确使用

参数匹配器让测试更加精确和健壮,避免因不相关的参数变化导致测试失败:

// 精确值匹配
EXPECT_CALL(mock, Process(42, "test")); 

// 通配符匹配
EXPECT_CALL(mock, Process(_, _));           // 任意两个参数
EXPECT_CALL(mock, Process(Gt(10), _));      // 第一个参数大于10

// 类型匹配
EXPECT_CALL(mock, Process(An<int>(), _));   // 第一个参数是int类型

// 自定义匹配器
EXPECT_CALL(mock, Process(IsEven(), _));    // 使用自定义匹配器

调用顺序的精确控制

对于需要特定调用顺序的场景,GoogleMock提供了多种顺序控制机制:

// 使用InSequence控制严格顺序
{
    testing::InSequence seq;
    EXPECT_CALL(mock, Initialize()).Times(1);
    EXPECT_CALL(mock, ProcessData(_)).Times(AtLeast(1));
    EXPECT_CALL(mock, Cleanup()).Times(1);
}

// 使用After指定相对顺序
Expectation init = EXPECT_CALL(mock, Initialize()).Times(1);
EXPECT_CALL(mock, ProcessData(_)).Times(AtLeast(1)).After(init);
EXPECT_CALL(mock, Cleanup()).Times(1).After(init);

期望的生命周期管理

正确管理期望的生命周期对于避免测试间的相互影响至关重要:

TEST_F(MyTest, TestCase1) {
    MockService mock;
    
    // 设置期望
    EXPECT_CALL(mock, Method1()).Times(1);
    EXPECT_CALL(mock, Method2()).Times(AnyNumber());
    
    // 执行测试
    TestFunction(&mock);
    
    // 期望在mock对象析构时自动验证
}

// 手动验证和清理
TEST_F(MyTest, TestCase2) {
    MockService mock;
    
    EXPECT_CALL(mock, Method()).Times(2);
    
    // 部分执行
    mock.Method();
    
    // 手动验证(可选)
    testing::Mock::VerifyAndClearExpectations(&mock);
    
    // 重新设置期望
    EXPECT_CALL(mock, Method()).Times(1);
    mock.Method();
}

避免常见的期望设置陷阱

在实际使用中,需要注意以下几个常见问题:

过度指定期望:避免为每个方法调用都设置精确的期望,这会降低测试的健壮性。

// 不推荐:过度指定
EXPECT_CALL(mock, Method1()).Times(1);
EXPECT_CALL(mock, Method2()).Times(1);
EXPECT_CALL(mock, Method3()).Times(1);

// 推荐:只关注关键行为
EXPECT_CALL(mock, CriticalMethod(_)).Times(1).WillOnce(Return(success));

忽略默认行为:合理使用ON_CALL设置默认行为,减少重复的期望设置。

// 设置默认返回值
ON_CALL(mock, NonCriticalMethod()).WillByDefault(Return(default_value));

// 测试中只关注特定场景
EXPECT_CALL(mock, NonCriticalMethod(special_input))
    .WillOnce(Return(special_output));

未处理边界情况:确保测试覆盖异常和边界情况。

// 测试正常流程
EXPECT_CALL(mock, Process(_)).Times(1).WillOnce(Return(success));

// 测试异常流程
EXPECT_CALL(mock, Process(invalid_input))
    .Times(1)
    .WillOnce(Throw(std::runtime_error("Invalid input")));

性能优化的期望设置

对于性能敏感的测试场景,可以采用以下优化策略:

// 使用AnyNumber避免精确计数开销
EXPECT_CALL(mock, FrequentMethod(_)).Times(AnyNumber());

// 批量设置相似期望
for (int i = 0; i < 100; ++i) {
    EXPECT_CALL(mock, ProcessItem(i)).WillOnce(Return(i * 2));
}

// 使用动作工厂减少重复代码
auto action_factory = [](int base) {
    return [base](int input) { return input + base; };
};

EXPECT_CALL(mock, Transform(_))
    .WillRepeatedly(Invoke(action_factory(100)));

期望验证的最佳实践

期望验证不仅仅是检查调用次数,还包括调用参数、顺序和返回值的全面验证:

// 全面验证示例
TEST_F(ComplexTest, ComprehensiveVerification) {
    MockProcessor processor;
    
    // 设置精确的期望
    EXPECT_CALL(processor, Initialize(config_matcher))
        .Times(1)
        .WillOnce(Return(true));
    
    EXPECT_CALL(processor, ProcessData(data_matcher))
        .Times(Between(1, 10))
        .WillRepeatedly(Invoke(&real_processor, &RealProcessor::ProcessData));
    
    EXPECT_CALL(processor, GetStatus())
        .Times(AtLeast(1))
        .WillRepeatedly(Return(ProcessorStatus::RUNNING));
    
    EXPECT_CALL(processor, Cleanup())
        .Times(1)
        .WillOnce(Return(true));
    
    // 执行测试
    TestScenario(&processor);
    
    // 所有期望自动验证
}

通过遵循这些最佳实践,您可以编写出更加健壮、可维护和高效的单元测试,确保代码质量的同时提高开发效率。

Nice/Strict Mock区别与应用场景

在GoogleMock框架中,Mock对象的行为严格程度是一个重要的设计选择。框架提供了三种不同严格程度的Mock类型:NiceMock、NaggyMock和StrictMock,它们对未预期调用(uninteresting calls)的处理方式完全不同。

三种Mock类型的核心区别

Mock类型未预期调用处理方式适用场景
NiceMock静默忽略,不产生警告或错误测试关注重点功能,忽略次要交互
NaggyMock产生警告信息但测试继续执行默认行为,用于调试和开发阶段
StrictMock立即失败,终止测试执行严格验证所有交互的测试场景

技术实现机制

GoogleMock通过模板类包装器来实现不同的严格程度:

// NiceMock实现 - 允许未预期调用
template <class MockClass>
class NiceMock : private internal::NiceMockImpl<MockClass>,
                 public MockClass {
    // 继承所有MockClass的构造函数
};

// StrictMock实现 - 严格验证所有调用  
template <class MockClass>
class StrictMock : private internal::StrictMockImpl<MockClass>,
                   public MockClass {
    // 继承所有MockClass的构造函数
};

每个实现类内部使用不同的策略类来处理未预期调用:

mermaid

具体行为差异示例

NiceMock使用场景
// 测试关注主要功能,忽略次要交互
TEST(PaymentProcessorTest, ProcessPaymentIgnoresLoggingCalls) {
    NiceMock<MockLogger> logger;  // 忽略日志调用的未预期调用
    PaymentProcessor processor(&logger);
    
    // 只关注支付处理的主要逻辑
    EXPECT_CALL(logger, LogPaymentStart()).Times(1);
    EXPECT_CALL(logger, LogPaymentSuccess()).Times(1);
    
    processor.ProcessPayment(100.0);
    // 即使logger有其他未预期的调用,测试也不会失败
}
StrictMock使用场景
// 严格验证所有交互的协议测试
TEST(ProtocolHandlerTest, StrictValidationOfAllMessages) {
    StrictMock<MockNetworkInterface> network;
    ProtocolHandler handler(&network);
    
    // 明确期望所有可能的网络交互
    EXPECT_CALL(network, SendHandshake()).Times(1);
    EXPECT_CALL(network, ReceiveAck()).WillOnce(Return(true));
    EXPECT_CALL(network, SendData(_)).Times(1);
    EXPECT_CALL(network, CloseConnection()).Times(1);
    
    handler.HandleProtocol();
    // 任何未预期的网络调用都会导致测试失败
}

应用场景选择指南

适合使用NiceMock的场景
  1. 大型复杂系统的单元测试:当被测试对象与多个依赖交互,但测试只关注特定功能时
  2. 遗留代码重构:在重构过程中,逐步添加测试而不破坏现有功能
  3. 第三方库集成测试:当无法控制所有外部依赖的行为时
  4. 性能监控测试:关注性能指标而非所有交互细节
// 示例:性能监控测试中使用NiceMock
TEST(PerformanceMonitorTest, MeasuresResponseTime) {
    NiceMock<MockDatabase> db;      // 忽略数据库的未预期调用
    NiceMock<MockCache> cache;      // 忽略缓存的未预期调用
    PerformanceMonitor monitor(&db, &cache);
    
    // 只关注关键的性能相关调用
    EXPECT_CALL(db, Query(_)).WillOnce(Return(ResultSet{}));
    EXPECT_CALL(cache, Get(_)).WillOnce(Return(nullptr));
    
    auto duration = monitor.MeasureQueryTime("SELECT * FROM users");
    EXPECT_LT(duration, 100ms);  // 只验证性能指标
}
适合使用StrictMock的场景
  1. 协议一致性测试:验证通信协议的所有消息交换
  2. 安全关键系统:确保所有安全相关的交互都被正确执行
  3. API契约测试:验证客户端和服务端之间的严格契约
  4. 边界条件测试:确保在异常情况下所有清理操作都正确执行
// 示例:安全关键系统中的StrictMock使用
TEST(SecurityManagerTest, StrictAuthenticationFlow) {
    StrictMock<MockAuthService> auth;
    StrictMock<MockAuditLogger> audit;
    SecurityManager manager(&auth, &audit);
    
    // 严格定义认证流程的所有步骤
    InSequence s;  // 确保调用顺序
    EXPECT_CALL(auth, ValidateCredentials(_, _)).WillOnce(Return(true));
    EXPECT_CALL(audit, LogLoginSuccess(_)).Times(1);
    EXPECT_CALL(auth, CreateSession(_)).WillOnce(Return("session123"));
    
    auto result = manager.Authenticate("user", "pass");
    EXPECT_TRUE(result.success);
    // 任何未预期的认证相关调用都会导致测试失败
}

最佳实践建议

  1. 默认使用NaggyMock:在开发阶段使用默认的NaggyMock,它提供警告但不中断测试,有助于发现潜在的未预期调用

  2. 渐进式严格化:开始使用NiceMock建立测试基础,然后逐步引入StrictMock来增强测试的严格性

  3. 按模块区分严格程度:对核心业务逻辑使用StrictMock,对辅助功能使用NiceMock

  4. 避免过度使用StrictMock:过度严格的测试可能变得脆弱,难以维护。只在确实需要验证所有交互时使用

  5. 结合使用场景选择:根据测试的具体目的选择合适的Mock类型,而不是一概而论

// 混合使用示例:核心业务严格,辅助功能宽松
TEST(OrderProcessingTest, MixedStrictnessStrategy) {
    StrictMock<MockInventory> inventory;  // 库存管理必须严格
    NiceMock<MockNotifier> notifier;      // 通知系统可以宽松
    OrderProcessor processor(&inventory, &notifier);
    
    // 严格验证库存相关的关键交互
    EXPECT_CALL(inventory, ReserveItem(_, _)).WillOnce(Return(true));
    EXPECT_CALL(inventory, UpdateStock(_)).Times(1);
    
    // 通知相关的调用允许有未预期的交互
    EXPECT_CALL(notifier, SendConfirmation(_)).Times(1);
    
    processor.ProcessOrder(order);
    // 库存相关的未预期调用会失败,通知相关的不会
}

常见陷阱与解决方案

  1. 构造函数中的Mock方法调用:StrictMock在构造函数中可能无法正常工作,因为虚函数表尚未完全建立
// 错误示例:在构造函数中使用StrictMock
class ProblematicClass {
public:
    ProblematicClass() {
        strictMock.SomeMethod();  // 可能导致未定义行为
    }
private:
    StrictMock<MockClass> strictMock;
};

// 解决方案:使用初始化方法或工厂模式
class SafeClass {
public:
    void Initialize() {
        strictMock.SomeMethod();  // 在完全构造后调用
    }
private:
    StrictMock<MockClass> strictMock;
};
  1. 测试维护性:过度使用StrictMock会使测试变得脆弱,对代码的小幅修改可能导致大量测试失败

  2. 性能考虑:StrictMock的运行时检查可能带来轻微性能开销,在性能敏感的测试中需要考虑这一点

通过合理选择NiceMock和StrictMock,开发者可以在测试的严格性和维护性之间找到最佳平衡点,构建出既可靠又易于维护的测试套件。

总结

GoogleMock作为C++单元测试领域的事实标准,提供了强大而灵活的Mocking解决方案。通过本文的系统介绍,我们深入理解了GoogleMock的架构设计、核心组件和使用最佳实践。从基础的MOCK_METHOD宏使用到复杂的期望设置,从NiceMock的宽松验证到StrictMock的严格检查,GoogleMock为不同测试场景提供了全方位的支持。合理运用这些技术,开发者可以编写出更加健壮、可维护和高效的单元测试,有效提升代码质量和开发效率。掌握GoogleMock不仅是一种技术能力,更是现代C++开发中保证软件质量的重要艺术。

【免费下载链接】googletest GoogleTest - Google Testing and Mocking Framework 【免费下载链接】googletest 项目地址: https://gitcode.com/gh_mirrors/googl/googletest

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值