关注httprunner这个工具

https://zhuanlan.zhihu.com/p/84066392

 

1. 工欲善其事,必先利其器

作为一个测试从事人员,

你是否为每天“点点点”的工作而感到索然无味,提不起丝毫兴趣?

你是否因为没有强大的代码编写能力,对有难度的测试任务望而却步?

每天重复的功能测试,日复一日的“原地踏步”,你,感到焦虑吗?

好像HttpRunner可以解决上述问题.

HttpRunner作者介绍该工具遵循 约定大于配置 的准则.

不追求重复造轮子,而是将强大的轮子组装成战车

追求投入产出比,一份投入即可实现多种测试需求

通过配置文件的方式描述用例.

如此一来,简单的测试用例并不需要有较强的代码编写能力.

只需要依葫芦画瓢,修改若干配置,将所需用例描述出来即可.

2. 磨刀不误砍柴工

效率,考量一件工具是否好用的标准.

在使用之前, 我们先来分析一下该工具能好用么?

HttpRunner作者反复的考量设计模式. 给出了适合并优雅的设计.

抽象测试工作组件(HttpRunner文档原图):

 

 

  • 测试用例 : 测试用例为最小组件, 测试用例 = 测试脚本 + 测试数据
    • 测试脚本 : 重点是描述测试的 业务功能逻辑,包括预置条件、测试步骤、预期结果等,并且可以结合辅助函数(debugtalk.py)实现复杂的运算逻辑;可以将 测试脚本 理解为编程语言中的 类(class)
    • 测试数据 : 重点是对应测试的 业务数据逻辑,可以理解为类的实例化数据;
  • 测试步骤 : 测试用例是测试步骤的 "有序" 集合 而对于接口测试来说,每一个测试步骤应该就对应一个 API 的请求描述。
  • 测试用例集 : 测试用例集 是 测试用例 的 "无序" 集合 集合中的测试用例应该都是相互独立,不存在先后依赖关系的。 如果确实存在先后依赖关系怎么办,例如登录功能和下单功能。 正确的做法应该是,在下单测试用例的前置步骤中执行登录操作。

有了以上的思路,就不难想象出应该以何种正确的姿势使用工具了.

3. 擒龙要下海,打虎要上山

咱们动手试试这个工具是否真的很厉害.

首先HttpRunner是支持测试接口录制的.

快速上手用法很简单

  1. 打开代理工具Charles
  2. 操作一遍测试的接口用例
  3. 导出.har格式抓包结果
  4. 通过工具自带命令har2case导出成yml配置文件
  5. 通过命令工具hrun 执行yml配置文件

如此就重放了一遍刚刚操作的测试接口用例. 就是这么简单!

进阶用法,自定义一个测试用例.

使用官方的脚手架工具可以生产一套示例代码.

目录结构(官方推荐)如下

hrun --startproject demo

Start to create new project: demo
CWD: /Users/fangzimo/Downloads/test

created folder: demo
created folder: demo/api
created folder: demo/testcases
created folder: demo/testsuites
created folder: demo/reports
created file: demo/api/demo_api.yml
created file: demo/testcases/demo_testcase.yml
created file: demo/testsuites/demo_testsuite.yml
created file: demo/debugtalk.py
created file: demo/.env
created file: demo/.gitignore

#################################################

tree -a demo

demo
├── .env
├── .gitignore
├── api
│   └── demo_api.yml
├── debugtalk.py
├── reports
├── testcases
│   └── demo_testcase.yml
└── testsuites
    └── demo_testsuite.yml

说一下目录结构:

  • .env 放置在项目根目下,一般将敏感 公用信息存放与此
  • debugtalk.py 封装例中用到的一些自定义处理逻辑
  • 接口定义(API)描述api调用基础信息(如同api文档一般的基础信息),地址,入参,返回
  • 测试用例(testcase)应该是完整且独立的,每条测试用例应该是都可以独立运行的
  • 测试用例集(testsuite)是测试用例的 无序 集合,集合中的测试用例应该都是相互独立
  • 若有存储参数化文件,或者项目依赖的文件,可以新建并放到 data 文件夹
  • reports 存储 HTML 测试报告

将上面的demo简单用起来:

  1. 定义一个实际的api
  2. 针对这个api创建2个测试用例
  3. 定义一个测试用例集包含这两个测试用例.
  4. 用数据驱动的方式跑这个测试用例集

借用一下百度翻译的接口: https://fanyi.baidu.com/sug 这个接口需要post方式传递一个参数kw 然后会返回输入建议信息

curl "https://fanyi.baidu.com/sug" -d "kw=test"

{
  "errno": 0,
  "data": [
    {
      "k": "test",
      "v": "n. 测验; 考查; (医疗上的)检查,化验,检验; 试验; 测试; v. 测验; 考查; 试验; "
    },
    {
      "k": "tests",
      "v": "n. 测验; 考查; (医疗上的)检查,化验,检验; 试验; 测试; v. 测验; 考查; 试验; "
    },
    {
      "k": "testimony",
      "v": "n. 证据; 证明; 证词; 证言; 口供;"
    },
    {
      "k": "testing",
      "v": "n. 试验; 测试; 检查; adj. 棘手的; 伤脑筋的; 难应付的; v. 测验; 考查; 试验"
    },
    {
      "k": "tested",
      "v": "v. 测验; 考查; 试验; 检查; 化验; 检验; 测试;  test的过去分词和过去式;"
    }
  ]
}

.env中定义出接口的公用信息:

BASE_URL=https://fanyi.baidu.com

在 api 创建 sug.yml 定义api

name: sug api
variables:           
    key_word: ""        #定义变量
request:
    url: /sug           #路径
    method: POST        
    headers:            #请求头信息
        Content-Type: "application/x-www-form-urlencoded; charset=UTF-8" 
    data:
        kw: $key_word   #通过变量赋值
validate:               #返回校验
    - eq: ["status_code", 200]

创建一个teststep,在 testcases 中新建 sug_step.yml

config:
    name: "get sug info"
    id: sug_info
    variables:
        key_word: test
    base_url: ${ENV(BASE_URL)}
    verify: False
    export:
        - data

teststeps:
    - name: sug info
      api: api/sug.yml
      variables:
          key_word: $key_word
      extract:
          - data: content.data

创建两个测试用例,一个测试成功状态 sug_info_success.yml,一个测试失败状态 sug_info_fail.yml

sug_info_success.yml

config:
    name: "get sug info success"
    id: sug_info_succ
    base_url: ${ENV(BASE_URL)}
    variables:
        key_word: test

teststeps:
    - name: get sug info
      testcase: testcases/sug_step.yml
      extract:
          - data
      validate:
          - eq: ["status_code", 200]
          - eq: ["content.errno", 0]

sug_info_fail.yml

config:
    name: "get sug info fail"
    id: sug_info_fail
    base_url: ${ENV(BASE_URL)}
    variables:
        key_word: ""

teststeps:
    - name: get sug info
      testcase: testcases/sug_step.yml
      extract:
          - data
      validate:
          - eq: ["status_code", 200]
          - eq: ["content.errno", 1]

最后在 testsuites 中创建一个测试用例集把上面两个用例包含进来. 并且采用数据驱动的方式跑测试成功用例.注意定义方式,本示例中是通过函数方式返回.

config:
    name: "sug info testsuite"
    variables:
        key_word: ""
    base_url: ${ENV(BASE_URL)}

testcases:
    - name: call sug info success with data
      testcase: testcases/sug_info_success.yml
      variables:
          key_word: $key_word
      parameters:
          key_word: ${get_key_word(1)}
    - name: call sug info fail
      testcase: testcases/sug_info_fail.yml
      variables:
          key_word: ""

debugtalk.py中完善 get_key_word方法 简单演示了函数的使用,和传参方式,以及数据的返回.

def get_key_word(index):
    words = {
        1: ['a', 'b', 'c', 'd'],
        2: ['e', 'f', 'g', 'h']
    }
    return words.get(index)

最后激动人心的时刻到了!执行用例~

先来试试完整且独立的最小单元case

➜  demo hrun testcases/sug_info_success.yml
INFO     HttpRunner version: 2.2.5
INFO     Loading environment variables from /Users/fangzimo/Downloads/test/demo/.env
INFO     Start to run testcase: get sug info success
get sug info
INFO     POST https://fanyi.baidu.com/sug
INFO     status_code: 200, response_time(ms): 187.6 ms, response_length: 889 bytes

INFO
==================== Output ====================
Variable         : Value
---------------- : -----------------------------
data             : [{"k": "test", "v": "n. \u6d4b\u9a8c; \u8003\u67e5; (\u533b\u7597\u4e0a\u7684)\u68c0\u67e5\uff0c\u5316\u9a8c\uff0c\u68c0\u9a8c; \u8bd5\u9a8c; \u6d4b\u8bd5; v. \u6d4b\u9a8c; \u8003\u67e5; \u8bd5\u9a8c; "}, {"k": "tests", "v": "n. \u6d4b\u9a8c; \u8003\u67e5; (\u533b\u7597\u4e0a\u7684)\u68c0\u67e5\uff
0c\u5316\u9a8c\uff0c\u68c0\u9a8c; \u8bd5\u9a8c; \u6d4b\u8bd5; v. \u6d4b\u9a8c; \u8003\u67e5; \u8bd5\u9a8c; "}, {"k": "testimony", "v": "n. \u8bc1\u636e; \u8bc1\u660e; \u8bc1\u8bcd; \u8bc1\u8a00; \u53e3\u4f9b;"}, {"k": "testing", "v": "n. \u8bd5\u9a8c; \u6d4b\u8bd5; \u68c0\u67e5; adj. \u68d8\u624b\u7684; \u4f24\u8111\
u7b4b\u7684; \u96be\u5e94\u4ed8\u7684; v. \u6d4b\u9a8c; \u8003\u67e5; \u8bd5\u9a8c"}, {"k": "tested", "v": "v. \u6d4b\u9a8c; \u8003\u67e5; \u8bd5\u9a8c; \u68c0\u67e5; \u5316\u9a8c; \u68c0\u9a8c; \u6d4b\u8bd5;  test\u7684\u8fc7\u53bb\u5206\u8bcd\u548c\u8fc7\u53bb\u5f0f;"}]
------------------------------------------------

.

----------------------------------------------------------------------
Ran 1 test in 0.189s

OK
INFO     Start to render Html report ...
INFO     Generated Html report: /Users/fangzimo/Downloads/test/demo/reports/1569234066.html


➜  demo hrun testcases/sug_info_fail.yml
INFO     HttpRunner version: 2.2.5
INFO     Loading environment variables from /Users/fangzimo/Downloads/test/demo/.env
INFO     Start to run testcase: get sug info fail
get sug info
INFO     POST https://fanyi.baidu.com/sug
INFO     status_code: 200, response_time(ms): 176.11 ms, response_length: 21 bytes

INFO
==================== Output ====================
Variable         : Value
---------------- : -----------------------------
data             : []
------------------------------------------------

.

----------------------------------------------------------------------
Ran 1 test in 0.177s

OK
INFO     Start to render Html report ...
INFO     Generated Html report: /Users/fangzimo/Downloads/test/demo/reports/1569234134.html

测试报告就在reports目录下.可以查看具体的详情(请求信息,响应信息等))

最后我们来跑一下这个测试集

➜  demo hrun testsuites/sug_info_testsuite.yml
INFO     HttpRunner version: 2.2.5
INFO     Loading environment variables from /Users/fangzimo/Downloads/test/demo/.env
INFO     Start to run testcase: call sug info success with data
get sug info
INFO     POST https://fanyi.baidu.com/sug
INFO     status_code: 200, response_time(ms): 158.67 ms, response_length: 846 bytes

INFO     
==================== Output ====================
Variable         : Value
---------------- : -----------------------------
data             : [{"k": "and", "v": "conj. \u548c; \u4e0e; \u540c; \u53c8; \u800c; \u52a0; \u52a0\u4e0a; \u7136\u540e; \u63a5\u7740;"}, {"k": "available", "v": "adj. \u53ef\u83b7\u5f97\u7684; \u53ef\u8d2d\u5f97\u7684; \u53ef\u627e\u5230\u7684; \u6709\u7a7a\u7684;"}, {"k": "as", "v": "prep. \u50cf; \u5982\u540c; \u4f5c\u4e3a; \u5f53\u4f5c; adv. (\u6bd4\u8f83\u65f6\u7528)\u50cf\u2026\u4e00\u6837\uff0c\u5982\u540c; (\u6307\u4e8b\u60c5\u4ee5\u540c\u6837\u7684\u65b9"}, {"k": "all", "v": "det. \u6240\u6709; \u5168\u90e8; \u5168\u4f53; \u4e00\u5207; (\u4e0e\u5355\u6570\u540d\u8bcd\u8fde\u7528\uff0c\u8868\u793a\u67d0\u4e8b\u5728\u67d0\u6bb5\u65f6\u95f4\u5185\u6301\u7eed\u53d1\u751f)\u5168\u90e8\u7684\uff0c\u6574"}, {"k": "at", "v": "prep. \u5728(\u67d0\u5904); \u5728(\u5b66\u4e60\u6216\u5de5\u4f5c\u5730\u70b9); \u5728(\u67d0\u65f6\u95f4\u6216\u65f6\u523b);"}]
------------------------------------------------

.

----------------------------------------------------------------------
Ran 1 test in 0.160s

OK
INFO     Start to run testcase: call sug info success with data
get sug info
INFO     POST https://fanyi.baidu.com/sug
INFO     status_code: 200, response_time(ms): 133.78 ms, response_length: 961 bytes

INFO     
==================== Output ====================
Variable         : Value
---------------- : -----------------------------
data             : [{"k": "bear", "v": "v. \u627f\u53d7; \u5fcd\u53d7; \u4e0d\u9002\u4e8e\u67d0\u4e8b(\u6216\u505a\u67d0\u4e8b); \u627f\u62c5\u8d23\u4efb; n. \u718a; (\u5728\u8bc1\u5238\u5e02\u573a\u7b49)\u5356\u7a7a\u7684\u4eba;"}, {"k": "but", "v": "conj. \u800c; \u76f8\u53cd; \u7136\u800c; \u5c3d\u7ba1\u5982\u6b64; \u8868\u793a\u6b49\u610f\u65f6\u8bf4; prep. \u9664\u4e86; \u9664\u2026\u4e4b\u5916; adv"}, {"k": "break", "v": "v. (\u4f7f)\u7834\uff0c\u88c2\uff0c\u788e; \u5f04\u574f; \u635f\u574f; \u574f\u6389; \u5f04\u7834; \u4f7f\u6d41\u8840; n. \u95f4\u6b47; \u4f11\u606f; \u8bfe\u95f4\u4f11\u606f;"}, {"k": "back", "v": "n. (\u4eba\u4f53\u6216\u52a8\u7269\u7684)\u80cc\u90e8\uff0c\u80cc; \u8170\u80cc; \u810a\u67f1; \u810a\u6881\u9aa8; \u540e\u90e8; \u540e\u9762; \u672b\u5c3e; adj. \u80cc\u540e\u7684"}, {"k": "business", "v": "n. \u5546\u4e1a; \u4e70\u5356; \u751f\u610f; \u5546\u52a1; \u516c\u4e8b; \u8425\u4e1a\u989d; \u8d38\u6613\u989d; \u8425\u4e1a\u72b6\u51b5;"}]
------------------------------------------------

.

----------------------------------------------------------------------
Ran 1 test in 0.135s

OK
INFO     Start to run testcase: call sug info success with data
get sug info
INFO     POST https://fanyi.baidu.com/sug
INFO     status_code: 200, response_time(ms): 233.54 ms, response_length: 1124 bytes

INFO     
==================== Output ====================
Variable         : Value
---------------- : -----------------------------
data             : [{"k": "can", "v": "modal (\u8868\u793a\u6709\u80fd\u529b\u505a\u6216\u80fd\u591f\u53d1\u751f)\u80fd\uff0c\u4f1a; (\u8868\u793a\u77e5\u9053\u5982\u4f55\u505a)\u61c2\u5f97; \u4e0e\u52a8\u8bcdfeel\u3001hear\u3001"}, {"k": "correct", "v": "adj. \u51c6\u786e\u65e0\u8bef\u7684; \u7cbe\u786e\u7684; \u6b63\u786e\u7684; \u6070\u5f53\u7684; \u5408\u9002\u7684; (\u4e3e\u6b62\u8a00\u8c08)\u7b26\u5408\u516c\u8ba4\u51c6\u5219\u7684\uff0c\u5f97\u4f53\u7684;"}, {"k": "certain", "v": "adj. \u786e\u5b9e; \u786e\u5b9a; \u80af\u5b9a; \u786e\u4fe1; \u65e0\u7591; (\u4e0d\u63d0\u53ca\u7ec6\u8282\u65f6\u7528)\u67d0\u4e8b\uff0c\u67d0\u4eba\uff0c\u67d0\u79cd; pron. "}, {"k": "common", "v": "adj. \u5e38\u89c1\u7684; \u901a\u5e38\u7684; \u666e\u904d\u7684; \u5171\u6709\u7684; \u5171\u4eab\u7684; \u5171\u540c\u7684; \u666e\u901a\u7684; \u5e73\u5e38\u7684; \u5bfb\u5e38\u7684; "}, {"k": "course", "v": "n. (\u6709\u5173\u67d0\u5b66\u79d1\u7684\u7cfb\u5217)\u8bfe\u7a0b\uff0c\u8bb2\u5ea7; (\u5927\u5b66\u4e2d\u8981\u8fdb\u884c\u8003\u8bd5\u6216\u53d6\u5f97\u8d44\u683c\u7684)\u8bfe\u7a0b; (\u8239\u6216\u98de\u673a\u7684)\u822a\u5411\uff0c"}]
------------------------------------------------

.

----------------------------------------------------------------------
Ran 1 test in 0.234s

OK
INFO     Start to run testcase: call sug info success with data
get sug info
INFO     POST https://fanyi.baidu.com/sug
INFO     status_code: 200, response_time(ms): 223.75 ms, response_length: 951 bytes

INFO     
==================== Output ====================
Variable         : Value
---------------- : -----------------------------
data             : [{"k": "during", "v": "prep. \u5728\u2026\u671f\u95f4; \u5728\u2026\u671f\u95f4\u7684\u67d0\u4e2a\u65f6\u5019;"}, {"k": "draw", "v": "v. (\u7528\u94c5\u7b14\u3001\u94a2\u7b14\u6216\u7c89\u7b14)\u753b\uff0c\u63cf\u7ed8\uff0c\u63cf\u753b; \u62d6(\u52a8); \u62c9(\u52a8); \u7275\u5f15; \u62c9\uff0c\u62d6(\u8f66); \u5438\u5f15\uff0c"}, {"k": "dress", "v": "n. \u8fde\u8863\u88d9; \u8863\u670d; v. \u7a7f\u8863\u670d; \u7ed9(\u67d0\u4eba)\u7a7f\u8863\u670d; \u7a7f\u2026\u7684\u670d\u88c5; \u7a7f\u6b63\u5f0f\u670d\u88c5;"}, {"k": "do", "v": "v. \u505a\uff0c\u5e72\uff0c\u529e(\u67d0\u4e8b); (\u4ee5\u67d0\u79cd\u65b9\u5f0f)\u505a; \u884c\u52a8; \u8868\u73b0; (\u95ee\u8be2\u6216\u8c08\u8bba\u65f6\u7528)\u8fdb\u5c55\uff0c\u8fdb\u884c; au"}, {"k": "drop", "v": "v. (\u610f\u5916\u5730)\u843d\u4e0b\uff0c\u6389\u4e0b\uff0c\u4f7f\u843d\u4e0b; (\u6545\u610f)\u964d\u4e0b\uff0c\u4f7f\u964d\u843d; \u7d2f\u5012; \u7d2f\u57ae; n. \u6ef4; \u6c34\u73e0; \u5c11"}]
------------------------------------------------

.

----------------------------------------------------------------------
Ran 1 test in 0.225s

OK
INFO     Start to run testcase: call sug info fail
get sug info
INFO     POST https://fanyi.baidu.com/sug
INFO     status_code: 200, response_time(ms): 225.91 ms, response_length: 21 bytes

INFO     
==================== Output ====================
Variable         : Value
---------------- : -----------------------------
data             : []
------------------------------------------------

.

----------------------------------------------------------------------
Ran 1 test in 0.227s

OK
INFO     Start to render Html report ...
INFO     Generated Html report: /Users/fangzimo/Downloads/test/demo/reports/1569234320.html

没问题,成功,鼓掌!

但好像工作中并没有这么简单. 通常会遇见的问题:

  1. 接口中的参数需要依赖其他接口返回
  2. 测试用例需要数据驱动,通常是一组数据
  3. 需要对接口做类似等待一段时间再请求的控制干预等操作

这些问题是很容易出现的,一般使用的自动化框架,大多基于写代码逻辑实现,灵活度较高. 这些问题相对容易解决.但是基于配置的,灵活度就有很大的牺牲,更严格的定义好了玩法和规则, 之中的取舍可见设计者对测试自动化的理解, 可以看看设计者的博客,有设计的心路历程,对于上述问题,作者最后给出了较为优雅的解决方式.

  1. 接口中的参数需要依赖其他接口返回
    通过配置文件中对变量的支持来实现
    extract 提取变量
    variables 定义变量
  2. 测试用例需要数据驱动,通常是一组数据.
    参数化数据驱动方式解决
    通过配置参数为列表的方式实现
    通过文件cvs的格式配置实现
  3. 需要对接口做类似等待一段时间再请求的控制干预操作
    实现了hooks机制,可以在请求前和请求后调用钩子函数
    setup_hooks 在HTTP请求发送前执行hook函数,主要用于准备等工作
    teardown_hooks 在HTTP请求发送后执行hook函数,主要用户测试后的清理等工作

4. 与Unittest+Request+HTMLRunner框架对比

最后我们来看看这个框架的优缺点,到底适不适合我们呢?

HttpRunner

  1. 优点
  • 基于YAML/JSON格式,专注于接口本身的编写
  • 接口编写简单,容易上手,对代码编写能力要求较低
  • 生成测试报告,可以自动生成测试报告,框架自带的测试报告模板基本满足需求,支持自定义测试报告的模板
  • 接口录制功能,操作简单,只需3步即可完成测试,对于较为简单的场景尤其方便
  • 分层机制,适合冒烟流程测试,无需重复编写接口,只要根据需求灵活调用即可

 

  1. 缺点
  • 没有编辑器插件对语法校验,容易出错,HttpRunner 没有编辑器插件,本身就是一个配置文件,所以只要是合法的YAML/JSON格式,就算写错了,也看不出来,只有运行起来才知道
  • 框架推出时间相对较短,官方文档没有特别详细的说明,且网上资料相对其他主流测试框架较少
  • 扩展不方便,数据驱动需要依赖其他接口返回,且有先后顺序,这个比较麻烦,暂时框架不支持很优雅的解决这种情况.可以通过分步来解决这个问题
    • 首先数据驱动可以通过测试用例集的定义方式实现
    • 调用有序这个问题,就只能通过分拆测试用例,定义两个数据集,然后通过python脚本来控制先后执行顺序
    • 由于用例的数据导出只能在一个测试周期中,所以我们还要解决测试数据传递的问题
    • 通过写入文件的方式解决.接口返回的测试数据写入文件,然后需要的地方通过读取文件的方式读回数据

这样可以解决这个问题,但是很繁琐,这样的情况多了以后 项目大了,就会变得很凌乱了,和我们的预期也并不相符

Unittest+Request+HTMLRunner

  1. 优点
  • 足够灵活强大!只要你懂Python开发,想怎么玩就怎么玩
  • 支持分层测试、数据驱动、测试报告、集成CI等
  • 支持自动发送邮件、定时任务等
  • 支持读取excel文件的测试数据,能够组织多个用例去执行
  • 提供丰富的断言方法等
  1. 缺点
  • 有一定的学习成本,对于代码编写能力要求较高,不易上手

 

总结来说:

HttpRunner 清晰的配置文件,对于不复杂项目测试需求基本都能满足.实际调试中,错误提示不够明显, 最后在利弊之间权衡, 个人感觉HttpRunner不太适合大型的逻辑复杂的项目.本来简单的特性反而成了制约的弊端.

HttpRunner文档写的很详细,实际使用的时候一定要多阅读.作者博客也非常值得拜读,能更加深入的理解和学习作者的设计.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值