WEB UI自动化测试之Pytest框架学习

发布于:2025-05-09 ⋅ 阅读:(16) ⋅ 点赞:(0)


前言

阅读本文前请注意最后编辑时间,文章内容可能与目前最新的技术发展情况相去甚远。欢迎各位评论与私信,指出错误或是进行交流等。


在之前已经学习过用unittest测试框架,本文介绍另外一个目前主流的测试框架Pytest。

Pytest简介

官方文档介绍:

Pytest is a framework that makes building simple and scalable tests easy. Tests are expressive and readable—no boilerplate code required. Get started in minutes with a small unit test or complex functional test for your application or library.
官方地址:https://docs.pytest.org

pytest是一个非常成熟的Python测试框架,主要有以下几个特点:

  • 简单灵活,容易上手
  • 支持参数化
  • 能够支持简单的单元测试和复杂的功能测试,还可以配合selenium/appnium等进行自动化测试、接口自动化测试(pytest+requests)
  • pytest具有很多第三方插件,并且可以自定义扩展,比较好用的如pytest-selenium(集成selenium)、pytest-html(html测试报告生成)、pytest-rerunfailures(失败case重复执行)、pytest-xdist(多CPU分发)等
  • 测试用例的skip和xfail处理
  • 可以很好的和jenkins集成
  • 测试报告框架----allure 也支持pytest

Pytest安装

安装命令

pip install -U pytest

查看是否安装成功

pytest --version # 展示当前已安装版本

Pytest的常用插件

插件列表网址:https://plugincompat.herokuapp.com
包含很多插件包,大家可依据工作的需求选择使用。

Pytest的命名约束

1)模块名(py文件)通常被统一放在一个testcases文件夹中,必须是以test_开头或者_test.py结尾
2)测试类(class)必须以Test开头,并且不能带init方法,类里的方法必须以test_开头
3)测试用例(函数)必须以test_开头
此时,在执行pytest命令时,会自动从当前目录及子目录中寻找符合上述约束的测试函数来执行。
在这里插入图片描述

# 测试函数必须以test_开头(类以外)
 def test_demo1(self):
        print("测试用例1")

# 测试类(class)必须以Test开头 
class TestDemo:
	# 类里的方法必须以test_开头
    def test_demo2(self):
        print("测试用例2")

Pytest的运行方式

Pytest运行方式与unittest对比

运行步骤 Pytest unittest
导包 导入pytest模块 导入unittest模块
创建测试方法(不写在测试类中的) 测试方法名称必须以test开头 测试方法名称必须以test开头
创建测试类 必须以Test开头,并且不能带init方法 测试类的命名不做要求,但需要继承unittest.TestCase类。
添加测试固件(fixtrue 非必需) setUp()、tearDown()系列方法 、 fixtrue装饰器 使用setUp()、tearDown()系列方法
测试类中定义测试方法(即测试用例) 测试方法名称必须以test开头 测试方法名称必须以test开头
执行测试用例 通过主函数/命令行/配置文件 方式运行 通过主函数 或者 使用TestSuite/TestLoader/TextTestRunner

主函数运行

import  pytest
#  创建测试方法(不写在测试类中的)
def test_01():
    print("啥也没有")
# 通过主函数方式运行
if __name__=='__main__':
    pytest.main()

main()中可使用的参数有:

参数 描述 案例
-v 输出调试信息。 如:打印信息 pytest.main([‘-v’,‘testcase/test_one.py’,‘testcase/test_two.py’])
-s 输出更详细的信息,如:文件名、用例名 pytest.main([‘-vs’,‘testcase/test_one.py’,‘testcase/test_two.py’])
-n 多线程或分布式运行测试用例
-x 只要有一个用例执行失败,就停止执行测试 pytest.main([‘-vsx’,‘testcase/test_one.py’])
– maxfail 出现N个测试用例失败,就停止测试 pytest.main([‘-vs’,‘-x=2’,‘testcase/test_one.py’]
–html=report.html 生成测试报告 pytest.main([‘-vs’,‘–html=./report.html’,‘testcase/test_one.py’])
-m 通过标记表达式执行
-k 根据测试用例的部分字符串指定测试用例,可以使用and,or

命令行运行

文件路径:testcase/test_one.py

def test_a():
    print("啥也没有")
    assert 1==1

终端输入:pytest ./testcase/test_one.py --html=./report/report.html

命令行中可使用的参数有:

参数 描述 案例
-v 输出调试信息。 pytest -x ./testcase/test_one.py
-q 输出简单信息。 pyets -q ./testcase/test_one.py
-s 输出更详细的信息,如:文件名、用例名 pytest -s ./testcase/test_one.py
-n 多线程或分布式运行测试用例
-x 只要有一个用例执行失败,就停止执行测试 pytest -x ./testcase/test_one.py
– maxfail 出现N个测试用例失败,就停止测试 pytest --maxfail=2 ./testcase/test_one.py
–html=report.html 生成测试报告 pytest ./testcase/test_one.py --html=./report/report.html
-k 根据测试用例的部分字符串指定测试用例,可以使用and,or

执行结果代码说明

  • Exit code 0 所有用例执行完毕,全部通过
  • Exit code 1 所有用例执行完毕,存在Failed的测试用例
  • Exit code 2 用户中断了测试的执行
  • Exit code 3 测试执行过程发生了内部错误
  • Exit code 4 pytest 命令行使用错误
  • Exit code 5 未采集到可用测试用例文件

pytest.ini配置文件方式运行(推荐)

不管是mian执行方式还是命令执行,最终都会去读取pytest.ini文件。
配置有 pytest.ini 的工程, 只需要打开命令行输入pytest,即可进行测试。
在项目的根目录下创建pytest.ini文件,包含以下内容

[pytest]
addopts=-vs -m slow --html=./report/report.html
testpaths=./scripts
test_files=test_*.py
test_classes=Test*
test_functions=test_*
makerers=
	smock:冒烟测试用例
	regression: 回归测试标记
参数 作用
[pytest] 用于标志这个文件是pytest的配置文件
addopts 命令行参数,多个参数之间用空格分隔
testpaths pytest要进行测试的工作目录
test_files 工作目录下要进行测试的文件名匹配规则
test_classes 工作目录下 测试文件中 可执行类的名称匹配规则
test_functions 测试类中 测试方法名的匹配规则
markers 用例标记,自定义mark,需要先注册标记

使用markers标记测试用例

在 pytest 中,markers 是一种非常有用的功能,它可以对测试用例进行标记,以便实现更灵活的测试执行和分组等操作。
**在pytest.ini文件中自定义 markers。**例如:

makerers=
smock:冒烟测试用例
regression: 回归测试标记

这里创建了冒烟测试和回归测试的标记,我们就可以使用这个标记为测试用例进行标记。如下所示:

import pytest

@pytest.mark.smoke
def test_01():
    assert 1==1
    
@pytest.mark.regression
def test_02():
    assert 2==2

在这里插入图片描述
如果不想每次都输入-m smoke,我们可以把这部分放到pytest.ini下。
在这里插入图片描述

pytest中添加Fixture(测试夹具)

pytest中的setup与teardown

Pytest也提供了类似于unittest中 setup、teardown的方法,并且分为了五类:

  • 模块级别:setup_module、teardown_module
  • 函数级别:setup_function、teardown_function,不在类中的方法
  • 类级别:setup_class、teardown_class
  • 方法级别:setup_method、teardown_method
  • 方法细化级别:setup、teardown

我们直接来看代码和运行结果

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
 
import pytest
 
 
def setup_module():
    print("=====整个.py模块开始前只执行一次:打开浏览器=====")
 
 
def teardown_module():
    print("=====整个.py模块结束后只执行一次:关闭浏览器=====")
 
 
def setup_function():
    print("===每个函数级别用例开始前都执行setup_function===")
 
 
def teardown_function():
    print("===每个函数级别用例结束后都执行teardown_function====")
 
 
def test_one():
    print("one")
 
 
def test_two():
    print("two")
 
 
class TestCase():
    def setup_class(self):
        print("====整个测试类开始前只执行一次setup_class====")
 
    def teardown_class(self):
        print("====整个测试类结束后只执行一次teardown_class====")
 
    def setup_method(self):
        print("==类里面每个用例执行前都会执行setup_method==")
 
    def teardown_method(self):
        print("==类里面每个用例结束后都会执行teardown_method==")
 
    def setup(self):
        print("=类里面每个用例执行前都会执行setup=")
 
    def teardown(self):
        print("=类里面每个用例结束后都会执行teardown=")
 
    def test_three(self):
        print("three")
 
    def test_four(self):
        print("four")
 
 
if __name__ == '__main__':
    pytest.main(["-q", "-s", "-ra", "setup_teardown.py"])

在这里插入图片描述

注意,从执行结果我们可以看到:

  • 模块级别:setup_module、teardown_module 在整个.py模块开始前和结束后只执行一次;
  • 函数级别:setup_function、teardown_function,对不在类中的方法生效。在每个函数级别用例开始前和结束后执行;
  • 类级别:setup_class、teardown_class,整个测试类开始前和结束后执行一次;
  • 方法级别:setup_method、teardown_method,测试类里面每个测试用例开始前和结束后都执行一次;
  • 方法细化级别:setup、teardown,跟前面方法级别使用类似,不过setup在setup_method之后执行,teardown在teardown_method之前执行;
    Pytest除了与unittest中类似的提供了setup和teardown方法外,还提供了使用装饰器的格式来为测试用例添加Fixture。

conftest

在介绍装饰器格式的Fixture前,先介绍conftest。
Conftest它是pytest的一个组件,用于配置测试环境和参数。
因此,conftest.py 文件是存放测试夹具(Fixtures)的理想场所。
测试夹具就像是测试用例执行时的得力助手,能够为测试提供各种必要的资源,如数据、对象、环境等。
通过在 conftest.py 中定义测试夹具,我们可以在多个测试模块中轻松地共享和复用这些资源,避免了在每个测试文件中重复编写相同的准备代码,大大提高了代码的可维护性和测试效率。

  • 运行测试之前,pytest会自动识别并执行conftest.py文件中的配置。
  • 运行测试之前,pytest 会默认读取 conftest.py里面的所有 fixture
  • conftest.py 文件名称是固定的,不能改动
  • conftest.py 只对同一个 package 下的所有测试用例生效
  • 不同目录可以有自己的 conftest.py,一个项目中可以有多个 conftest.py
  • 测试用例文件中不需要手动 import conftest.py,pytest 会自动查找

** 各级工作目录下的conftest.py**
在这里插入图片描述

pytest中的fixtrue装饰器

我们可以在conftest.py中,或者是测试模块中定义fixtrue装饰器。

如果有以下场景:1.用例一需要执行登录操作;2.用例二不需要执行登录操作;3.用例三需要执行登录操作,则unittest框架中的setup和teardown则不满足要求。而Pytest中的fixture装饰器就可以解决这一问题

pytest中的fixtrue装饰器的优势

  • 命名方式灵活,不限于setup和teardown两种命名
  • conftest.py可以实现数据共享,不需要执行import 就能自动找到fixture装饰器

Fixture装饰器的定义方式

@pytest.fixture(scope = "function",params=None,autouse=False,ids=None,name=None)

fixtrue装饰器参数详解-scope

用于控制Fixture装饰器的作用范围,默认取值为function(函数级别)

取值 范围 说明
function 函数级 每一个函数或方法都可以调用,在测试用例执行前生效。
class 模块级 测试类内的测试用例可以调用,具体在何处生效得视情况而定
module 模块级 每一个.py文件调用一次, 在第一次调用fixtrue的地方生效
session 会话级 每次会话只运行一次,会话内所有方法及类,模块都共享这个fixtrue
scope = “function”
  • 场景一:做为参数传入
import pytest

@pytest.fixture()
def login():
    print("打开浏览器")
    a = "account"
    return a
    
@pytest.fixture()
def logout():
    print("关闭浏览器")
 
class TestLogin:
    #传入lonin fixture
    def test_001(self, login):
        print("001传入了loging fixture")
        assert login == "account"
 
    #传入logout fixture
    def test_002(self, logout):
        print("002传入了logout fixture")
 
    def test_003(self, login, logout):
        print("003传入了两个fixture")
 
    def test_004(self):
        print("004未传入仍何fixture哦")
 
if __name__ == '__main__':
    pytest.main()
 

在这里插入图片描述

从运行结果可以看出,fixture函数做为参数传入时,会先执行所有的fixture函数。

  • 场景二:Fixture的相互调用
import pytest

@pytest.fixture()
def account():
    a = "account"
    print("第一层fixture")
    return a
    
#Fixture的相互调用一定是要在测试类里调用这层fixture才会生次,普通函数单独调用是不生效的
@pytest.fixture()   
def login(account):
    print("第二层fixture")
 
class TestLogin:
    def test_1(self, login):
        print("直接使用第二层fixture,返回值为{}".format(login))
 
    def test_2(self, account):
        print("只调用account fixture,返回值为{}".format(account))
 
 
if __name__ == '__main__':
    pytest.main()

在这里插入图片描述

1.即使fixture之间支持相互调用,但普通函数直接使用fixture是不支持的,一定是在测试类中的测试函数内调用才会逐级调用生效
2.有多层fixture调用时,最先执行的是最内层fixture,而不是先执行传入测试函数的fixture
3.上层fixture的值不会自动return,这里就类似函数相互调用一样的逻辑

scope = “class”

当fixture的作用范围是class时, 具体如何生效要根据fixture的写法来决定。一共有两种情况
1.当测试类内的每一个测试方法都调用了fixture,fixture只在该class下所有测试用例执行前执行一次
2.测试类下面只有部分测试方法使用了fixture函数名。fixture只在该class下第一个使用fixture函数的测试用例执行前执行一次

  • 场景一: 每一个测试方法都调用了fixture
import pytest
# fixture作用域 scope = 'class'
@pytest.fixture(scope='class')
def login():
    print("scope为class")
 
 
class TestLogin:
    def test_1(self, login):
        print("用例1")
 
    def test_2(self, login):
        print("用例2")
 
    def test_3(self, login):
        print("用例3")
 
 
if __name__ == '__main__':
    pytest.main()

在这里插入图片描述

  • 场景二: 部分测试方法都调用了fixture
import pytest
@pytest.fixture(scope='class')
def login():
    a = '123'
    print("输入账号密码登陆")
 
class TestLogin:
    def test_1(self):
        print("用例1")
 
    def test_2(self, login):
        print("用例2")
 
    def test_3(self, login):
        print("用例3")
 
    def test_4(self):
        print("用例4")
 
if __name__ == '__main__':
    pytest.main()

在这里插入图片描述

scope = “module”
import pytest

@pytest.fixture(scope='module')
def login():
    print("fixture范围为module")

def test_01():
    print("用例01")
 
def test_02(login):
    print("用例02")
 
class TestLogin():
    def test_1(self):
        print("用例1")
 
    def test_2(self):
        print("用例2")
 
    def test_3(self):
        print("用例3")
 
if __name__ == '__main__':
    pytest.main()

在这里插入图片描述

fixtrue参数详解-autouse

默认False
若为True,每个测试函数都会自动调用该fixture,无需传入fixture函数,作用范围跟着scope走
autouse=ture的效果如下:

在这里插入图片描述

测试用例添加、加载、运行(对比unittest)

  • 如果要运行多个测试用例,unittest提供了TestSuite/TestLoader的方法加载测试用例,随后利用TextTestRunner执行测试用例套件。
  • Pytest 可通过pytest.ini配置文件 确定测试的工作目录。对测试方法、测试类、测试模块的命名进行了约束后,在配置文件中写好名称匹配规则后,通过主函数或者命令行的方式运行。Pytest会自动加载符合条件的测试用例并运行。

pytest跳过测试用例skip、skipif

@pytest.mark.skip

跳过执行测试,有可选参数 reason:跳过的原因,会在执行结果中打印
@pytest.mark.skip可以加在函数上,测试类上,测试类中的方法上
如果加在测试类上面,测试类里面的所有测试用例都不会执行

import pytest

@pytest.mark.skip(reason="不执行该用例!!因为没写好!!")
def test_case01():
    print("skip加在函数上")

@pytest.mark.skip(reason="skip加在测试类上") 
class TestSkip:
 
    def test_1(self):
        print("%% 不会执行 %%")
 
    def test_2(self):
        print("%% 不会执行 %%")
 
class Test1:
	@pytest.mark.skip(reason="skip加在测试类中的方法上")
    def test_1(self):
        print("%% 不会执行 %%")

@pytest.mark.skipif

方法:
skipif(condition, reason=None)
参数:
condition:跳过的条件,必传参数
reason:标注原因,必传参数
使用方法:
@pytest.mark.skipif(condition, reason=“xxx”)

 
import pytest
class Test_ABC:
    def setup_class(self):
        print("------->setup_class")
    def teardown_class(self):
        print("------->teardown_class")
    def test_a(self):
        print("------->test_a")
        assert 1
    @pytest.mark.skipif(condition=2>1,reason = "跳过该函数")
    def test_b(self):
        print("------->test_b")
            assert 0
执行结果:
   test_abc.py 
   ------->setup_class
   ------->test_a #只执行了函数test_a
   .
   ------->teardown_class
       s # 跳过函数
 

跳过标记

可以将 pytest.mark.skip 和 pytest.mark.skipif 赋值给一个变量,在不同模块之间共享这个变量。可以用一个单独的文件去管理这些通用标记

# 标记
skipmark = pytest.mark.skip(reason="不能在window上运行=====")
skipifmark = pytest.mark.skipif(sys.platform == 'win32', reason="不能在window上运行啦啦啦=====")
 
 
@skipmark
class TestSkip_Mark(object):
 
    @skipifmark
    def test_function(self):
        print("测试标记")
 
    def test_def(self):
        print("测试标记")
 
 
@skipmark
def test_skip():
    print("测试标记")
 

Pytest参数化

@pytest.mark.parametrize(argnames, argvalues)# argnames 含义:参数列表
# argvalues 含义:参数值列表

# 例如:
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
    print(f"测试数据{test_input},期望结果{expected}")
    assert eval(test_input) == expected
# 参数列表就是"test_input,expected" 与函数中的形参一致。 参数值必须是一个列表,且由于有两个参数,需要以元组的方式存放
# 如果只有一个参数,如:@pytest.mark.parametrize(“username”, [“yy”, “yy2”, “yy3”])
# 如果有多个参数例,则需要用元组来存放值,一个元组对应一组参数的值

函数返回值作为参数

import pytest
def return_test_data():
	# 中间代码掠过
    return [(1,2),(0,3)]
class Test_ABC:
    def setup_class(self):
        print("------->setup_class")
    def teardown_class(self):
            print("------->teardown_class")
	@pytest.mark.parametrize("a,b",return_test_data()) # 使用函数返回值的形式传入参数值
	def test_a(self,a,b):
	    print("test data:a=%d,b=%d"%(a,b))
	    assert a+b == 3
    
  执行结果:
  test_abc.py 
  ------->setup_class
  test data:a=1,b=2 # 运行第一次取值 a=1,b=2
  .
  test data:a=0,b=3 # 运行第二次取值 a=0,b=3
  .
      ------->teardown_class

“笛卡尔积”,多个参数化装饰器

# 笛卡尔积,组合数据
data_1 = [1, 2, 3]
data_2 = ['a', 'b']
@pytest.mark.parametrize('a', data_1)
@pytest.mark.parametrize('b', data_2)
def test_parametrize_1(a, b):
    print(f'笛卡尔积 测试数据为 : {a},{b}')

输出测试报告

目前主流的与Pytest搭配的测试报告是Allure,Allure是一个灵活轻量级多语言测试报告工具。
关于Allure会在另外一篇博文中介绍。

预期失败函数

作用

期望测试用例是失败的,但是仍会运行此测试用例,并且也不会影响其他测试用例的的执行。
如果预期失败的测试用例执行失败,则输出结果是xfail(不会额外显示出错误信息)
如果预期失败的测试用例执行成功,则输出结果是xpass。(不符合预期的成功)

应用场景

  • 用例功能不完善,或者用例执行一直失败。
  • 对尚未实现的功能进行测试时。
  • 尚未修复的错误进行测试时。

语法与参数

@pytest.mark.xfail(condition=None, reason=None);

  • condition:表示预期结果,然后用例实际执行的结果,与预期结果对比,会出现4种测试结果状态。
    failed, passed, xfailed, xpassed。
    提示:condition可以等于True或者False,也可以等于一个表达式,如:condition=1>2等。

  • reason:说明用例标记为预期失败的原因, 默认为None。(必填)

另外,我们也可以通过pytest.xfail方法在用例执行过程中直接标记用例结果为XFAIL,并跳过剩余的部分:

def test_function():
    if not valid_config():
        pytest.xfail("failing configuration (but should work)")

控制方法执行顺序

在使用 Pytest 进行单元测试或集成测试时,通常测试用例的执行顺序是自动排序的。不过在某些情况下,特别是当测试用例存在依赖关系时,我们可能希望自定义测试的执行顺序。为了解决这一需求,Pytest 提供了一个实用插件——pytest-ordering。

简介

pytest-ordering 是 Pytest 的一个插件,允许我们自定义测试用例的执行顺序。通过为测试用例指定顺序标记,可以在确保测试独立性的同时,满足特定的执行需求。

安装 pytest-ordering

pip install pytest-ordering

使用方法

在安装插件后,可以使用 @pytest.mark.run 标记来为测试用例设置执行顺序。常用的执行顺序标记有以下几种:

  • @pytest.mark.run(order=N):使用具体的整数值 N 定义执行顺序,数字越小,优先级越高。
  • @pytest.mark.run(before=‘test_name’):表示当前测试用例应在指定测试 test_name 之前运行。
  • @pytest.mark.run(after=‘test_name’):表示当前测试用例应在指定测试 test_name 之后运行。

示例

示例1:基于 order 的执行顺序

import pytest
 
@pytest.mark.run(order=2)
def test_case_1():
    assert True
 
@pytest.mark.run(order=1)
def test_case_2():
    assert True
 
@pytest.mark.run(order=3)
def test_case_3():
    assert True

执行顺序将按照 order 的值来确定,因此运行顺序为:test_case_2 -> test_case_1 -> test_case_3。

示例2:基于 before/after 的执行顺序

import pytest
 
@pytest.mark.run(before="test_case_2")
def test_initial_setup():
    assert True
 
def test_case_1():
    assert True
 
@pytest.mark.run(after="test_case_1")
def test_case_2():
    assert True

在此示例中,执行顺序为 test_initial_setup -> test_case_1 -> test_case_2。

失败重试

有时候用例失败并非代码问题,而是由于网络等因素,导致请求失败。从而降低了自动化用例的稳定性,最后还要花时间定位到底是自身case的原因还是业务逻辑问题,还是其他原因,增加了定位成本。
增加容错机制,失败重试,会解决大部分由于网络原因、服务重启等原因造成的case失败问题。那该如何增加失败重试机制呢?使用pytest-rerunfailures插件来实现失败重试功能。

简介

pytest-rerunfailures 是一个基于 pytest 框架的插件,它允许我们对测试用例进行失败重试。当一个测试用例失败时,插件会自动重新运行失败的测试用例,直到达到预定的重试次数或测试用例通过为止。这样可以增加用例的稳定性,并减少因为偶发性问题导致的测试失败。

文档地址:
https://github.com/pytest-dev/pytest-rerunfailures
https://pypi.org/project/pytest-rerunfailures/#description

安装

pip install pytest-rerunfailures 

使用方法

装饰器方式

import pytest
# 参数:reruns=n(重新运行次数),reruns_delay=m(每次重试之间的延迟时间)
@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_case():
assert 1 == 2

运行case,看一下执行结果:

# 执行命令
pytest -s -v test_demo.py::test_case

# 结果
RERUN
test_dir/test_demo.py::test_case RERUN
test_dir/test_demo.py::test_case RERUN
test_dir/test_demo.py::test_case FAILED

可以看到,重试了3次,最终结果为失败。

参数添加运行方式

pytest-rerunfailures支持使用命令行选项和配置文件的方式进行配置。
并且是对所有的测试用例生效

# 在命令行运行中添加参数 --reruns 3 --reruns-delay 2
pytest -s -v --reruns 3 --reruns-delay 2 test_demo.py::test_case

# 主函数运行中添加参数
pytest.main(["-s", "-v", "--reruns", "3", "--reruns-delay", "2", "test_demo.py::test_case"])

# 或者在pytest.ini配置文件addopts中添加参数
addopts=-vs --reruns 3 --reruns-delay 2 --html=./report/report.html

注意:如果指定了用例的重新运行次数,则在命令行添加 --reruns 对这些用例是不会生效的


参考目录

https://www.bilibili.com/video/BV18Q4y1y7v3
https://blog.csdn.net/kkkkk19980517/article/details/139065687
https://blog.csdn.net/lovedingd/article/details/98952868
https://blog.csdn.net/qq_42610167/article/details/101204066
https://blog.csdn.net/qq_45609369/article/details/140007322
https://blog.csdn.net/m0_63463510/article/details/145914339
https://blog.csdn.net/m0_37135615/article/details/146145220
https://blog.csdn.net/fyyaom/article/details/102938704
https://blog.csdn.net/cebawuyue/article/details/144106872
https://blog.csdn.net/weixin_56331124/article/details/145190520


网站公告

今日签到

点亮在社区的每一天
去签到