ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python+Pytest自动化测试实战:从环境搭建到接口框架落地

Python+Pytest自动化测试实战:从环境搭建到接口框架落地 这两年软件测试的岗位要求肉眼可见地变了光会点功能测试、写几条Excel用例已经不够看。翻翻招聘JD十条里有八条写着“熟悉Python”“掌握Pytest自动化测试框架”。不少干了三五年的功能测试朋友来找我聊说看到代码就头大不知道从哪下手。其实PythonPytest这套组合恰恰是所有自动化测试方向里最适合半路出家的人切入的路径——语法简单、生态成熟、社区资料多你不用啃完一整本编程书就能写出能跑的接口用例。这篇内容就是围绕Python和Pytest这套技术栈从环境搭建、框架核心用法、项目实战到面试和工作里的高频问题一条线捋下来。适合刚转行测试、正在自学自动化、以及简历上写着“熟悉Pytest”但心里没底的朋友。我把实际项目中踩过的坑、排查问题的思路、还有面试官真正会问的东西都揉进来你照着走一遍基本就能在自己项目里跑起来了。1. 为什么软件测试都在推PythonPytest而不是别的1.1 技术选型背后的真实逻辑很多人一上来就纠结到底学Java还是Python用JUnit还是Pytest我先说结论——如果你不是在大厂核心测试框架组做底层开发PythonPytest足够覆盖90%的日常测试场景而且是投入产出比最高的组合。Java系测试框架TestNG、JUnit不是不好但有个现实问题Java语言本身的学习曲线比Python陡峭不少光一个环境变量、类加载机制就劝退一批测试转行的朋友。而Python的语法接近自然语言写出来的用例可读性极强测试团队里每个人都看得懂。我见过不少测试小组用Python写的用例产品经理都能帮着评审。再一个很现实的原因现在接口测试、UI自动化、数据校验这些场景Python的三方库几乎都是标配。Requests、Selenium、Appium、Allure全部对Python支持最友好。你选Python相当于用最小成本接入了整个测试生态。至于为什么Pytest而不是Python自带的unittest——用一句话说unittest是“能跑”Pytest是“好用”。具体差在哪我列个表对比维度unittestpytest用例编写必须继承TestCase类方法名格式固定普通函数即可用assert原生断言断言方式必须用assertEqual、assertTrue等一堆API直接Python assert pytest.raisesFixture机制setUp/tearDown作用域固定装饰器conftest作用域灵活可配参数化写法繁琐可读性差pytest.mark.parametrize一行搞定插件生态官方为主1200插件Allure、xdist随便接断言失败重跑不支持pytest-rerunfailures插件完美支持你光看“断言失败重跑”这一条就行了。接口测试里有大量偶发超时、网络抖动导致的用例失败用unittest你只能手动重跑或者写循环重试而Pytest加个参数就搞定。这就是框架层面的代差。1.2 Pytest到底解决了测试里的什么问题自动化测试真正落地的时候你会发现难点从来不是“会不会写代码”而是怎么组织用例、怎么管理数据、怎么让用例稳定、怎么出报告。Pytest这套框架设计之初就是冲着解决这些问题去的。第一个是用例发现和收集。Pytest默认按“test_.py”或“_test.py”的文件名规则自动递归收集目录下所有用例你不用手动去注册什么TestSuite。配合-k参数可以做关键字过滤配合-m参数可以按标记mark过滤。这在大型项目里太重要了——几千条用例我只想跑登录相关的一条命令就搞定。第二个是fixture共享机制。这是Pytest的核心王牌。你要在每条用例前初始化数据库、创建测试用户、清缓存不需要每个文件写一遍setUp。conftest.py里定义一次fixture全项目随便用。作用域还能配function、class、module、session灵活得要命。第三个是插件体系。Pytest-alure做漂亮的可视化报告pytest-xdist做分布式并发执行pytest-rerunfailures做失败重跑pytest-assume做断言不中断pytest-ordering控制用例执行顺序。这些插件大多数是配置一下就用你在unittest里要实现同样的功能得自己写不少代码。把这些串起来看Pytest不是一个“能跑用例”的玩具框架而是一套完整的产品化解决方案。这也是为什么现在企业招自动化测试JD里都直接写Pytest而不是unittest的原因——用人方要的是能直接落到项目里的产出不是让你来了现学框架怎么用。2. 环境搭建与第一个测试用例先把地基打好2.1 Python安装与环境配置的避坑指南虽然题目说的是搜索热词但Python安装确实是很多人第一步就翻车的地方。我见过太多人因为环境变量没配好卡在“pip不是内部或外部命令”这一关折腾一下午就放弃了。这里把最稳的路径给你。去Python官网下载安装包装的时候有一个非常关键的勾选项——“Add Python to PATH”默认是没勾上的必须手动勾选。这个选项的意思是自动把Python的命令行工具加入系统环境变量没勾的话你后面在CMD里敲python、pip全部报“不是内部或外部命令”。装完验证方式很简单WinR打开CMD输入python --version pip --version两条命令都有版本信息输出环境就OK了。如果你用的是Windows 10以上系统还可以在Microsoft Store里直接搜Python安装这种方式会自动配置好环境变量对小白最省事但注意商店版更新相对滞后装完后建议立刻升级pippython -m pip install --upgrade pip再安装Pytest框架pip install pytest验证一下pytest --version看到版本号就说明框架装好了。顺带说一句如果你在的公司用的是Linux服务器大概率自带了Python 3直接用apt或yum装pip然后同样的命令装pytest就行。别在这步追求什么“最新版”稳定够用即可。2.2 手把手写第一个Pytest用例环境好了立刻写第一条用例。新建一个文件比如test_login.py输入def test_login_success(): username admin password admin123 expected_token abc123xyz actual_token mock_login(username, password) assert actual_token expected_token, 登录成功后返回的token与预期不符 def mock_login(user, pwd): if user admin and pwd admin123: return abc123xyz return None在文件所在目录打开终端运行pytest test_login.py -v你会看到Pytest自动收集到了两个test开头的函数一个用例通过另外一个用例的结果详细显示在控制台。这就是最朴素的自动化测试用例构造数据 → 执行操作 → 断言结果。你没继承任何类没调用任何固定API写的就是普通的Python函数Pytest自动发现了它们。有人可能会问为什么函数名必须是test开头这是Pytest默认的用例收集规则——以test_开头的函数或以Test开头的类。当然你可以改通过pytest.ini配置文件自定义python_functions参数但日常项目里完全没必要动它统一命名规范反而是件好事。2.3 pytest.ini配置文件里的门道项目稍微大一点你就不想每次运行都敲一堆参数了。这时候在项目根目录建一个pytest.ini文件[pytest] testpaths ./testcases python_files test_*.py python_classes Test* python_functions test_* addopts -v -s --tbshort markers smoke: 冒烟测试用例 regression: 回归测试用例 timeout: 超时相关用例这个配置文件起了这么几个作用testpaths指定Pytest去哪里找用例避免跑到venv虚拟环境里瞎扫。python_files/classes/functions定义用例收集规则一般保持默认就行。addopts每次运行都带的参数-s表示打印print输出不写这个你print的东西在测试里看不到--tbshort表示失败时展示简短的traceback控制台看起来干净很多。markers注册自定义标记mark注册过的标记用起来不会报警告后面做冒烟回归分离就用它。另外提醒一点创建的pytest.ini文件如果放在项目根目录Pytest会自动识别不需要额外指定参数。但注意pytest.ini文件需要以UTF-8编码保存别用记事本另存成带BOM的UTF-8格式否则容易出现乱码或解析问题。3. Pytest核心功能拆解断言、FIXTURE、参数化、标记3.1 断言不只是“相等”这么简单新手写断言最容易踩的坑就是只用assert a b。实际工作里断言的花样多得是。我给你罗列几个高频场景import pytest def test_assert_api_response(): # 场景1: 断言接口返回状态码 status_code 200 assert status_code 200 # 场景2: 断言响应时间小于指定值(单位ms) response_time 356 assert response_time 500, f接口响应耗时{response_time}ms超过500ms阈值 # 场景3: 断言JSON响应里的某个字段 response_json {code: 0, data: {name: 张三, age: 18}, msg: success} assert response_json[code] 0 assert response_json[data][name] 张三 assert response_json[msg] success注意第二个断言的写法断言里面直接拼接了错误提示信息这个习惯强烈建议养成。用例一旦在几百条用例的回归执行中失败带有详细上下文信息的失败报告能帮你省掉大量排查时间。还有两个高频断言技巧。第一个是断言某个异常被抛出用pytest.raisesdef test_raises_exception(): with pytest.raises(ValueError) as exc_info: int(abc) assert invalid literal in str(exc_info.value)第二个是断言浮点数的近似值用pytest.approxdef test_approx(): assert (0.1 0.2) pytest.approx(0.3)这两个小技巧看起来不起眼面试的时候拿出来很加分工作里也是实实在在能用到。3.2 Fixture机制测试数据准备的核心武器Fixture这词听着高大上其实就是“测试之前准备的东西”。比如你要测一个用户列表接口前提条件是有三个测试用户存在你要测订单支付流程前提是得先登录拿token。这些都是fixture的活。最基础的用法是函数级别的import pytest pytest.fixture def test_user(): 准备一个测试用户 user {username: 测试用户, age: 20} return user def test_get_user_info(test_user): assert test_user[username] 测试用户进阶用法是conftest.py共享fixture。新建一个conftest.py文件import pytest pytest.fixture(scopesession) def app_token(): 整个测试会话只登录一次返回token print(开始获取全局token...) token token_2024_session_001 yield token print(测试结束清理token)注意两个细节一个是scopesession表示这个fixture在整个测试会话期间只创建一次这对登录这类耗时操作极有价值另一个是yield代替returnyield之前的代码在用例执行前跑yield之后的代码在用例执行后跑——相当于setup和teardown合在了一块这个写法叫fixture的终结器做资源清理特别好用。使用的时候直接把fixture的名字写进测试函数的参数里Pytest会自动注入def test_order_query(app_token): headers {Authorization: app_token} # 其他断言逻辑...很多新手搞不懂fixture和普通函数的区别我打个比方普通函数是你主动调用fixture是框架帮你调用。你只需要声明“我需要这个依赖”Pytest在运行用例前自动帮你准备好用例结束后再帮你清理。这种“依赖注入”的思想贯穿整个Pytest理解了这个你就跨过了Pytest最难的坎。3.3 参数化同样的测试逻辑不同测试数据接口测试里最常见的场景就是同一个接口你需要用正常参数、异常参数、空参数、超长参数各跑一遍。如果每个写一条用例代码冗余到没法维护。Pytest的pytest.mark.parametrize就是干这个的import pytest # 参数化登录接口测试 pytest.mark.parametrize(username,password,expected_code, [ (admin, admin123, 0), # 正常登录 (admin, wrongpass, 1001), # 密码错误 (, admin123, 1002), # 用户名为空 (admin, , 1003), # 密码为空 (admin, None, 1003), # 无密码 ]) def test_login_parametrize(username, password, expected_code): result login_api(username, password) assert result[code] expected_code一条用例函数五组测试数据跑起来就是5条测试用例。Pytest执行的时候会把每一组参数组合展示为独立的用例ID比如test_login_parametrize[admin-admin123-0]失败了你能精准定位到是哪一组数据出了问题。更进阶的是参数化组合多个fixturepytest.mark.parametrize(platform, [android, ios, web]) pytest.mark.parametrize(role, [vip, normal]) def test_client_login(platform, role): assert check_client(platformplatform, rolerole) True这样会组合出3×26条用例覆盖不同端的不同角色登录场景。数据量还小的时候你可能觉得没什么等你维护几百上千条用例的时候参数化就是节省代码量的利器。3.4 Mark标记冒烟、回归、跳过一份代码三种跑法测试用例跑多了自然产生分类需求冒烟测试只跑核心流程、回归测试全量跑一遍、跳过某些已知问题用例、或者跑某类特殊情况用例。这些都是mark干的活。import pytest pytest.mark.smoke def test_smoke_login(): 冒烟用例登录必须能通 pass pytest.mark.regression def test_regression_order(): 回归用例订单流程 pass pytest.mark.skip(reason已知性能问题修复中) def test_performance_bug(): pass运行的时候配合-m参数# 只跑冒烟 pytest -m smoke # 跑回归跳过冒烟 pytest -m regression and not smoke # 用配置文件里的标记名 pytest -m regression-m参数支持Python逻辑表达式and、or、not都能用组合起来就能灵活控制每次跑什么用例。这就是为什么前面配置文件里我特意加了markers注册的配置——不注册的话Pytest会给你打warning警告。mark还有一个很有用的变体是pytest.mark.xfail用于标记“预期失败”的用例——比如这个bug还没修好但已知原因先标记上用例fail的时候不会算整体失败bug修好后自动变成通过。这在做长期项目迭代的时候非常实用。4. 项目实战从零搭一个接口自动化测试框架4.1 项目目录结构与场景设计前面讲了那么多零散的机制现在用一个实际项目把它们串起来。假设我们要测一个商城系统的核心接口登录、商品列表、购物车、下单。项目结构这样规划auto_test/ ├── pytest.ini ├── requirements.txt # 依赖清单 ├── conftest.py # 全局fixture ├── config/ │ └── settings.py # 环境配置、URL、超时时间 ├── utils/ │ ├── http_client.py # 封装requests请求 │ ├── log_util.py # 日志封装 │ └── read_data.py # 读取参数化数据 ├── testcases/ │ ├── test_login.py # 登录用例 │ ├── test_goods.py # 商品列表用例 │ ├── test_cart.py # 购物车用例 │ └── test_order.py # 下单用例 └── report/ # 测试报告输出目录这套结构是我自己经历了三个项目后沉淀下来的经验。核心思想是分层用例层放业务逻辑工具层放通用能力配置层放环境参数层与层之间通过fixture和函数调用来交互。新人最容易犯的错是把代码都堆在一个文件里一开始项目小没事用例超过50条就完全失控了。场景设计要考虑的是自动化用例不是把所有接口堆上去就完事你要规划测试场景的覆盖。核心原则是“业务链路优先单接口覆盖为辅”。什么意思比登录、浏览商品、加购、下单、支付这一条完整链路要比单独测五个接口价值高得多因为它更贴近用户真实操作。我的做法是设计1条核心正向链路3-5条异常分支链路再把每个接口的参数边界单独用参数化补足。4.2 核心代码实现conftest与HTTP请求封装先写全局conftest.pyimport pytest import requests from config.settings import BASE_URL, TOKEN_EXPIRE_TIME pytest.fixture(scopesession) def base_url(): 全局接口基础地址 return BASE_URL pytest.fixture(scopesession) def auth_token(base_url): 测试会话级登录获取全局token login_data {username: admin, password: admin123} resp requests.post(f{base_url}/api/login, jsonlogin_data) assert resp.status_code 200 token resp.json()[data][token] yield token # 模拟登出清理 requests.post(f{base_url}/api/logout, headers{Authorization: token}) pytest.fixture(scopefunction) def auth_headers(auth_token): 每个函数级别生成带token的请求头 return {Authorization: fBearer {auth_token}, Content-Type: application/json}再封装一个简单的HTTP客户端。为什么要自己封装而不直接用requests因为直接写requests库你需要在每条用例里自己处理异常、打印日志、统一返回格式代码重复度高。封装一层后用例代码会干净很多import requests import time class HttpClient: def __init__(self, base_url, headersNone, timeout10): self.session requests.Session() self.base_url base_url self.timeout timeout if headers: self.session.headers.update(headers) def request(self, method, path, **kwargs): url f{self.base_url}{path} start time.time() try: resp self.session.request(method, url, timeoutself.timeout, **kwargs) elapsed_ms (time.time() - start) * 1000 response_body resp.json() if resp.headers.get(Content-Type, ).find(json) 0 else resp.text return {status_code: resp.status_code, body: response_body, elapsed_ms: elapsed_ms} except requests.exceptions.Timeout: return {status_code: -1, body: {error: timeout}, elapsed_ms: self.timeout * 1000} except Exception as e: return {status_code: -1, body: {error: str(e)}, elapsed_ms: 0}这个方法返回统一结构的字典状态码、响应体、耗时时长。这样用例里你就可以统一断言这三个字段也在一定程度上模拟了真实业务对接口的关注维度——不只关注返回对不对还要关注快不快。4.3 测试用例完整示例从登录到下单有了前面的conftest和HttpClient写用例就很清爽了import pytest from utils.http_client import HttpClient pytest.fixture(scopefunction) def client(auth_headers): return HttpClient(base_urlhttps://api.example.com, headersauth_headers) class TestOrderFlow: pytest.mark.smoke def test_login_and_get_token(self, base_url): 正向链路第一步登录成功获取token client HttpClient(base_urlbase_url) result client.request(POST, /api/login, json{username: admin, password: admin123}) assert result[status_code] 200 assert result[body][code] 0 assert result[body][data][token] is not None pytest.mark.parametrize(product_id,expected_status, [ (1001, 200), (2003, 200), (9999, 404) ]) def test_get_goods_detail(self, client, product_id, expected_status): 商品详情接口参数化用例 result client.request(GET, f/api/goods/{product_id}) assert result[status_code] expected_status pytest.mark.smoke def test_add_cart_and_create_order(self, client): 正向链路第二步加购后创建订单 add_result client.request(POST, /api/cart, json{goods_id: 1001, count: 2}) assert add_result[status_code] 200 assert add_result[body][code] 0 order_result client.request(POST, /api/order/create, json{ goods_id: 1001, count: 2, payment: balance}) assert order_result[status_code] 200 assert order_result[body][data][order_no] ! 注意几个细节用例写在class里类名以Test开头方法名以test_开头Pytest同能自动发现。类内部定义了一个clientfixture作用域是function每条用例都会重新创建HTTP会话避免用例间数据共享导致干扰。参数化商品ID时故意给了一个9999不存在的商品期望状态码是404这样把“异常分支”也覆盖到了。跑一下看看pytest testcases/ -v -m smoke --tbshort如果你已经安装了pytest-html再加个--htmlreport/report.html --self-contained-html参数就能生成自带CSS的HTML报告方便给团队看。4.4 集成Allure报告让测试产出看得见说到报告现在企业里用Allure的越来越多因为它的报告比pytest-html美观太多而且支持历史趋势、用例分类、缺陷关联。接入步骤# 安装两个库 pip install allure-pytest # 运行并把结果导出为allure数据 pytest testcases/ --alluredir./report/allure-results # 查看报告需要先安装Allure命令行工具macOS: brew install allure allure serve ./report/allure-results运行后Allure会生成一个Web页面里面有用例通过率、耗时分布、每个用例的完整日志和截图配合Selenium时截图跟踪特别有用。我这几个项目用下来Allure报告给产品方和领导看说服力比干巴巴的txt日志强太多。有个细节要提醒Allure的数据依赖用例执行时正确添加步骤描述。你可以在用例里这样做import allure allure.title(登录接口正向用例) allure.description(验证正确用户名密码可以返回token) allure.severity(allure.severity_level.CRITICAL) pytest.mark.smoke def test_login_success(): ...这样Allure报告里每一条用例都有清晰的标题、描述和优先级。团队review起来一目了然接口自动化测试在团队里也就更有存在感。5. 常见问题与排查技巧实录5.1 环境与执行层面高频坑先说几个我踩过无数次的坑能帮你省下大量排查时间。问题一运行pytest命令提示“不是内部或外部命令”。这个基本是Python环境变量没配好回到2.1部分重新检查PATH。也有一种情况是你用了Anacondaconda环境下安装的pytest和系统的python不在同一个环境运行命令的时候一定要先确认当前激活的是哪个Python环境。问题二控制台里print的内容看不到。Pytest默认捕获stdout你print的东西不会显示。加-s参数或者在pytest.ini的addopts里加上-s。日常开发我建议直接加上配好了永远不用再想这个事。问题三用例执行顺序随机依赖顺序的用例经常失败。Pytest默认按文件内声明顺序执行但跨文件的顺序不一定。如果你确实有顺序依赖你有几种处理方式用pytest-ordering插件的pytest.mark.run(order1)或者更推荐的方式——用fixture把前置条件抽出来从根上消除顺序依赖。顺序依赖本质上是测试耦合长期看一定要改掉。问题四明明改了代码跑起来还是旧逻辑。Python的pyc缓存捣的鬼。在项目根目录执行pytest --cache-clear或者直接删掉__pycache__文件夹。尤其在团队协作、多分支切换的时候这个坑很容易出现。5.2 断言与用例设计层面高频坑断言不写错误信息。这看起来是小事但用例失败后定位成本是几何级数上升。统一格式assert result[code] 0, f接口返回code{result[code]}期望0完整响应: {result[body]}。失败时一眼就看出问题在哪。一条用例里塞太多断言。一条用例负责一个核心逻辑如果一条用例断了你根本分不清是前面的断言先挂的还是后面的问题。建议一条用例里的断言数量控制在3-5条以内且围绕同一业务场景。只测正向不测异常。我评审过不少测试团队的用例90%是“输入正确参数断言返回成功”。真实用户操作里错误输入、参数缺失、超时是家常便饭。用例设计时至少保持三分之一是异常场景和边界场景这个比例覆盖下来线上事故能减少很多。Fixture作用域没选对。这是最隐蔽的性能杀手。一个scopefunction的登录fixture如果被测系统登录很慢几百条用例全量跑一遍光登录时间就耗掉几个小时。分清哪些数据能复用token、缓存、基础数据哪些必须隔离订单状态、用户状态合理配置session、module、function三级作用域全量执行耗时能降一个数量级。参数化数据直接写在装饰器里数据量大就难维护。超过十组参数建议把数据抽到YAML、JSON或Excel文件里用工具读取传入。比如用utils/read_data.py读取JSON参数化数据改测试数据不需要改代码测试人员和开发协作更顺畅。6. 面试和真实工作里的经验6.1 面试官真正想问的核心点搜索热词里有“软件测试面试题”“软件测试八股文面试题”关于Pytest这块我作为过来人给你圈几个真正会问的点。第一个肯定是fixture的作用域和yield。面试官想确认你是背了概念还是真用过。你得能说出session、module、class、function四级的区别还能说出yield前后的代码分别在什么时候执行以及多fixture嵌套时的执行顺序。能举例子说明自己在项目里把登录token定义成了session级省了多少时间这都是加分项。第二个是用例怎么管理。对应的话题包括conftest.py的作用、mark标记怎么用、参数化怎么组织数据、接口依赖怎么处理。面试官问你“遇到A接口依赖B接口的返回值怎么办”你得说出两种方案一种是fixture封装B接口返回把依赖注入进用例另一种是session级保存B的结果直接调用。说出来这两种面试官就知道你是真做过项目的人。第三个是稳定性与效率。追问频率很高的是你的用例在跑的时候flaky怎么办几百条用例怎么跑得快答案对应pytest-rerunfailures插件做失败重跑、pytest-xdist做分布式执行控制在具体场景下怎么配置、最大重跑多少次、并行度怎么定。说出参数配置加上一句“我们对关键用例设置重跑1次其他不重跑避免掩盖真实问题”这个深度远超背题党。6.2 给你的自动化之路建议最后分享几条过来人的经验。第一条从接口测试切入。如果公司没有自动化基础不要一上来搞UI自动化Web端或App端UI自动化坑太多——页面元素定位、等待机制、浏览器兼容随便一个就能耗掉你几周时间。接口自动化测试投入产出比极高后端接口相对稳定用例可复用性强先跑起来再把精力延伸出去。第二条在项目里争取一个“小切口”。别求一步到位全量自动化。挑一条稳定的核心业务链路比如登录→查列表→提交流程做成第一条自动化用例跑起来给团队看。有了这个落地实例后面扩展就是水到渠成的事。第三条代码能力和业务理解都重要。光会Python语法能跑通用例不代表能设计出有价值的自动化测试。你得多想“业务上哪些逻辑最容易出错”“线上出现过什么类型的问题”把这些沉淀成用例。说白了自动化测试最大的价值藏在“测试设计”里代码是实现工具而已。第四条简历上写“熟练使用Pytest”之前先自己确认能回答几个问题。fixture怎么定义conftest.py有几个参数化的装饰器叫什么怎么生成Allure报告每条都能不假思索答上来面试再谈“熟练”。答不上来就先补再说因为面试官几乎肯定会追问两句答不圆比不写还减分。我在几个项目里从零搭过Pytest框架也面试过不少候选人。我的体会是Pytest这个东西并不难难的是把测试设计思路、代码组织能力和业务理解串成一个整体。别急着追新框架先把这篇文章里提到的点一个个在自己电脑上敲一遍从一个真实项目的小模块开始练手比任何八股文都管用。最后再送一个小技巧写用例之前先把被测系统的接口文档摸透。一份清晰的接口文档参数、返回值、错误码能让你少走很多弯路。要是公司接口文档不全就自己用Requests跑一遍抓真实返回互动着来。测试这个行当永远是多动手才会的东西。
返回列表