复旦大学961-软件工程-第四章-软件测试

961全部内容链接

软件测试及测试用例的概念

软件测试的目的

  • 软件测试是一个为了发现错误而执行程序的过程
  • 一个好的测试用例是指很可能找到迄今为至尚未发现的错误的测试用例
  • 一个成功的测试是指揭示了迄今为至尚未发现的错误的测试

软件测试的重要原则

  • 所有的测试都应可追溯到客户需求,即输入和输出满足客户的需求
  • 应该在测试工作真正开始前的较长时间就进行测试计划
  • Pareto原则:测试中发现的80%的错误可能来自于20%的程序代码
  • 测试应从“小规模”开始,逐步转向“大规模”
  • 穷举测试是不可能的
  • 为了达到最有效的测试,应由独立的第三方来承担测试,即需要专门的测试人员
  • 检查程序是否做了应做的事仅是成功的一半,另一半是检查程序是否做了不该做的事。例如,一段程序不能把长度为0,0,0的三条边认为是等边三角形。

测试用例(test case)是由一个输入数据预期结果组成,测试时通过输入数据,运行被测程序,如果运行的实际输出与预期结果不一致,则表明发现了程序中的错误。

测试用例的原则:

  • 在设计测试用例时,应包括合理的输入条件和不合理的输入条件

测试策略

单元测试

单元测试的基本概念:

  • 单元测试又称模块测试,它着重对软件设计的最小单元(软件构件或模块)进行测试
  • 单元测试根据设计描述,对重要的控制路径进行测试,以发现构件或模块内部的错误
  • 单元测试通常采用白盒测试,并且多个构件或模块可以并行进行测试

单元测试的内容:

  • 模块接口:确保模块的输入/输出参数信息是正确的。这些信息包括参数的个数、次序、类型等
  • 局部数据结构:确保临时存储的数据在算法执行的整个过程中都能维持其完整性。典型错误有:不合适的类型说明、不同数据类型的比较或赋值、文件打开和关闭的遗漏、超越数据结构的边界等
  • 边界条件:确保程序单元在极限或严格的情况下仍能正确地执行
  • 所有独立路径:确保模块中的所有语句都至少执行一次。程序执行的路径实际上体现了计算的过程,计算中常见的错误有:不正确的操作优先级、不同类型数据间的操作、不正确的初始化、不精确的精度、不正确的循环中止、不适当地修改循环变量、发散的迭代等
  • 所有错误处理路径:单元测试应该对所有的错误处理路径进行测试。错误处理部分潜在的错误有:报错信息没有提供足够的信息来帮助确定错误的性质及其发生的位置、报错信息与真正的错误不一致、错误条件在错误处理之前就已引起系统异常、异常条件处理不正确等

单元测试过程:

  • 单元测试通常与编码工作结合起来进行
  • 模块本身不是一个独立的程序,在测试模块时,必须为每个被测模块开发一个驱动(driver)程序和若干个桩(stub)模块在这里插入图片描述
    • 驱动模块接收测试数据,调用被测模块,把测试数据传送给被测模块,被测模块执行后,驱动模块接收被测模块的返回数据,并打印相关结果。
    • 桩模块的功能是替代被被测模块调用的模块,它接受被测模块的调用,验证入口信息,把控制连同模拟结果返回给被测模块。
    • 个人理解:驱动程序就是帮助你调用被测模块的程序。在Java中,Junit就是驱动程序。而桩模块就是用来模拟某些不想被真正执行的代码(如远程调用)的。比如一段代码中要调用一个方法A,该方法为去数据库删除数据,但实际因为是测试,并不想真正删除数据,所以利用桩模块来模拟该方法。当被测模块执行时,执行到方法A时,并没有真正调用方法A中的内容,而是执行了你事先写好的桩模块。在Java中,有各种Mock框架,就是帮助实现桩模块。

集成测试

集成测试也称组装测试、联合测试。集成测试是对集成后的软件系统进行测试。经单元测试后,每个模块都能独立工作,但把它们放在一起往往不能正常工作。主要原因在于:

  • 数据可能在通过接口时丢失
  • 一个模块可能对另一个模块产生非故意的、有害的影响(即副作用)
  • 当子功能被组合起来时,可能不能达到期望的主功能
  • 单个模块可以接受的不精确性(如误差),连接起来后可能会扩大到无法接受的程度
  • 全局数据结构可能会存在问题

集成的两种方式:

  • 非增量式集成:使用“一步到位”的方法来构造程序。先将所有经过单元测试的模块组合在一起,然后对整个程序(作为一个整体)进行测试。这种测试在发现错误时,很难为错误定位。改正错误时容易引入新的错误,新旧错误混在一起,更难定位
  • 增量式集成:根据程序结构图,按某种次序挑选一个(或一组)尚未测试过的模块,把它集成到已测试好的模块中一起进行测试,每次增加一个(或一组)模块,直至所有模块全部集成到程序中。在增量集成测试过程中发现的错误,往往与新加入的模块有关

增量式集成分为自顶向下集成自底向上集成

自顶向下

自顶向下集成:从主控模块(主程序)开始,然后按照程序结构图的控制层次,将直接或间接从属于主控模块的模块按深度优先或广度优先的方式逐个集成到整个结构中,并对其进行测试。在这里插入图片描述
如图该程序模块,其中M1为主程序模块(可以理解为Main函数)。如果采用深度优先的顺序,则集成测试顺序为:M1,M2,M5,M8,M6,M3,M7,M4。若采用广度优先测试顺序,则为:M1,M2,M3,M4,M5,M6,M7,M8。

自顶向下集成的步骤为
1. 主控模块(主程序)被直接用作驱动程序,所有直接从属于主控模块的模块用桩模块替换,然后对主控模块进行测试
2. 根据集成的实现方式(深度优先或广度优先),下层的桩模块一次一个地替换成真正的模块,从属于该模块的模块用桩模块替换,然后对其进行测试
3. 用回归测试来保证没有引入新的错误
4. 重复第(2)和第(3)步,直至所有模块都被集成

自顶向下集成的优点:不需要驱动模块;能尽早对程序的主要控制和决策机制进行检验,能较早发现整体性的错误;深度优先的自顶向下集成能较早对某些完整的程序功能进行检查

自顶向下集成的缺点:测试时低层模块用桩模块替代,不能反映真实情况;重要数据不能及时回送到上层模块

自底向上

自底向上集成:从程序结构的最底层模块(即原子模块)开始,然后按照程序结构图的控制层次将上层模块集成到整个结构中,并对其进行测试。自底向上集成在测试一个模块时,它的下层模块(已测试过)可用作它的桩模块

自底向上集成的步骤

  1. 将低层模块组合成能实现软件特定功能的簇
  2. 为每个簇编写驱动程序,并对簇进行测试
  3. 移走驱动程序,用簇的直接上层模块替换驱动程序,然后沿着程序结构的层次向上组合新的簇
  4. 凡对新的簇测试后,都要进行回归测试,以保证没有引入新的错误
  5. 重复第(2)步至第(4)步,直至所有的模块都被集成

自底向上集成的优点:不需要桩模块,所以容易组织测试;将整个程序结构分解成若干个簇,对同一层次的簇可并行进行测试,可提高效率

自底向上集成的缺点:整体性的错误发现得较晚


对自底向上集成步骤的解释:在这里插入图片描述
还使用之前的图。

  1. 将M8, M6, M7, M4分别组成4个簇,分别命名为(簇1,簇2,簇3,簇4)。进行并行测试。当测试通过时进入2。
  2. 将M5合并到簇1中(簇1=M8+M5),M3合并到簇3(簇3=M3+M7),对簇1和簇3进行测试。此时不需要M8和M7的驱动程序,而是分别由M5和M3对其调用。
  3. 将“M2,M5,M8,M6”组成簇1。与2同理,测试簇1
  4. 将所有模块组成一个簇,整体测试。

策略的选择

自顶向下和自底向上应结合起来使用: 自顶向下集成测试与自底向上集成测试各有优缺点,其中一种策略的优点差不多就是另一种策略的缺点。将这两种策略组合起来可能是一种最好的折衷,这种折衷的策略是:在程序结构的高层使用自顶下向策略,而在低层则使用自底向上策略,这种测试策略也称为三明治测试(sandwich testing)

关键模块的概念:集成测试时应特别关注关键模块(critical module)的测试。关键模块是指具有下列一个或多个特征的模块:

  • 与多个软件需求有关
  • 含有高层控制(位于程序结构的高层)
  • 本身是复杂的或是容易出错的
  • 含有确定的性能需求。

关键模块应尽早测试,回归测试时也应集中在关键模块的功能上

确认测试

确认测试就是系统在要交付之前,做最后确认的测试。

确认测试的标准:确认测试以软件需求规约为依据,以发现软件与需求不一致的错误。主要检查软件是否实现了规约规定的全部功能要求,文档资料是否完整、正确、合理,其他的需求,如可移植性、可维护性、兼容性、错误恢复能力等是否满足

确认测试的结果分为两类:

  • 满足需求规约要求的功能或性能特性,用户可以接受
  • 发现与需求规约有偏差,此时需列出问题清单

软件配置评审:软件配置评审也称软件审核(audit),其目的是保证软件配置的所有成分都齐全,各方面的质量都符合要求,具有维护阶段必需的细节,而且已经编排好分类目录
软件配置主要包括计算机程序(源代码和可执行程序),针对开发者和用户的各类文档,包含在程序内部或程序外部的数据

α测试和β测试

如果软件是为一个客户开发的,那么,最后由客户进行验收测试(acceptance test),以使客户确认该软件是他所需要的。

如果软件是针对许多客户(如腾讯QQ),需要经历α测试和β测试:

  • α测试:由一个用户在开发者的场所进行的,软件在开发者对用户的“指导下”进行测试。经α测试后的软件称为β版(Beta)软件。
  • β测试:由软件的最终用户在一个或多个用户场所进行的,与α测试不同,开发者通常不在测试现场,因此,β测试是软件在一个开发者不能控制的环境中的“活的”应用,用户记录所有在β测试中遇到的(真正的或想象的)问题,并定期把这些问题报告给开发者,在接到β测试的问题报告后,开发者对软件进行最后的修改,然后着手准备向所有的用户发布最终的软件产品。通俗的说:先开放一批用户进行内测,然后把问题反馈给开发者。最后等内测结束之后,就可以玩了。玩游戏的人应该都不陌生。

系统测试

系统测试是对整个基于计算机的系统进行的一系列测试。

主要有以下几种:

  • 恢复测试(recovery testing)
  • 安全保密性测试(security testing)
  • 压力测试(stress testing)
  • 性能测试(performance testing)

恢复测试

恢复测试是通过各种手段,强制软件发生故障,然后来测试系统能否在指定的时间间隔内恢复正常,包括修正错误并重新启动系统

如果恢复是由系统自身来完成的,那么,需测试重新初始化、检查点机制、数据恢复和重启动等的正确性

如果恢复需要人工干预,那么要估算平均修复时间MTTR(mean time to repair)是否在用户可以接受的范围内

安全保密性测试

安全保密性测试(也称安全测试)用来验证集成在系统中的保护机制能否实际保护系统不受非法侵入

在安全测试过程中,测试者扮演一个试图攻击系统的角色,采用各种方式攻击系统。例如,截取或码译密码;借助特殊软件攻击系统;“制服”系统,使他人无法访问;故意导致系统失效,企图在系统恢复之机侵入系统;通过浏览非保密数据,从中找出进入系统的钥匙等等

一般来说,只要有足够的时间和资源,好的安全测试一定能最终侵入系统。系统设计者的任务是把系统设计成:攻破系统所付出的代价大于攻破系统后得到信息的价值

压力测试

压力测试也称强度测试,它是在一种需要非正常数量、频率或容量的方式下执行系统,其目的是检查系统对非正常情况的承受程度。例如:

  • 当系统的中断频率是每秒1或2个时,执行每秒10个中断的测试用例
  • 将输入数据的数量提高一个数量级来测试输入功能如何响应
  • 执行需要最大内存或其它资源的测试用例
  • 执行可能导致大量磁盘驻留数据的测试用例

性能测试

性能测试用来测试软件在集成的系统中的运行性能。它对实时系统和嵌入式系统尤为重要。

性能测试可以发生在测试过程的所有步骤中:

  • 单元测试时,主要测试一个独立模块的性能,如算法的执行速度
  • 软件集成后,进行软件整体的性能测试
  • 计算机系统集成后,进行整个计算机系统的性能测试

性能测试常常需要与压力测试结合起来进行,而且常常需要一些硬件和软件测试设备,以监测系统的运行情况

回归测试

回归测试就是回去重新再测一遍。

在集成测试过程中,每当增加一个(或一组)新模块时,原先已集成的软件就发生了改变。新的数据流路径被建立,新的I/O操作可能出现,还可能激活新的控制逻辑,这些改变可能使原本正常的功能产生错误

当测试时发现错误后,需修改程序;或者在软件维护时也需修改程序。这些对程序的修改也可能使原本正常的功能产生错误

回归测试就是对已经进行过测试的测试用例子集的重新测试,以确保对程序的改变和修改,没有传播非故意的副作用

回归测试集(已经通过测试的测试用例子集)包括三种不同类型的测试用例:

  • 能测试软件所有功能的代表性测试用例
  • 专门针对可能会被修改影响的软件功能的附加测试
  • 注重于修改过的软件模块的测试

调试相关概念

调试的概念

测试的目的是发现错误,调试(也称排错)的目的是确定错误的原因和准确位置,并加以纠正

调试的过程如图所示:在这里插入图片描述
调试的过程分为以下两种情况:

  • 找到错误的原因和位置,则将其改正,并进行回归测试,以确保这一改正未影响其他正常的功能。
  • 未找到错误的原因和位置,此时应假设错误的原因,并设计测试用例来验证此假设,重复这一过程直至找到错误的原因,并加以改正。

调试有如下几种方法:

蛮力法

蛮力法是一种最省脑筋但又最低效的方法。它通过在程序中设置断点,输出寄存器、存储器的内容,打印有关变量的值等手段,获取大量现场信息,从中找出错误的原因

这种方法效率低,输出的信息大多是无用的,通常在其他调试方法未能找到错误原因时,才使用这种方法

可以采用二分法来逐步缩小出错的范围

回溯法

回溯法是从错误的征兆出发,人工沿着控制流程往回跟踪,直至发现错误的根源。这种方法适用于小型程序,对大型程序,由于回溯的路径太多,难以彻底回溯。

就是把自己想成机器,然后一步一步走,看哪一步会出错。所以如果程序偏大, 这个就不合适了。

原因排除法

原因排除法又可分为归纳法演绎法

归纳法是一种从特殊推断一般的系统化思考方法。其基本思想是:从一些线索(错误征兆)着手,通过分析它们之间的关系来找出错误的原因。如下图所示:在这里插入图片描述

演绎法从一般原理或前提出发,假设所有可能出错的原因,排除不可能正确的假设,最后推导出结论。如图所示:在这里插入图片描述

调试与测试的关系

这个从上面的概念中可以看出。这里我自己做一个总结:测试是测试代码是否出错,是否符合预期。而调试是当结果不符合预期时,通过一些方法手段,找出错误原因,并加以修改。

白盒测试的概念

白盒测试(又称为结构测试)把测试对象看作一个透明的盒子,测试人员根据程序内部的逻辑结构及有关信息设计测试用例,检查程序中所有逻辑路径是否都按预定的要求正确地工作。

白盒测试主要用于对模块的测试,包括:

  • 程序模块中的所有独立路径至少执行一次
  • 对所有逻辑判定的取值(“真”与“假”)都至少测试一次
  • 在上下边界及可操作范围内运行所有循环
  • 测试内部数据结构的有效性等

白盒测试中的基本路径测试方法

在实际问题中,一个不太复杂的程序,特别是包含循环的程序,其路径数可能非常大。因此测试常常难以做到覆盖程序中的所有路径,为此,我们希望把测试的程序路径数压缩到一定的范围内

基本路径测试首先根据程序或设计图画出控制流图,并计算其区域数,然后确定一组独立的程序执行路径(称为基本路径),最后为每一条基本路径设计一个测试用例

程序控制流图

流图由结点和边组成,分别用圆和箭头表示。设计图中一个连续的处理框(对应于程序中的顺序语句)序列和一个判定框(对应于程序中的条件控制语句)映射成流图中的一个结点,设计图中的箭头(对应于程序中的控制转向)映射成流图中的一条边。对于设计图中多个箭头的交汇点可以映射成流图中的一个结点(空结点)

上述映射的前提是设计图的判定中不包含复合条件。如果设计图的判定中包含了复合条件,那么必须先 将其转换成等价的简单条件设计图

如图:在这里插入图片描述
这里对于上面复杂的话做一个解释。

  1. 流图由节点和边组成。图c是一个数据流图,节点就是1234这些节点,边就是1->2之间的那条边
  2. 设计图中一个连续的处理框序列和一个判定框映射成流图中的一个结点。对应图b到图c的映射的话,节点1就是“y=0”和“a>b”共同组成的。之所以要这样,是因为如果多条顺序语句在碰到条件控制语句之前(if、while等)是不会出现分支的。所以就可以将其合并成一个节点,方便处理
  3. 关于拆分复合条件,书上是让拆分复合条件,但网上有些拆分,有些不拆分。我觉得以书上为准吧。

黑盒测试的概念

黑盒测试(又称行为测试)把测试对象看做一个黑盒子,测试人员完全不考虑程序内部的逻辑结构和内部特性,只依据程序的需求规格说明书,检查程序的功能是否符合它的功能需求

黑盒测试可用于各种测试,它试图发现以下类型的错误:

  • 不正确或遗漏的功能
  • 接口错误,如输入/输出参数的个数、类型等
  • 数据结构错误或外部信息(如外部数据库)访问错误
  • 性能错误
  • 初始化和终止错误

黑盒测试主要有以下方法:等价类划分、边界值分析、比较测试、错误猜测、因果图

黑盒测试中的等价类划分方法

由于不能穷举所有可能的输入数据来进行测试,所以只能选择少量有代表性的输入数据,来揭露尽可能多的程序错误

等价类划分方法将所有可能的输入数据划分成若干个等价类,然后在每个等价类中选取一个代表性的数据作为测试用例。

等价类是指输入域的某个子集,该子集中的每个输入数据对揭露软件中的错误都是等效的,测试等价类的某个代表值就等价于对这一类其他值的测试。也就是说,如果该子集中的某个输入数据能检测出某个错误,那么该子集中的其他输入数据也能检测出同样的错误;反之,如果该子集中的某个输入数据不能检测出错误,那么该子集中的其他输入数据也不能检测出错误

等价类划分方法把输入数据分为有效输入数据和无效输入数据:

  • 有效输入数据指符合规格说明要求的合理的输入数据,主要用来检验程序是否实现了规格说明中的功能
  • 无效输入数据指不符合规格说明要求的不合理或非法的输入数据,主要用来检验程序是否做了规格说明以外的事

在确定输入数据等价类时,常常还要分析输出数据的等价类,以便根据输出数据等价类导出输入数据等价类

根据软件的规格说明,对每一个输入条件(通常是规格说明中的一句话或一个短语)确定若干个有效等价类和若干个无效等价类。一般使用如下表格表示

输入条件有效等价类无效等价类

例题:某编译程序的规格说明中关于标识符的规定如下:标识符是由字母开头,后跟字母或数字的任意组合构成;标识符的字符数为1∽8个;标识符必须先说明后使用;一个说明语句中至少有一个标识符;保留字不能用作变量标识符。

输入条件有效等价类无效等价类
第一个字符字母(1)数字(2)
非字母数字字符(3)
后跟的字符字母(4)
数字(5)
非字母数字字符(6)
保留字(7)
字符数1-8个(8)0个(9)
大于8个(10)
标识符的使用先说明后使用(11)未说明已使用(12)
标识符个数大于等于1个(13)0个(14)

测试覆盖度的概念

测试覆盖度也称覆盖率。主要用来度量测试是否全面。

白盒测试中,经常用到的覆盖率是白盒覆盖率,包含以下几种:语句覆盖、判定覆盖、条件覆盖、判定/条件覆盖、条件组合覆盖、路径覆盖。

语句覆盖

语句覆盖是指选择足够的测试用例,使得运行这些测试用例时,被测程序的每个可执行语句都至少执行一次

语句覆盖率=(至少被执行一次的语句数量)/可执行的语句总数。

判定覆盖

判定覆盖(也称分支覆盖)是指选择足够的测试用例,使得运行这些测试用例时,被测程序的每个判定的所有可能结果都至少执行一次(即判定的每个分支至少经过一次)

判定覆盖=(判定结果被评价的次数)/判定结果的总数。

条件覆盖

条件覆盖是指选择足够的测试用例,使得运行这些测试用例时,被测程序的每个判定中的每个条件的所有可能结果都至少出现一次

条件覆盖=(条件操作数值至少被评价一次的数量)/(条件操作数值得总数)

注意判定与条件的区别,例如

if (条件1||条件2&&条件3) {
	// 分支1
} else {
	// 分支2
}

条件指的是if括号内的东西,可以有多个。而判定(分支)指的是当条件成立或不成立后的路径走向。

判定/条件覆盖

判定/条件覆盖是指选择足够的测试用例,使得运行这些测试用例时,被测程序的每个判定的所有可能结果都至少执行一次,并且,每个判定中的每个条件的所有可能结果都至少出现一次

判定条件覆盖=(条件操作数值或判定结果至少被评价一次的数量)/(条件操作数值总数+判定结果总数)

条件组合覆盖

条件组合覆盖是指选择足够的测试用例,使得运行这些测试用例时,被测程序的每个判定中条件结果的所有可能组合都至少出现一次

显然,满足条件组合覆盖标准的测试用例一定也满足判定覆盖、条件覆盖、判定/条件覆盖、语句覆盖标准

条件组合覆盖=(至少被执行到一次的条件组合)/总的可能的条件组合数。

注意判定/条件覆盖和条件组合覆盖的区别,例如:

if (条件1||条件2&&条件3) {
	// 分支1
} else {
	// 分支2
}

判定/条件覆盖是条件和判定的总和。只要每个条件都执行过,每个分支也都执行过就可以。比如使用 “条件1=true, 条件2=true,条件3=true” 和 “条件1=false, 条件2=false,条件3=false” 只需要这两中测试数据就可以达到判定/条件全覆盖。因为每一个分支和每一个条件都执行过。

而条件组合覆盖强调的是组合。比如上述两种情况是两种组合,还可以有 “条件1=false, 条件2=true,条件3=true”这样的组合。条件越多,组合数越多。所以说条件组合判定是上述5种覆盖标准中最强的一种。因为只要这个全覆盖了,上面都全覆盖了。

路径覆盖

路径覆盖是指选择足够的测试用例,使得运行这些测试用例时,被测程序的每条可能执行到的路径都至少经过一次(如果程序中包含环路,则要求每条环路至少经过一次)

路径覆盖实际上考虑了程序中各种判定结果的所有可能组合,但它未必能覆盖判定中条件结果的各种可能情况。因此,它是一种比较强的覆盖标准,但不能替代条件覆盖和条件组合覆盖标准

路径覆盖=(至少被执行到一次的路径数)/总的路径数。

代码圈复杂度的计算方法

圈复杂度(Cyclomatic Complexity)是用来描述一段代码的复杂程度用的。如果圈复杂度越低说明代码写的越好。

计算代码圈复杂度要进行如下几个步骤:

  1. 将代码转化为程序控制流图(上面的“白盒测试”中有提到),若包含复合条件,需要将符合条件拆开
  2. 根据公式,计算圈复杂度

计算代码圈复杂度的公式为: M = E − N + 2 P M=E-N + 2P M=EN+2P

其中:

  • E:表示图中边的个数
  • N:表示图中节点的个数
  • P:P表示使程序结束的节点的个数

关于P,网上很多文章给出的不一致。但实际效果是一样的。比如典型的有:

  1. return和throw的数量之和
  2. 使程序结束的节点
  3. 更专业点,还有:连通分量的个数

他们其实表示的是一个东西,计算下来也都一样。

个人感觉(没找到具体资料验证):最终求的M就是该程序段的路径数。

例1:
在这里插入图片描述
M=7 - 6 + 2*1 = 3 (1上面和6下面那个箭头肯定不算)

例2:
在这里插入图片描述
M = 8 - 7 + 2 * 1 = 3

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

iioSnail

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

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

抵扣说明:

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

余额充值