单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
1. 定义: 【单元测试】, 【敏捷开发】, 【TDD】 , 【黑盒/白盒测试】 和 【覆盖率】
1.1. 软件测试的4个阶段: 单元测试 → 集成测试 → 系统测试 → 验收测试
软件测试按照阶段可划分为: 单元测试(Unit Testing),集成测试(Integration Testing),系统测试(System Testing)和验收测试(Acceptance Testing) 。
1.1.1. 单元测试(Unit Testing): 基于最小可测试模块(单个类及方法/代码行)的独立性测试
单元测试是指对软件中的最小可测试单元进行检查和验桩模块(stud)
是指模拟被测模块所调用的模块。驱动模块(driver)
是指模拟被测模块的上级模块,驱动模块用来接收测试数据,启动被测模块并输出结果。
1.1.2. 集成测试(Integration Testing):基于模块/接口组装的测试
集成测试是单元测试的下一阶段,是指将通过测试的单元模块组装成系统或子系统,再进行测试,重点测试不同模块的接口部门。
集成测试就是用来检查各个单元模块结合到一起能否协同配合,正常运行。
1.1.3. 系统测试(System Testing):基于软件、系统的整体功能/性能测试
系统测试指的是将整个软件系统看做一个整体进行测试,包括对功能、性能,以及软件所运行的软硬件环境进行测试。
系统测试的主要依据是《系统需求规格说明书》文档。
1.1.4. 验收测试(Acceptance Testing): 基于产品的交付性测试
验收测试指的是在系统测试的后期,以用户测试为主,或有测试人员等质量保障人员共同参与的测试,它也是软件正式交给用户使用的最后一道工序。
验收测试又分为:α测试
和β测试
:其中α测试指的是由用户、 测试人员、开发人员等共同参与的内部测试;而β测试指的是内测后的公测,即完全交给最终用户测试。
1.2. 单元测试用例
单元测试用例是一部分代码,可以确保另一端代码(方法)按照预期工作。
一个正式的编写好的单元测试用例的特点是:已知输入和预期输出,即在测试执行前就已知。已知输入需要测试的先决条件,预期输出需要测试后置条件。
每一项需求至少需要两个单元测试用例:一个正检验,一个负检验。如果一个需求有子需求,每一个子需求必须至少有正检验和负检验两个测试用例。
1.3. 黑盒测试与白盒测试
软件测试按照是否查看程序内部结构可分为黑盒测试和白盒测试:
1.3.1. 黑盒测试(Black-Box Testing)
黑盒测试只关心输入和输出的结果。已知产品的功能设计规格,可以进行测试证明每个实现了的功能是否符合要求。
1.3.2. 白盒测试(White-Box Testing)
白盒测试要研究软件内里的源代码和程序结构。
已知产品的内部工作过程,可以通过测试证明每种内部操作是否符合设计规格要求,所有内部成分是否以经过检查。
1.4. 敏捷开发(Agile software development)
敏捷开发是一种以人为核心、迭代、循序渐进的开发方法。在敏捷开发中,软件项目的构建被切分成多个子项目,各个子项目的成果都经过测试,具备集成和可运行的特征。
简单地来说,敏捷开发并不追求前期完美的设计、完美编码,而是力求在很短的周期内开发出产品的核心功能,尽早发布出可用的版本。然后在后续的生产周期内,按照新需求不断迭代升级,完善产品。
敏捷开发的实现主要包括 SCRUM、XP(Extreme Programming,极限编程)、Crystal Methods****、FDD(特性驱动开发)等等。
其中 SCRUM 与 XP 最为流行:
XP极限编程 更侧重于实践,并力求把实践做到极限。这一实践可以是测试先行,也可以是结对编程等,关键要看具体的应用场景。
SCRUM是一种开发流程框架,也可以说是一种套路。SCRUM 框架中包含三个角色,三个工件,四个会议,听起来很复杂,其目的是为了有效地完成每一次迭代周期的工作。
1.5. TDD: 测试驱动开发(Test-Driven Development)
TDD是敏捷开发
中的一项核心实践和技术,也是一种设计方法论。其基本思想是:在明确要开发某个功能后,在开发功能代码之前,先编写测试代码,然后编写功能代码并用测试代码进行验证,如此循环直到完成全部功能的开发。 这有助于编写简洁可用和高质量的代码,并加速开发过程。
TDD的基本思路就是通过测试来推动整个开发的进行,但测试驱动开发并不只是单纯的测试工作,而是把需求分析,设计,质量控制量化的过程。
TDD虽是敏捷方法的核心实践,但不只适用于XP(Extreme Programming),同样可以适用于其他开发方法和过程。
为了充分理解TDD的思想和原理,现假设一个需求场景,并运用TDD的方法实现该需求:
———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
1.5.1. 需求描述
实现一个检查给定数字是否为质数的函数(is_prime)
1.5.2. 利用TDD的思想实现该需求的基本思路:
1.5.2.1. 需求分析
- 接收一个参数,且为数值类型;
- 判断该数值是否为质数,返回boolean值.
1.5.2.2. 设计测试用例(开发功能代码前先实现测试代码)
assert is_prime(5)
assert is_prime(8)
assert is_prime(0)
assert is_prime(1)
assert is_prime(-3)
1.5.2.3. 开发测试代码
def test_is_prime():
assert is_prime(5)
assert not is_prime(8)
assert not is_prime(0)
assert not is_prime(1)
assert not is_prime(-3)
1.5.2.4. 开发功能代码(使用测试代码验证功能代码,驱动功能完善)
# 开发功能代码 标记版本为:v1
def is_prime(number):
for element in range(2,number):
if number % element == 0:
return False
return True
1.5.2.5. 使用测试代码对V1版本的功能代码进行测试
if __name__ == '__main__':
test_is_prime() #执行测试用例检查函数实现是否正确
此时测试结果展示用例assert not is_prime(0)
检查出功能实现存在缺陷:
Traceback (most recent call last):
File "****", line xxx, in test_is_prime
assert not is_prime(0)
AssertionError
1.5.2.6. 更新功能代码至v2版本,使用测试代码再次测试
# 修改后,标记版本为:V2
def is_prime(number):
if number in (0,1):
return False
for element in range(2,number):
if number % element == 0:
return False
return True
测试结果展示用例is_prime(0)
检查出功能实现存在缺陷:
Traceback (most recent call last):
File "****" line xxx, in test_is_prime
assert not is_prime(0)
AssertionError
1.5.2.7. 更新功能代码至v3版本,使用测试代码再次测试
# 修改后,标记版本为:V3
def is_prime(number):
if number <0 or number in (0,1):
return False
for element in range(2,number):
if number % element == 0:
return False
return True
本次测试用例全部通过,功能实现在有限用例的验证下已符合需求。
———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
TDD的实施手段是单元测试。
在每次版本改动后,使用测试用例验证了版本修复情况,同时也验证了本次改动是否引起回归问题。由此,TDD中测试代码的作用可以总结为:在被测代码发生改动后,执行单元测试用例即可验证本次改动是否对函数原有功能造成影响,是未来函数重构的信心保证。
TDD三定律
在编写不能通过的单元测试前,不可编写生产代码。
只可编写刚好无法通过的单元测试,不能编译也算不通过。
只可编写刚好足以通过当前失败测试的生产代码。
测试与生产代码一起编写,测试只比生产代码早写几秒钟。
1.6. Clean Code
Clean Code 即整洁代码,落实到我们工程师日常coding中就是如何写出看上去干净、逻辑清晰、有一定抽象能力扩展性好的代码。
《Clean Code(评注版)》提出一种观念:代码质量与其整洁度成正比。干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好的基础。
开发规范,一个团队共同认可的规范是Clean Code的基石,大概包含如下部分:
1.6.1. 1. 代码格式化
代码格式化:其一是统一的格式化插件、模板;另一是格式化的意识。
如果没有团队维度的代码格式插件、模板,团队每个人的代码那真是”千人千面”了:最重要的多个开发共同修改一个分支、批次之前如果都去格式化共同的类,结果会非常酸爽,merge的开销非常大,直接影响团队的整体效率。
其二就是要培养团队内开发成员们格式化的意识,每次push前养成format code的习惯。
1.6.2. 2. 开发规范
开发规范就是Clean Code的背书了,或者是像一个一个具体的约束细则。
关于这一项,java世界国内国外各自两个典范:一个是google,一个是阿里。
1.6.2.1. 阿里巴巴Java开发手册(推荐)
alibaba/p3c: https://github.com/alibaba/p3c
开发的同时可以配套的阿里巴巴开发手册的检查插件,结合idea内置的代码检查可以明确地帮助开发者修复问题代码,推荐下载配置。
1.6.2.2. google Java开发规范
Google Java Style Guide: https://google.github.io/styleguide/javaguide.html
1.6.3. 3. cr标准
cr标准是开发规范的落地阶段。
每个团队必不可少的及时cr,如何在cr阶段去发现问题代码、不干净的代码是个问题。
除开逻辑上的cr,代码规范、整洁的检查很重要。可以参考如下部分标准和规范:
0. 遵从阿里巴巴开发手册里的所有规范
1. 及时抽取重复代码
2. 避免过长方法(>=50行 or 一屏)
太长方法或者功能太多的类一定要及时进行重构,不然最受影响的是开发者本人及系统交接人。
3. 去除不必要的代码注释/main方法
代码是最好是自描述的,无用或不必要的注释会浪费磁盘空间。
但是自描述这个要有度,如果是自描述不了的比如复杂的业务逻辑(这种建议进行业务重构)或者一段算法还是需要有详细的注释的。
4. 合理地设置方法及变量的private/public/protected属性(先闭后开)
5. 适当地清除IDE提示的代码问题(warning)
6. 使用稳定的第三方包进行代码简化,切忌造轮子
7. 免除重复无用的代码coding(如:lombok)
1.6.3.1. Maven相关的规范: Clean Maven
总体目标:工程使用的依赖才引入。不要copy-paste其他工程的pom
文件造成大量的无用引用,使得自身工程笨重不堪。
1. 在
dependencyManagement
中管理version
/exclusion
/scope
等jar包属性.dependencies
中负责实际引入不去关心以上所有信息,即使是单module工程也应该遵循改规则。2. 使用tc组件的系统尽量使用TC-POM去管理组件的jar包,规避组件之间的版本管理.
3. 使用第三方jar包,最好能够在
pom
文件中加上注释,并附上说明文档的链接.
1.7. 单元测试: Unit Testing
单元测试是开发人员编写的、用于检测在特定条件下目标代码正确性的代码; 是指对软件中的最小可测试单元进行检查和验证。
对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义:
如:
C语言中单元指一个函数;
Java里单元指一个类;
图形化的软件中可以指一个窗口或一个菜单等.
总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。
要进行充分的单元测试,应专门编写测试代码,并与产品代码隔离。比较简单的办法是为产品工程建立对应的测试工程,为每个类建立对应的测试类,为每个函数(很简单的除外)建立测试函数。
1.7.1. 单元测试通过后有什么意义呢?
假设我们的代码中有一个abs()函数:如果我们对abs()函数代码做了修改,只需要再跑一遍已有的单元测试,如果通过,说明我们的修改不会对abs()函数原有的行为造成影响,如果测试不通过,说明我们的修改与原有行为不一致,我们需要修改代码或修改相应的测试。
总结:
单元测试可以有效地测试某个程序模块的行为,是未来重构代码的信心保证。
单元测试的测试用例要覆盖常用的输入组合、边界条件和异常。
单元测试代码要非常简单,如果测试代码太复杂,那么测试代码本身就可能有bug。
单元测试通过了并不意味着程序就没有bug了,但是不通过程序肯定有bug。
1.8. (代码)覆盖率: Coverage
测试覆盖率是衡量测试完整性的一种手段。
从广义的角度来讲,覆盖率主要分为两大类:
- 面向项目的需求覆盖率
- 更偏向技术的代码覆盖率
现在人们口中的测试覆盖率,通常默认指代码覆盖率,而不是需求覆盖率。
定义:
代码覆盖率=至少被执行了一次的【条目数】占整个【条目数】的百分比
如果【条目数】是语句/代码行,对应的就是代码行(line)覆盖率;
如果【条目数】是函数/方法,对应的就是函数/方法覆盖率(methdod);
如果【条目数】是路径,那么对应的就是路径覆盖率(Path).
统计代码覆盖率的根本目的是找出潜在的遗漏测试用例,并有针对性的进行补充,同时还可以识别出代码中那些由于需求变更等原因造成的不可达的废弃代码。
进一步考虑,代码覆盖率可以看作是产品代码质量的间接指标--之所以说是间接指标,因为测试覆盖率评价的是测试代码的质量,并不是产品代码的质量。
代码覆盖率是一种白盒测试,因为测试覆盖率是评价产品代码类内部的指标,而不是评价系统接口或规约。测试覆盖率尤其用于评价测试代码是否已经覆盖了产品代码所有的路径。
通过测试覆盖率我们可以知道测试是否充分,还存在哪些潜在的风险和弱点,指导测试人员有目的补充增加覆盖率的测试用例:
【绿色】:代码被执行过
【黄色】:代码部分被执行过
【红色】:代码没有被执行过
当然,我们也无需机械的追求100%的覆盖率;因为这不仅提高了成本,而且即便覆盖率达到了100%也仍会有未被用例设计到潜在问题。
在实际项目中,无论覆盖率多高,没有根据需求正确的写assert其实也是无法利用测试用例发现bug,提高代码质量,在实际的测试用例中,正向的case一般比较容易写,难得是测试error handling和模拟各种异常情况下的代码行为。
总结来讲,高的代码覆盖率不一定能保证软件的质量,但是低的代码覆盖率一定不能能保证软件的质量。
2. 单元测试的几个原则: F / I / R / S / T
2.1. Fast: 运行快速
测试运行一定要足够快。通常一个测试的方法完成运行过程的时间为毫秒量级。
2.2. Independent: 独立
测试应该相互独立: Be isolated.
每个测试方法都可独立运行,以及可以以任何顺序运行测试。
单元测试不负责检查跨类或跨系统的交互,这些应该是集成测试的领域;也不依赖于环境配置参数(Sys-Environment
)/第三方工具(3rd Party)/IO
/DB Connection
/Spring Container
/其他程序部件(class
)及模块(module
)/网络等。
测试方法中也不应存在if-else
/switch
/catching
/exception
代码块。
在这里可以解释一下:
- 对于DAO层代码,我们一般不会做(典型的)单元测试。规范来说,DAO层的测试一般在集成测试环节完成。
虽然这样说,但DAO层的代码仍可以编写Unit-Test的相关代码(通常借助Mock-Test).
- 对于Controller/Service层代码,我们需要做单元测试。
Controller层也可借助: Utillize
MockHttpServletRequest
/MockHttpServletResponse
/MockHttpSession
等Mock对象来完成测试。
2.3. Repeatable: 可重复
测试应当可在任何环境中重复通过,不受任何外界环境影响。
2.4. Self-Validating: 自足验证
测试应以布尔值输出: Derectly tells pass or not.
另外,测试方法不建议设置为main方法。
2.5. Timely: 及时
测试应及时编写: Sync up with the code.
单元测试应该恰好在使其通过的生产代码之前编写。
在git-commit之前,确保单元测试通过。
2.6. 其他相关原则
尽可能减小测试粒度。
只有测试粒度小,出错时才能尽快定位到出错的位置。在Maven项目中,单元测试代码一定要写在
src/test/java
工程目录;不允许写在业务代码目录下。
源码构建时会跳过测试代码目录,而单元测试的默认框架会默认扫描测试代码目录。单元测试的基本目标:语句(行)覆盖率不低于70%; 核心模块的语句/分支覆盖率要达到100%。
核心业务、应用、模块的增量代码应及时修正,确保单元测试通过。
新增代码要及时补充单元测试;如新增代码影响了原有的单元测试,要及时修正。对于数据库相关的查询、更新、删除等操作,不可假设数据是存在的,或直接手动编辑数据库(插入/改写数据)。要使用程序插入或导入数据的方式来准备数据。
与数据库相关的测试,可以设定自动回滚机制,避免造成数据库脏数据。或对单元测试产生的数据加以明确的前后缀标识。
对于不可测的代码建议重构。
也就是说,如果一段代码不可测或不好测,很可能是因为这段代码不够合理。
为了更方便做单元测试,业务代码应避免如下问题:
- 构造方法中做的事情太多;
- 存在过多的全局变量和静态方法;
- 存在过多的外部依赖;
- 存在过多的条件语句。
- 单元测试尽量遵守BCDE原则,以保证被测试模块的交付质量。
BCDE原则:
- Border: 边界值测试。包含: 循环边界、特殊取值、特殊时间点、数据顺序等。
- Correct: 正确的输入,正确的预期结果。
- Design: 结合设计文档来编写单元测试。
- Error: 强制错误信息输入(如: 非法数据、异常流程、非业务允许输入等)并得到预期结果。
3. 关于Mock
在面向对象程序设计中,模拟对象(mock object)是以可控的方式模拟真实对象行为的假的对象。程序员通常创造模拟对象来测试其他对象的行为,很类似汽车设计者使用碰撞测试假人来模拟车辆碰撞中人的动态行为。
在单元测试中,模拟对象可以模拟复杂的、真实的(非模拟)对象的行为, 如果真实的对象无法放入单元测试中,使用模拟对象就很有帮助。
简单来说,Mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。Mock对象就是真实对象在调试期间的代替品。
在如下情形下,可能比较适合使用模拟对象来代替真实对象:
- 真实对象的行为是不确定的(例如,当前的时间或当前的温度);
- 真实对象很难搭建起来;
- 真实对象的行为很难触发(例如,网络错误);
- 真实对象速度很慢(例如,一个完整的数据库,在测试之前可能需要初始化);
- 真实的对象是用户界面,或包括用户界面在内;
- 真实的对象使用了回调机制;
- 真实对象可能还不存在;
- 真实对象可能包含不能用作测试(而不是为实际工作)的信息和方法。
常见的Mock-Test工具包含:
- Mocktio: https://site.mockito.org/
- jMock: http://jmock.org/
- EasyMock: http://easymock.org/
4. 常用测试框架: JUnit/Spring Test/TestNG/
4.1. JUnit
JUnit是个优秀的单元测试框架,严格的遵守一个实现类一个测试类的方式。
JUnit是起源于一个统称为xUnit的单元测试框架之一。xUnit是一套基于测试驱动开发的测试框架,在c++,paython,java语言中测试框架的名字都不相同。
4.2. TestNG
在Java生态中,最为流行的测试框架应该是JUnit和TestNG,它们的功能也十分相似。
与JUnit相比,TestNG既包涵了JUnit的单元测试的功能,同时也可以进行集成测试。我们仅需对功能点(接口)编写相应的集成测试,这能减少大量的代码量。
而对JUnit来说,遇到Spring项目的代码,通常分为Dao层、Service层、Controller层;即便只是完成一个小功能,都需要编写多个测试类,来完成测试。这中间会耗费许多的时间。
测试人员一般使用TestNG来写自动化测试;而开发人员一般用使JUnit写单元测试。
4.3. Spring Test
Spring Test与Spring框架是绑在一起的。
大部分国外的开源框架都集成了测试所需的一些工具类,如Spring Boot有单独的一节讲解测试。
在这里我们一般会需要用到它的一个TestNG支持的抽象类AbstractTransactionalTestNGSpringContextTests
,这个类的用于初始化Spring环境以及添加事务支持。
5. 相关第三方辅助工具
5.1. EclEmma: 基于Java的开源软件测试Eclipse插件
EclEmma能够对Java程序进行覆盖测试,可以结合JUnit/TestNG使用。
EclEmma直接对代码覆盖进行分析;覆盖结果将立即被汇总并在Java源代码编辑器中高亮显示。
同时EclEmma也可对程序运行的结果生成详尽的覆盖测试报告。
5.2. JaCoCo: 基于Java的开源覆盖率工具
JaCoCo: https://github.com/jacoco/jacoco 是一款基于Java代码的主流开源覆盖率工具,可以很方便地嵌入到 Ant、Maven 中,并且和很多主流的持续集成工具以及代码静态检查工具,如 Jekins 和 Sonar 等,都有很好的集成。
5.3. SonarQube: 代码的质量/测试分析工具
SonarQube®: https://www.sonarqube.org/ 是一种自动代码审查工具,用于检测代码中的错误,漏洞和代码异味。
它可以与现有的工作流程集成,以便在项目分支和拉取请求之间进行连续的代码检查。
SonarQube基于本地服务和mvn命令进行的代码分析,并将分析结果推送到sonar服务器中。