pytest小白学习笔记(一):pytest框架结构和fixture/mark装饰器的基本原理和应用

Lamaara ·
更新时间:2024-09-21
· 799 次阅读

记录笔记的原因是,我太浮躁了,这知识它不进脑子啊。本身pytest测试框架概念又多,自己静不下心,之前看了点视频就想上手练了,结果发现在编写实际用例时,问题很多,用fixture带参数传递的时候总有一些问题,可见还是处于一知半解迷迷糊糊的状态。所以决定还是走慢一点,先弄懂最基本的原理,做些简单的模拟,为了强迫自己全神贯注,就记录下来了。
我这个人永远三分钟热度,之前摸索pygame也是,练习一个项目练了一半扔了。可能这就是为啥我做啥啥都不专的原因吧。希望这次能坚持下去,记录笔记也是监督我自己。
Only step by step,the work will be down .

基本概念和应用

1.pytest测试框架结构介绍
介绍pytest框架结构之前先说一下传统的单元测试(setup/teardown),setup和teardown是属于unittest测试框架的概念(unitttest我没怎么接触,一般开发都会用它进行单元测试)。顾名思义,setup是前置条件,在每个用例前执行一次,比如说在开始某个测试用例之前打印一些信息;而teardown就是后置条件,即在每个用例后执行一次,比如说当测试用例执行完后打印一些信息。

而pytest也有类似的语法。并且种类更多,更加灵活。按照用例运行级别可以分为以下几类:
模块级(setup_module/teardown_module)模块始末,全局的(优先级最高)
函数级(setup_function/teardown_function)只对函数用例生效(不在类中)
类级(setup_class/teardown_class)只在类中,前后运行一次(在类中)
方法级(setup_method/teardown_method)开始于方法始末(在类中)
类里面的(setup/teardown)运行在调用方法的前后

-它们执行用例优先级顺序是什么呢?
简单用代码验证下,为了显示出它们执行的优先级顺序,我打乱了顺序:

__author__ = 'YiLin' #coding=utf-8 import pytest def teardown_module(): print("this is teardown_module") def setup_module(): print("this is setup_module") def setup_method(): print("this is external setup_method") def teardown_method(): print("this is external teardown_method") def setup_function(): print("this is external setup_function") def teardown_function(): print("this is external teardown_function") def test_login(): print("this is external method") class Testdemo3(): def setup_class(self): print("this is internal setup_class") def teardown_class(self): print("this is internal teardown_class") def setup_method(self): print("this is internal setup_method") def teardown_method(self): print("this is internal teardown_method") def setup(self): print("this is internal setup") def teardown(self): print("this is internal teardown") def test_one(self): print("开始执行 test_one方法") x = 'this' assert 'h'in x def test_two(self): print("开始执行 test_two方法") a = "hello" assert 'e' not in a def test_three(self): print("开始执行 test_three方法") a = "hello" b = "hello world" assert a not in b if __name__ == "__main__": pytest.main()

结果如下:

test_setup_teardown.py::test_login this is setup_module this is external setup_function PASSED [ 25%]this is external method this is external teardown_function test_setup_teardown.py::Testdemo3::test_one test_setup_teardown.py::Testdemo3::test_two this is internal setup_class this is internal setup_method this is internal setup PASSED [ 50%]开始执行 test_one方法 this is internal teardown this is internal teardown_method this is internal setup_method this is internal setup FAILED [ 75%]开始执行 test_two方法 test_setup_teardown.py:57: AssertionError this is internal teardown this is internal teardown_method this is internal setup_method this is internal setup FAILED [100%]开始执行 test_three方法 test_setup_teardown.py:58 (Testdemo3.test_three) test_setup_teardown.py:63: AssertionError this is internal teardown this is internal teardown_method this is internal teardown_class this is teardown_module

可见它们的优先级执行顺序是:
(PS:TestCase就是测试用例,对应代码里的test_one,test_two,test_three方法)
整个模块:
setup_moudule > setup_function > teardown_function > setup_class > setup_method > setup >TestCase> teardown > teardown_method > teardown_class>teardown_moudule

类里面
setup_class > setup_method > setup >TestCase> teardown > teardown_method > teardown_class

但有些时候,如果我只想选择性的执行某些用例,那怎么办呢?比如说测一个登录功能时:我想让用例1需要先登录,用例2不需要登录,用例3需要登陆,显然setup和teardown就无法实现这个要求了,因为setup和teardown是按顺序执行的,每个用例都会被执行到。所以pytest引入了fixture,通过这个神奇的函数,它可以在指定的位置(比如函数啦,类啦,模块啦等等)被激活。

2.fixture的介绍
fixture的目的是提供一个固定基线,在该基线上测试可以可靠地和重复地执行。
它的主要功能:
1.配置测试前用例的初始状态,也就是预置条件,有点像 setup
2.传入测试中的数据
3.为批量测试提供数据

(1)如何使用fixture呢?
用法:
在你想要添加fixture的位置前面,比如某个函数前面加:
@pytest.fixture(),这就是一个pytest的装饰器了

(我一开始对装饰器这个词一脸懵逼,后来明白了,所谓装饰装饰,就是在不修改原有函数代码的情况下,额外加一些功能,并且利于代码的复用。
举个例子,知名歌星张敬轩的金色话筒就是个装饰器,当他拿起这个话筒讲话时,他的音色就加了立体环绕声,也就是说本人音色还是那个音色,但是加了点特效;而装饰器的作用可以在增加张敬轩声音魅力的同时也能保住他自己本身的音色,如果哪天张敬轩他想清唱,扔了话筒就行了;但是如果张敬轩在此前为了让自己的声音变得更有魅力,而去做手术改变声带(直接修改原来的函数代码),那他想再恢复自己本身的音色就很困难啦。)

(2)fixture函数的执行顺序是什么呢?
pytest 首先会检测到 fixture 函数, 并在运行测试用例之前先执行fixture,fixture既可以完成一些测试之前的工作(初始化数据),也可以返回数据(测试数据)给测试用例。

话不多说,先简单模拟一下我上段所说的,如何实现“”有的用例需要登录才能执行,有的用例不需要登录“”这个场景。
步骤:
导入pytest
在登录的函数上面加@pytest.fixture
在要使用的测试方法中传入(登录函数名称),就先登录再执行测试方法
不传入的就不登录直接执行测试方法

__author__ = 'YiLin' #coding=utf-8 import pytest def test_case1(login): print("test_case1,要登录") pass def test_case2(): print("test_case2,不需要登录") def test_case3(login): print("test_case3,要登录") pass @pytest.fixture() def login(): print("This is login method") if __name__=="__main__": pytest.main()

结果如下:

test_fixture.py::test_case1 This is login method PASSED [ 33%]test_case1,要登录 test_fixture.py::test_case2 PASSED [ 66%]test_case2,不需要登录 test_fixture.py::test_case3 This is login method PASSED [100%]test_case3,要登录

由此可见,在test_case1和test_case2的测试用例之前都先执行了fixture(login方法),test_case3没有传入fixture,所以直接执行了测试用例。

(3)关于scope的参数说明
scope是fixture函数里的一个参数,默认为function。
scope=“function” 时,它的作用范围是每个测试用例来之前运行一次,而销毁代码(yield)在测试用例之后运行。

scope=“class” 时,如果一个class里面有多个用例都调用了一次fixture,那么此fixture只在此class里所有用例开始前执行一次。

scope=“module” 时,在当前.py脚本里面所有用例开始前只执行一次。

scope=“session” 时,可以跨.py模块调用的,也就是当我们有多个.py文件的用例的时候,如果多个用例只需调用一次fixture,那就可以设置为scope=“session”,并且写到conftest.py文件里。(下面会提到这个文件)

所以说fixture可以通过scope参数控制setup级别。

(4)conftest
设想一个场景:如果我与其他测试工程师合作一起开发时,公共模块要在不同文件中,要在大家都访问到的地方,那该怎么办呢?
引入conftest。conftest.py文件名称是固定的,pytest会自动识别该文件。放到项目的根目录下就可以全局调用了,如果放到某个package下,那就在该package内有效。因此有了这个文件,就能进行数据共享,并且它可以放在不同的位置起着不同的范围共享作用。比如说全局的配置和前期工作都可以写在这里,放在某个包下,就是这个包数据共享的地方。

话不多说,简单模拟下怎么应用:
步骤:
将登陆模块带@pytest.fixture写在conftest.py
conftest.py配置需要注意:conftest文件名是不能换的
conftest.py与运行的用例要在同一package下,并且有__init__.py文件
不需要import导入conftest.py,pytest用例会自动查找

(5)yield
接(4)部分,既然有setup作为用例之前的操作,用例执行完之后那肯定也有teardown操作。这里用到fixture的teardown操作并不是独立的函数,用yield关键字呼唤teardown操作。
设想一个场景:如果我已经可以将测试用例前需要额外执行的函数或依赖的包都解决了,那我测试方法后销毁清除的数据要如何进行呢?此时我需要的清除范围应该是模块级别的,也就是scope=module。
解决方法就是通过在同一模块加入yield关键字,yield是调用第一次的执行yield下面的语句返回。
话不多说,简单模拟下怎么应用:
步骤:
在@pytest.fixture(scope=module)
在登陆的方法中加yield,之后加销毁清除的步骤

__author__ = 'YiLin' #coding=utf-8 import pytest def test_login1(login): print("CASE1:yilin") pass def test_login2(login): print("CASE2:hins") pass # 如果第一个用例异常了,不影响其他的用例执行 def test_login3(login): raise NameError #模拟异常 @pytest.fixture(scope="module") def login(): print("pls input login name") yield print("调用teardown!") print("关闭登录界面") if __name__=='__main__': pytest.main()

结果如下:

test——scope.py::test_login1 pls input login name PASSED [ 33%]CASE1:yilin test——scope.py::test_login2 PASSED [ 66%]CASE2:hins test——scope.py::test_login3 FAILED [100%] test——scope.py:14 (test_login3) login = None def test_login3(login): > raise NameError #模拟异常 E NameError test——scope.py:16: NameError 调用teardown! 关闭登录界面

可见,当三个用例都执行完以后,才会调用一次yield后面的返回语句teardown作为结束。

另外我在第三个用例时,模拟了一个异常请求。
-在python里,模拟异常请求用raise,一旦执行了raise语句,raise后面的语句将不能执行。
-之所以我要模拟这个异常请求,是因为yield遇到异常时,是不影响它后面的teardown执行,运行结果互不影响。并且全部用例执行完之后,yield会呼唤teardown操作。
-但如果在setup就异常了,那么就不会去执行yield后面的teardown内容了
我把上面的py代码稍微修改下,在fixture下面加一个模拟异常:

@pytest.fixture(scope="module") def login(): #print("pls input login name") raise NameError yield print("调用teardown!") print("关闭登录界面") if __name__=='__main__': pytest.main()

结果如下(因为内容比较长,我只保留了第一个完整打印信息):

test——scope.py::test_login1 ERROR [ 33%] test setup failed @pytest.fixture(scope="module") def login(): #print("pls input login name") > raise NameError E NameError test——scope.py:32: NameError test——scope.py::test_login2 ERROR [ 66%] test setup failed test——scope.py:32: NameError test——scope.py::test_login3 ERROR [100%] test setup failed test——scope.py:32: NameError

可见,yield后面的teardown内容了没有被执行
不过使用yield这种方法没有返回值,如果希望返回,使用addfinalizer。

(6)autouse
其实我觉得学习的时候,思考这些用法的时候代入实际场景,是高效的。所以问题又来了,如果在实际测试中,我不想原测试方法有任何改动,并且yield后置要求全部都能自动实现在每个用例后,没特例,也都不要返回值时可以选择自动应用,怎么办?
解决:使用fixture中参数autouse实现
步骤:
在方法上面加@pytest.fixture(autouse=True)

__author__ = 'YiLin' #coding=utf-8 import pytest def test_search1(open): print("test_search1") raise NameError pass def test_search2(open): print("test_search2") pass def test_search3(open): print("test_search3") pass @pytest.fixture(autouse=True) def open(): print("open browser") yield print("执行teardown!") print("最后关闭浏览器") if __name__=="__main__": pytest.main()

结果如下:

test_auto.py::test_search1 open browser FAILED [ 33%]test_search1 test_auto.py:11 (test_search1) open = None def test_search1(open): print("test_search1") > raise NameError E NameError test_auto.py:14: NameError 执行teardown! 最后关闭浏览器 test_auto.py::test_search2 open browser PASSED [ 66%]test_search2 执行teardown! 最后关闭浏览器 test_auto.py::test_search3 open browser PASSED [100%]test_search3 执行teardown! 最后关闭浏览器

可见,yield后置要求能够全部应用到每个测试用例中

(7)fixture-request传参
我在第二部分介绍fixture的时候,提到了它的三个主要功能,其中一个就是:传入测试中的数据。毕竟测试离不开数据,为了数据灵活,一般数据都是通过参数传的。
那么怎么传递参数呢?
fixture可以通过固定参数request传递。request就是我需要什么东西,用来接受参数的。它也代表 fixture 的调用状态。而request 有一个字段 param,可以使用类似@pytest.fixture(params=tasks_list)的方式,在 fixture 中使用 request.param的方式作为返回值供测试函数调用。其中 tasks_list 包含多少元素,该 fixture 就会被调用几次,分别作用在每个用到的测试函数上。(关于request的概念引用的这篇文章内容)
所以可步骤如下:
在fixture中增加@pytest.fixture(params=tasks_list)
在需要被调用的方法中,参数写request
在测试用例中传入fixture的函数名作为参数
返回 request.param

__author__ = 'YiLin' #coding=utf-8 import pytest def test_name1(name): print('test name is {}'.format(name)) @pytest.fixture(params=['lili','hins','kk']) def name(request): return request.param if __name__=='__main__': pytest.main()

结果如下:

test——scope.py::test_name1[lili] PASSED [ 33%]test name is lili test——scope.py::test_name1[hins] PASSED [ 66%]test name is hins test——scope.py::test_name1[kk] PASSED [100%]test name is kk

tasks_list 包含3个元素,fixture就被调用了3次。测试用例被执行了3次。

(8)mark-parametrize传参
为了搞清楚这两种传参的区别,我又啰嗦几句才能长记性。(7)提到的request传参,是在装饰器pytest.fixture里写入要测试的参数,fixture用request去接受这些参数,并用request.param返回这些参数,然后测试用例去调用fixture的函数名作为参数,拿到request.param的返回值,可见这仍然是属于对前置进行操作。(我个人认为是这样的,但凡是fixture里写入参数,函数或类里面方法直接传fixture的函数名作为参数,都属于前置传参。)
而使用装饰器@pytest.mark.parametrize( )修饰需要运行的用例,属于给测试用例传参,也就是说这个装饰器是在我测试用例之前定义的,而不是在我需要调用的方法前面定义。
使用方法是什么呢?
@pytest.mark.parametrize(‘参数名’,list)可以实现测试用例参数化
-第一个参数是字符串,多个参数中间用逗号隔开
-第二个参数是list,多组数据用元祖类型表示;传三个或更多参数也是这样传,list的每个元素都是一个元组,元组里的每个元素和按参数顺序一一对应。
比如说这样:
-传入一个参数:
-@pytest.mark.parametrize(‘参数名1’,[(参数1_data) ]进行参数化
-传两个参数:
-@pytest.mark.parametrize(‘参数名1,参数名2’,[(参数1_data[0], 参数2_data[1]),(参数1_data[0], 参数2_data[1])]) 进行参数化

举个简单的小栗子,
这里我用了assert断言,eval是python的内置函数,用来判断结果是否相等

__author__ = 'YiLin' #coding=utf-8 import pytest @pytest.mark.parametrize('test_input,expected',[('3+5',8),('2+5',7),('7*3',30)]) def test_eval(test_input,expected): assert eval(test_input)==expected

结果如下:

test_mark_paramize.py::test_eval[3+5-8] test_mark_paramize.py::test_eval[2+5-7] test_mark_paramize.py::test_eval[7*3-30] PASSED [ 33%]PASSED [ 66%]FAILED [100%] test_mark_paramize.py:3 (test_eval[7*3-30]) 21 != 30 Expected :30 Actual :21

再举个栗子,我不用内置函数,而是先定义一个函数,让我的测试用例去调用它
还是拿用户登录功能来说明:
先独立一个login函数,传入形参为:username,password
在需要调用login函数的测试用例前,写上pytest.mark.parametrize(“username,password”,[输入几组数据])
在测试用里调用login函数

#coding=utf-8 __author__ = 'YiLin' import pytest data = [("hins","123456"),("kk","")] def login(username,password): print("亲爱的{}".format(username)) print("您输入的密码为{}".format(password)) if password: print("{},恭喜你登陆成功".format(username)) return True else: print("{},登陆失败".format(username)) return False @pytest.mark.parametrize("username,password",data) def test_user1(username,password): results = login(username,password) assert results==True,"失败原因:密码为空"

结果如下:

test_mark_paramize.py::test_user1[hins-123456] test_mark_paramize.py::test_user1[kk-] PASSED [50%]亲爱的hins 您输入的密码为123456 hins,恭喜你登陆成功 FAILED [100%]亲爱的kk 您输入的密码为 kk,登陆失败 AssertionError: 失败原因:密码为空 False != True Expected :True Actual :False

-而元组里的每个元素可以是字符串,可以是列表,可以是字典。
比如说这样:
@pytest.mark.parametrize(‘请求方式,接口地址,传参,预期结果’,[(‘get’,‘https://junon.com.hk/’,’{“page”:1}’,’{“code”:0,“msg”:“成功”})’,(‘post’,‘https://junon.com.hk/’,’{“page”:2}’,’{“code”:0,“msg”:“成功”}’)])

而此前让我真正混乱的问题是,pytest.fixture和pytest.mark.parametrize这两个装饰器怎么搭配使用。而且pytest.mark.parametrize里面有个参数是indirect,在indirect=True和indirect=False不同的情况下,参数传递方式是不一样的。

其实说白了,当我使用pytest.fixture装饰器,用request默认参数去进行传参的时候,是已经把方法(比如login函数)放在前置里进行操作了。然后我在测试用例前去使用pytest.mark.parametrize装饰器时,就可以把login函数名“login”当做参数传入,再加上参数对应的数据;而不是像之前需要写明参数username,password。
而关于indirect的用法,我参考了这篇文章

当indirect=True
举个栗子:

#coding=utf-8 __author__ = 'YiLin' import pytest user_name=["hins","kk",""] @pytest.fixture(scope='module') def loginlogin(request): name = request.param print('英皇的大老板之一:{}'.format(name)) return name @pytest.mark.parametrize("loginlogin",user_name,indirect=True) def test_name(loginlogin): name = loginlogin print(name) assert name!="","失败原因,用户名为空" if __name__=="__main__": pytest.main()

结果如下:

test_verify_indirect.py::test_name[hins] test_verify_indirect.py::test_name[kk] test_verify_indirect.py::test_name[] 英皇的大老板之一:hins PASSED [ 33%]hins 英皇的大老板之一:kk PASSED [ 66%]kk 英皇的大老板之一: FAILED [100%]

当indirect=False,也就是indirect默认值,我把这个删掉就行,如下所示

#coding=utf-8 __author__ = 'YiLin' import pytest user_name=["hins","kk",""] @pytest.fixture(scope='module') def loginlogin(request): name = request.param print('英皇的大老板之一:{}'.format(name)) return name @pytest.mark.parametrize("loginlogin",user_name) def test_name(loginlogin): name = loginlogin print(name) assert name!="","失败原因,用户名为空" if __name__=="__main__": pytest.main()

结果如下:

test_verify_indirect.py::test_name[hins] test_verify_indirect.py::test_name[kk] test_verify_indirect.py::test_name[] PASSED [ 33%]hins PASSED [ 66%]kk FAILED [100%]

通过这两组对比可知,
当我indirect=True的时候,执行的时候会把login当做函数来执行,也就是login变为一个可执行的函数,再将params(user_name)当参数传入login函数中去执行,只要当其他变量被赋值为login的时候都满足这个执行方式。所以我在fixture函数里打印的“英皇的大老板之一“每次都会被执行到,执行数量会根据username里的参数数量决定,这里有三个用户名,所以被打印了3次。也就是我第(7)点提到的

在 fixture 中使用 request.param的方式作为返回值供测试函数调用。其中 tasks_list 包含多少元素,该 fixture 就会被调用几次,分别作用在每个用到的测试函数上

而当我indirect=False的时候,login就理解成一个变量,执行后的结果就是login=[“hins”,“kk”,""],它不会跑到login函数里去执行代码,仅仅引用request.param的参数,所以不会打印“英皇的大老板之一“。

所以结论可以是,当indirect=true的时候,将变量当做函数执行,当为false的时候,只是当做变量来引用了。

(9)mark.parametrize装饰器叠加
叠加的原因是因为当测试用例需要多个数据时,list一般使用嵌套序列(嵌套元组&嵌套列表)来存放测试数据,列表嵌套多少个多组小列表或元组,就能生成多少条测试用例。也就是测试组合。
(PS:@pytest.mark.parametrize,可以使用单个变量接收数据,也可以使用多个变量接收,同样,测试用例函数也需要与其保持一致。parametrize后面可以也传元组,可以传列表。)
举个栗子:

#coding=utf-8 __author__ = 'YiLin' import pytest @pytest.mark.parametrize('a',[(2),(3)]) @pytest.mark.parametrize('b',[(2),(3),(4)]) def test_foo(a,b): print('测试数据组合{},{}'.format(a,b)) if __name__=="__main__": pytest.main()

结果如下:

test_mark_paramize.py::test_foo[2-2] PASSED [ 16%]测试数据组合2,2 test_mark_paramize.py::test_foo[2-3] PASSED [ 33%]测试数据组合3,2 test_mark_paramize.py::test_foo[3-2] PASSED [ 50%]测试数据组合2,3 test_mark_paramize.py::test_foo[3-3] PASSED [ 66%]测试数据组合3,3 test_mark_paramize.py::test_foo[4-2] PASSED [ 83%]测试数据组合2,4 test_mark_paramize.py::test_foo[4-3] PASSED [100%]测试数据组合3,4

(10)mark中的skip和xfail
下面介绍引用自这篇文章
实际工作中,测试用例的执行可能会依赖于一些外部条件,例如:只能运行在某个特定的操作系统(Windows),或者我们本身期望它们测试失败,例如:被某个已知的Bug所阻塞;如果我们能为这些用例提前打上标记,那么pytest就相应地预处理它们,并提供一个更加准确的测试报告;

在这种场景下,常用的标记有:
skip:只有当某些条件得到满足时,才执行测试用例,否则跳过整个测试用例的执行;例如,在非Windows平台上跳过只支持Windows系统的用例;
xfail:因为一个确切的原因,我们知道这个用例会失败;例如,对某个未实现的功能的测试,或者阻塞于某个已知Bug的测试;
-pytest默认不显示skip和xfail用例的详细信息,我们可以通过-r选项来自定义这种行为;
-通常,我们使用一个字母作为一种类型的代表,具体的规则如下:
(f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed, §assed, §assed with output, (a)ll except passed(p/P), or (A)ll
-例如,显示结果为XFAIL、XPASS和SKIPPED的用例:
eg: pytest -rxXs

(11)使用自定义标记mark只执行某部分用例
它主要运用于这样的场景:
-如果要求只执行符合要求的某一部分测试用例,可以把一个web项目划分为多个模块,然后指定模块名称执行。
-APP自动化时,如果想安卓和ios公用一套代码时,也可以使用标记功能,标明哪些是ios的用例,哪些是安卓的,运行时指定mark名称运行就行。

方法:
在测试用例方法上加@pytest.mark.webtest

执行:
-s参数:输出所有测试用例的print信息
-m:z执行自定义标记的相关用例pytest -s test_mark_zi_09.py
pytest -s test_mark_zi_09.py -m=webtest
pytest -s test_mark_zi_09.py -m apptest
pytest -s test_mark_zi_09.py -m “not ios”

#coding=utf-8 import pytest @pytest.mark.search def test_search1(): print("test_search1") raise NameError pass @pytest.mark.search def test_search2(): print("test_search2") pass @pytest.mark.search def test_search3(): print("test_search3") pass @pytest.mark.login def test_login1(): print("test_login1") pass @pytest.mark.login def test_login2(): print("test_login2") pass if __name__=="__main__": pytest.main()

打开终端 敲入
pytest -s test文件名.py -m login

platform win32 -- Python 3.5.4, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 rootdir: D:\pythonlearn\testtest\teststuding plugins: assume-2.2.1, rerunfailures-9.0 collected 5 items / 3 deselected / 2 selected

可见只有两个被标记为login的测试用例被执行了(selceted)

作为小白处于刚刚摸索的状态,哪里有错误,希望能得到指正。
暂时就先写这么多,希望我不要半途而废吧~~~


作者:想不通的一零



mark 学习笔记 学习 pytest

需要 登录 后方可回复, 如果你还没有账号请 注册新账号