赞
踩
维基百科中是这样描述的:在计算机编程中,单元测试又称为模块测试,是针对程序模块来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。
单元测试和集成测试使用的测试框架和工具大部分是相同的。
首先需要达成一致的是,无论是单元测试还是集成测试,它们都是自动化测试。为了更好地区分,我们可以这样理解:和生产代码以及单元测试代码在同一个代码仓库中,由开发同学自己编写的,对外部环境(数据库、文件系统、外部系统、消息队列等)有真实调用的测试就是集成测试。
下表中也从各种角度来对比了单元测试、集成测试和系统级别测试(包括端到端测试、链路测试、自动化回归测试、UI测试等)的区别。
单元测试 | 集成测试 | 系统级别测试 | |
---|---|---|---|
编写人员 | 开发 | 开发 | 开发 / 测试 |
编写场地 | 生产代码仓库内 | 生产代码仓库内 | 生产代码仓库内 / 生产代码仓库外 |
编写时间 | 代码发布前 | 代码发布前 | 代码发布前 / 代码发布后 |
编写成本 | 低 | 中 | 高 |
编写难度 | 低 | 中 | 高 |
反馈速度 | 极快,秒级 | 较慢,分钟级 | 慢,天级别 |
覆盖面积 | 代码行覆盖60-80% 分支覆盖40-60% | 功能级别覆盖 | 核心保障链路 |
环境依赖 | 代码级别,不依赖环境 | 依赖日常或本地环境 | 依赖预发或生产环境 |
外部依赖模拟 | 全部模拟 | 部分模拟 | 不模拟,完全使用真实环境 |
好处:
提高系统稳定性,利于迭代。
有利于深度了解技术与业务。
单测成本低,速度快。
单测是最佳的、自动化的、可执行的文档。
单测驱动设计,提升代码简洁度和规范性,确保安全重构,代码修改后,单测仍然能通过,能够增强开发者的信心。
快速反馈,更快的发现问题,定位缺陷比集成测试更快更准确,降低修复成本。
1,
最直观的想法:
2,
这样的想法确实是最直观的。但这只是想到了第一层,如果我们把 开发流程所有步骤 都加进来,会发现是这样的:
在开发过程后面,几乎每个流程都可能抛出 Bug。越是到后面流程才抛出的 Bug,程序员就越是要投入比开发阶段更大的时间和业务,而且所承受的风险也是最高的。
下面这张图,也在说明两个问题:一是 85% 的缺陷都在代码设计阶段产生;二是发现 Bug 的阶段越靠后,耗费成本就越高,呈指数级别的增长。这种 “指数成本” 的案例也经常发生,当我们改正一个 Bug 的时候,可能随之而来又会多出 3 个 Bug,俗称:改崩了。
所以,在早期的单元测试就能发现bug,不仅可以省时省力,在开发流程上提高效率,也能降低反复修改出现的风险和时间成本。
知乎 https://zhuanlan.zhihu.com/p/547068206 2,5,6,9,11
误解1: 单元测试减慢了开发过程
事实是:像任何一种新工具一样,习惯进行单元测试也需要一点时间,不过,总的来说,进行单元测试可以节省时间,同时浪费的时间也会缩短。实际上,进行回归测试可以持续不断地推进开发过程,并且不会有任何担心。假若在日常构建时进行单元测试,那么这样的测试是不会占用开发时间的。
误解2:一旦项目结束,那么投入到单元测试上的工作就废掉了
完全不是这样的。如果你曾经重用过代码,那么你将会意识到你所做的一切都是资产。
事实是:在你在一个项目中采用了以前为另一个项目写的代码,或者对这段代码进行编辑的时候,你可以采用相同的单元测试,也可以对这些单元测试进行编辑。在同一个项目中使用相似的测试代码段也是没有问题的。
误解3:单元测试就是浪费时间
你要弄明白什么才是浪费时间?
一而再再而三地修改同样的漏洞
在整个开发过程中编写或者重写验证代码
修补了一个漏洞,不料在其他地方莫名其妙地出现另一个漏洞
在编写代码期间被意外打断,完全不知道该怎么办
拒绝进行单元测试是可以理解的,不过许多开发人员只有在使用单元测试完成一个项目以后,他们才会称赞单元测试多么的好。
事实是:你只需编写单元测试一次,但可多次运行。这与你对其他代码的修改没有任何关系。一开始进行的投入会得到长期的回报。
误解4:单元测试对程序调试没有任何帮助,或者说不能防止漏洞的出现
绝对不是这样的。单元测试可以让程序调试更加简单,因为这样你就可以把精力集中在有问题的代码上,修补问题,接着再重新合并修改后代码。在增加功能的时候,它还可以防止引入漏洞,尤其在使用面向对象方法编程的时候,它还可以阻止问题令人非常沮丧地反复出现。单元测试不能确保100%的排除漏洞,不过它却是减少漏洞的好方法。
事实是:单元测试虽然不能解决你调试过程中遇到的所有问题,但是在你发现漏洞的时候,单元测试中相互隔离的代码可以让漏洞的修补更加容易。根据开发人员中单元测试的铁杆粉丝所说,进行单元测试的最大好处就是让程序的调试非常容易了,简单了。
误解5:使用单元测试进行程序调试覆盖不全面
这仅仅是因为你不能对整个代码进行调试,但这并不意味着调试覆盖不全面。使用单元测试进行程序调试至少比其他类型的调试效果好。事实上,单元测试有一个非常突出的优点是:(如果不是大大地删除,那么就是)大大地减少汇报上面我所提到的漏洞的数量。在开发和调试程序的时候,重现漏洞是一个令人非常沮丧的事情。通过单元测试,你可以在增加、修改和删除功能的时候减少引入新漏洞的频率。调试从来都是“全覆盖的”,尤其是在程序运行的设备或者系统差异非常大的时候。
事实是:特别是在处理漏洞的时候,单元测试可以确保能找到从来都没有汇报过的漏洞。而且在你进行程序调试的时候,你不需要查看全部代码,只需要修改出现漏洞的地方。
JUnit:
import org.junit.Test;
import static org.junit.Assert.*;
public class MyTest {
@Test
public void testSomething() {
// 执行测试代码
assertEquals(2 + 2, 4);
}
}
Mockito:
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testSomething() {
// 创建模拟对象
MyObject mockObject = mock(MyObject.class);
// 设置模拟对象的行为
when(mockObject.someMethod()).thenReturn("Hello World");
// 执行测试代码
String result = mockObject.someMethod();
// 断言结果是否符合预期
assertEquals(result, "Hello World");
}
}
Spock:
import spock.lang.Specification import spock.lang.Subject class CalculatorSpec extends Specification { @Subject Calculator calculator = new Calculator() def "test add method"() { given: int a = 2 int b = 3 when: int result = calculator.add(a, b) then: result == 5 } def "test subtract method"() { given: int a = 5 int b = 2 when: int result = calculator.subtract(a, b) then: result == 3 } } class Calculator { int add(int a, int b) { return a + b } int subtract(int a, int b) { return a - b } }
TestNG:
import org.testng.annotations.Test; import org.testng.annotations.BeforeMethod; import org.testng.annotations.AfterMethod; import static org.testng.Assert.assertEquals; public class MyTestNGTest { @BeforeMethod public void setUp() { // 在测试方法执行前执行的代码 } @AfterMethod public void tearDown() { // 在测试方法执行后执行的代码 } @Test public void testAddition() { int result = Calculator.add(2, 3); assertEquals(result, 5); } @Test public void testSubtraction() { int result = Calculator.subtract(5, 3); assertEquals(result, 2); } } class Calculator { public static int add(int a, int b) { return a + b; } public static int subtract(int a, int b) { return a - b; } }
PowerMock:
import org.junit.Test; import org.junit.runner.RunWith; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import static org.powermock.api.mockito.PowerMockito.*; @RunWith(PowerMockRunner.class) @PrepareForTest(Example.class) public class ExampleTest { @Test public void testPrivateMethod() throws Exception { Example spy = spy(new Example()); doReturn("mocked value").when(spy, "privateMethod"); String result = spy.publicMethod(); assertEquals("mocked value called from publicMethod", result); } } class Example { public String publicMethod() throws Exception { return privateMethod() + " called from publicMethod"; } private String privateMethod() { return "privateMethod"; } }
以上仅是举了一个简单的例子,实际使用时还需要根据具体的情况进行相应的配置和编写测试代码。
框架和特点 | Mock功能 | 支持私有方法,静态方法 | 代码可读性 | 学习成本 | 语法概念 | 可拓展性和定制性 | 整合性和兼容性 | 文档和社区支持 | 性能和稳定性 |
---|---|---|---|---|---|---|---|---|---|
Junit | × | × | 可读性高 | 低 | 简单易理解 | 较差 | spring-test默认集成 | 良好 | 良好 |
Mockito | √ | × | 可读性高 | 低 | 简单的 API、优秀的文档 | 较差 | spring-test默认集成 | 良好 | 良好 |
TestNg | × | √ | 可读性一般 | 中等 | 语法简单,但依赖配置繁琐 | 提供了丰富的拓展点和插件机制 | 良好 | 一般,文档相对较少 | 良好 |
PowerMock | √ | √ | 代码冗长复杂,可读性不高 | 中等 | 语法相对较复杂 | 提供了丰富的拓展点和插件机制 | 良好 | 良好 | 良好 |
Spock | √ | √ | 需要懂Groovy语法 | 高 | 基于Groovy语言,语法复杂 | 提供了丰富的拓展点和插件机制 | 兼容性较差 | 良好 | 良好 |
总结:建议使用Junit+Mockito
上面提到的 4 个衡量维度,我们不能单一地去看待,而是要根据实际情况去综合判断,得出一个最适合的标准!
说明:单元测试在线上运行时,感觉像空气(AIR)一样感觉不到,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。
1、F-Fast(快速的)
单元测试应该是可以快速运行的,在各种测试方法中,单元测试的运行速度是最快的,大型项目的单元测试通常应该在几分钟内运行完毕。
2、I-Independent(独立的)
单元测试应该是可以独立运行的,单元测试用例互相之间无依赖,且对外部资源也无任何依赖。
3、R-Repeatable(可重复的)
单元测试应该可以稳定重复的运行,并且每次运行的结果都是稳定可靠的。
4、S-SelfValidating(自我验证的)
单元测试应该是用例自动进行验证的,不能依赖人工验证。
5、T-Timely(及时的)
单元测试必须及时进行编写,更新和维护,以保证用例可以随着业务代码的变化动态的保障质量。
核心业务、核心应用、核心模块的增量代码确保单元测试通过。
单元测试软件开发的最基础的手段,而在软件开发中,最重要的思想之一,就是分层思想。每个高层的模块都是又多个底层的模块组合而成,如果用一些不稳定的底层模块组装而成,高层模块也将变得不可靠。就像数学王国,是有几个基础的公理,一层一层的通过证明的方式严格构建而成。 另外,编写业务中,传统的 controller、service、dao 的三层模式,我们应该更重视其中的分层思想,而不是教条似的所有业务都这三板斧。以分层思路为指导思想,复杂的业务层次多一点,简单的业务层次少一点。
Controller -> service -> Manager 外部接口
— - - - - - - – - - - – - -> Dao 数据库
Controller:负责接受请求并返回响应,以及参数的简单校验 。对于无校验逻辑可以不做单测。复杂校验逻需要进行单测。主要用于集成测试
Service:搭积木的作用,负责业务逻辑编排,处理业务逻辑,处理来自Controller层的请求,并访问dao层和manager,需要写单测。
Manager:①负责协调多个 Service 层组件并处理服务层之间的交互,需要单测。②对外部接口进行封装,无额外处理逻辑就不需要单测。③与Dao层交互,控制事务,需要单测。
Dao:执行数据库相关操作。复杂的逻辑需要写单测,纯粹的取数据或者更新,基本不做单测。
使用嵌入式数据库:可以使用嵌入式数据库,如H2、HSQLDB等来进行单元测试。这些嵌入式数据库可以在内存中运行,因此不会污染生产数据库中的数据。
使用事务回滚:可以使用事务回滚来确保测试不会污染数据库中的数据。在测试方法开始前,开启一个事务,在测试结束后,回滚事务,这样所有修改的数据都将被还原到测试前的状态,从而避免了数据污染。(推荐使用)
@RunWith(SpringRunner.class) @SpringBootTest public class UserServiceTest { @Autowired private UserService userService; @Test @Transactional public void testAddUser() { User user = new User(); user.setName("John Doe"); userService.addUser(user); // perform assertion User savedUser = userService.getUserByName(user.getName()); Assert.assertNotNull(savedUser); } }
使用数据库迁移工具:可以使用数据库迁移工具,如Flyway、Liquibase等来进行单元测试。这些工具可以在每次测试前,自动创建一个新的数据库实例,并使用数据库迁移脚本来初始化数据,从而避免了数据污染的问题。
打个 docker 镜像里面再启动个数据库。
总之,编写单元测试的目的是为了保证每个模块的正确性和可靠性,只有每个模块都经过了单元测试的验证,才能组合成一个稳定的、可靠的整体。
1,遗留项目,代码混乱不好写单测,且不能推翻重写,如何优雅加入单测?
2,写好单测之后,功能变更,先改代码,还是写单测?
测试与编码,可以类比为人的两条腿。那么这个问题就变成了,走路是先迈左腿还是右腿?我想大家都会觉得这个问题回答没有意义,但细思一下走路,你会发现,左右腿的协同,是我们行走的关键,步子大了、一只腿瘸了、或者一只脚跳着走,都是不好的形态。 类比单元测试,也是一样,测试驱动编码,编码优化测试。编码臃肿了,就像步子太大了;不好的单测,就是一只腿瘸了;你不写单测,那就是单腿游戏了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。