极快,秒级
较慢,分钟级
缓慢,日间水平
覆盖区域
代码行覆盖率 60-80% 分支覆盖率 40-60%
功能级别覆盖
核心保障环节
环境依赖性
代码级别,与环境无关
依赖日常或当地环境
取决于暂存或生产环境
外部依赖模拟
所有模拟
部分模拟
没有模拟,完全使用真实环境
其次,为什么需要单元测试?
益处:
提高系统稳定性,方便迭代。
有利于深入了解技术和业务。
单次测试成本低、速度快。
单一测试是最好的、自动化的、可执行的文档。
单一测试驱动的设计提高了代码的简单性和标准化,确保了安全的重构。 代码修改后,单测依然可以通过,可以增强开发者的信心。
快速反馈,比集成测试更快发现问题、定位缺陷更快、更准确,降低修复成本。
开发成本低:
1、
最直观的想法:
2、
这个想法确实是最直观的。 但这只是想到第一层。 如果我们把开发过程的所有步骤加起来,我们会发现是这样的:
开发过程的背后,几乎每个过程都可能抛出Bug。 后期抛出的Bug越多,程序员比开发阶段投入的时间和业务就越多,承担的风险也最高。
下图也说明了两个问题:第一,85%的缺陷是在代码设计阶段产生的; 其次,发现Bug的阶段越晚,成本就越高,呈指数级增长。 这种“指数成本”的情况经常发生。 当我们修正一个bug时,可能会出现另外三个bug,俗称:修正崩溃。
因此,在早期的单元测试中就可以发现Bug,不仅节省了时间和精力,提高了开发过程中的效率,还降低了重复修改的风险和时间成本。
3.关于单元测试的一些误解
知乎2、5、6、9、11
误区 1:单元测试会减慢开发过程
事实是:像任何新工具一样,习惯单元测试需要一点时间,但总的来说,单元测试可以节省时间并浪费更少的时间。 事实上,执行回归测试可以让您无忧无虑地持续推进开发过程。 如果在日常构建期间进行单元测试,这样的测试将不会占用开发时间。
误解二:项目一旦结束,单元测试投入的工作就白费了
一点也不。 如果您曾经重用过代码,您就会意识到您所做的一切都是一种资产。
事实是:当您在一个项目中使用以前为另一个项目编写的代码或编辑此代码时,您可以使用相同的单元测试,也可以编辑这些单元测试。 在同一个项目中使用类似的测试代码片段是没有问题的。
误区 3:单元测试是浪费时间
你需要弄清楚什么是浪费时间?
一遍又一遍地修复相同的漏洞
在整个开发过程中编写或重写验证代码
修复了一个错误,却又突然在其他地方出现了另一个错误
我在写代码的时候不小心被打断了,我不知道该怎么办。
对单元测试的抵制是可以理解的,但是许多开发人员在使用单元测试完成项目之前不会称赞单元测试有多好。
事实是:您只需要编写一次单元测试,但可以多次运行它。 这与您对其他代码所做的更改无关。 从长远来看,最初的投资将会得到回报。
误区四:单元测试无助于程序的调试,或者无法防止漏洞的发生。
绝对不是这样的。 单元测试可以使程序调试更容易,因为您可以专注于有问题的代码,修复问题,然后再次合并修改后的代码。 它还可以防止在添加功能时引入漏洞,并防止问题再次出现,特别是在使用面向对象方法进行编程时。 单元测试不能保证100%消除漏洞,但它是减少漏洞的好方法。
事实是:虽然单元测试不能解决你在调试过程中遇到的所有问题,但是当你发现漏洞时,单元测试中隔离的代码可以让修复漏洞变得更加容易。 根据开发人员中单元测试的铁杆粉丝的说法,单元测试的最大好处是它使程序的调试变得非常容易和简单。
误区五:使用单元测试进行程序调试并不能提供全面的覆盖
仅仅因为您无法调试整个代码并不意味着调试覆盖范围不全面。 使用单元测试进行程序调试至少比其他类型的调试要好。 事实上,单元测试有一个非常显着的优势:(如果不是大大消除的话)大大减少我上面提到的报告的漏洞数量。 在开发和调试程序时,重现错误可能会非常令人沮丧。 通过单元测试,您可以减少添加、修改和删除功能时引入新漏洞的频率。 调试一直是“全覆盖”,特别是当程序运行的设备或系统差异很大时。
事实是:尤其是在处理漏洞时,单元测试可以确保您发现从未报告过的漏洞。 而且在调试程序时,不需要检查整个代码,只需要修改出现漏洞的地方即可。
4.主流的单元测试框架有哪些? Junit:Junit 是最常用的 Java 单元测试框架之一。 它提供了一组简单易用的API来轻松编写和运行单元测试。 Junit的主要优点包括易学易用、用途广泛、生态丰富等,但缺少一些高级功能,比如模拟对象等:是一个针对mock对象的Java单元测试框架,可以帮助开发者创建和管理模拟对象以进行实际单元测试。 主要优点包括功能强大、易学易用、支持扩展等,但可能会带来一些性能问题。 Spock:Spock是一个基于语言的Java单元测试框架,它提供了一组简洁易读的DSL(领域特定语言),允许开发人员轻松编写和运行单元测试。 Spock的主要优点包括易于阅读和维护、提供丰富的断言库、支持数据驱动测试等,但需要额外的编译器支持。 但其兼容性比较差。 如果依赖的版本稍有错误,就会报一些莫名其妙的错误,而且提示不明显,给排查问题带来困难。 :是一个Java测试框架,类似于Junit,提供了一套强大的测试功能,包括支持多线程测试、数据驱动测试、分组测试等。主要优点包括功能丰富、易于扩展、与各种持续集成工具集成,但缺乏一些高级功能,例如模拟对象。 :是Java单元测试的扩展框架,可以帮助开发人员编写更灵活的单元测试。 主要优点包括支持静态方法、私有方法等测试,易学易用,能够与其他测试框架配合使用等,但可能会引入更多的复杂度和维护成本。 5. 各框架的使用示例
J单元:
import org.junit.Test;
import static org.junit.Assert.*;
public class MyTest {
@Test
public void testSomething() {
// 执行测试代码
assertEquals(2 + 2, 4);
}
}
:
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");
}
}
斯波克:
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
}
}
:
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;
}
}
:
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";
}
}
上面只是一个简单的例子。 在实际使用中,需要根据具体情况进行配置和编写测试代码。
6. 框架和特性的比较。 Mock 函数支持私有方法和静态方法。 代码可读性。 学习成本。 语法概念。 可扩展性和定制性。 集成和兼容性。 文档和社区支持。 性能稳定。
朱尼特
×
×
高可读性
低的
简单易懂
贫穷的
-测试默认集成
好的
好的
√
×
高可读性
低的
简单的API,优秀的文档
贫穷的
-测试默认集成
好的
好的
×
√
平均可读性
中等的
语法简单,但是依赖配置繁琐
提供丰富的扩展点和插件机制
好的
一般来说,文档相对较少
好的
√
√
代码又长又复杂,可读性不太好
中等的
语法比较复杂
提供丰富的扩展点和插件机制
好的
好的
好的
斯波克
√
√
需要懂语法
高的
基于语言的复杂语法
提供丰富的扩展点和插件机制
兼容性差
好的
好的
总结:推荐使用Junit+
7、如何进行单次测试? 1、单一测试使用场景的代码复用率。 代码复用率越高,就越有必要实施单测,也就越有必要提高单测的要求。 因此,这些代码被很多业务方引用,所以一旦出现问题,就会影响很多业务方。 对此类代码实施单一测试更有利可图。 业务变化率。 业务变化越快,就越不适合使用单一测试。 如果业务变化很快,上线后几天内需要修改单测试的内容,那么不仅需要修改业务代码,还需要修改单测试代码,工作量是双倍的。 人员流动率。 人员变动率是指某个模块负责人的变动情况。 如果某个模块的负责人经常变动,则不适合单独测试。 因为新的负责人需要花费大量的时间来熟悉单测的内容,这会导致需求开发时间变得很长。 商业重要性。 业务越核心,越需要实施单项测试,越需要达到高标准。 因为核心业务的稳定性和健壮性对于公司来说肯定是非常重要的,而单次测试确实可以在最小的单元内提高系统的稳定性和系统健壮性。
我们不能孤立地看待上述四个衡量维度,而必须根据实际情况进行综合判断,才能得出最合适的标准!
2.好的单元测试必须遵守原则。 ①空气原理
注意:当单元测试在线运行时,感觉就像空气(AIR),但保证测试质量非常关键。 从宏观上看,好的单元测试具有自动化、独立性、可重复执行的特点。
②第一原则
1.F-Fast(快速)
单元测试应该能够快速运行。 在各种测试方法中,单元测试运行速度最快。 大型项目的单元测试通常应在几分钟内运行。
2. I-(独立)
单元测试应该能够独立运行。 单元测试用例彼此不依赖,也不依赖任何外部资源。
3. R-(可重复)
单元测试应能够稳定、重复运行,每次运行的结果应稳定、可靠。
4. S-(自我验证)
单元测试应该由用例自动验证,不能依赖手动验证。
5. T-(及时)
单元测试必须及时编写、更新和维护,以确保用例能够随着业务代码的变化动态保证质量。
3、按照BCDE原则编写单元测试代码,保证被测模块的交付质量。 4. 哪些场景需要编写单个测试?
核心业务、核心应用、核心模块的增量代码保证单元测试通过。
单元测试是软件开发最基本的手段,而软件开发中最重要的思想之一就是分层的思想。 每个高层模块由多个低层模块组成。 如果与一些不稳定的低层模块组装在一起,高层模块也会变得不可靠。 就像数学王国一样,有几个基本公理,都是通过证明一层层严格构造出来的。 另外,在写业务的时候,我们应该更加注重传统三层模型中的分层思维,而不是教条地说所有业务都有这三层。 以层次化思想为指导思想,复杂的业务层次较多,简单的业务层次较少。
-> -> 外部接口
— — — — — — — — — — — — — — —> Dao 数据库
:负责接受请求并返回响应,以及简单的参数验证。 对于未经验证的逻辑,不需要进行单独的测试。 复杂的验证逻辑需要单次测试。主要用于集成测试
:积木的角色,负责布置业务逻辑,处理业务逻辑,处理来自层的请求,以及访问dao层并需要编写单体测试。
:①负责协调多层组件,处理服务层之间的交互,需要单次测试。 ② 对外接口进行封装,无需额外处理逻辑,无需单独测试。 ③与Dao层交互、控制事务需要单次测试。
Dao:执行数据库相关操作。 复杂的逻辑需要编写单个测试,纯粹是为了取数据或者更新,基本上不做单个测试。
如何在不污染测试数据库的情况下进行DAO层的单一测试?
使用嵌入式数据库:可以使用嵌入式数据库,如H2等,进行单元测试。 这些嵌入式数据库可以在内存中运行,因此不会污染生产数据库中的数据。
使用事务回滚:可以使用事务回滚来保证测试不会污染数据库中的数据。 在测试方法开始之前,会启动一个事务。 测试完成后,事务回滚,使所有修改的数据恢复到测试前的状态,从而避免数据污染。 (推荐使用)
@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);
}
}
使用数据库迁移工具:可以使用数据库迁移工具,如 等进行单元测试。 这些工具可以在每次测试前自动创建一个新的数据库实例,并使用数据库迁移脚本来初始化数据,从而避免数据污染的问题。
创建镜像并启动数据库。
简而言之,编写单元测试的目的是保证各个模块的正确性和可靠性。 只有各个模块都经过单元测试验证后,才能组合成一个稳定可靠的整体。
8.讨论问题:
1.对于遗留项目,代码混乱,难以编写单个测试,且无法推翻重写。 如何优雅的添加单个测试?
2、写完单元测试后,如果功能发生变化,是先改代码还是再写单元测试?
测试和编码可以比作人类的两条腿。 那么问题来了,我应该用左腿先走路还是右腿先走路? 我想大家都会觉得这个问题的答案没有什么意义,但是如果你仔细思考一下走路的话,你会发现左右腿的协调是我们走路的关键。 若步长,一腿跛,或一脚跳,皆是不好。 类比单元测试,同样如此,测试驱动编码,编码优化测试。 臃肿的编码就像迈出了太大的一步; 单次测试不好就意味着一条腿瘸了; 如果你不写一个测试,那就是一场单腿游戏。