前言
测试包含以下四种类型
-
单元测试(Unit Test)
指对程序中的最小可测试单元进行验证。进行单元测试,可以尽早地发现编写代码中错误,减少后期测试开销和维护成本,提高软件质量
-
集成测试(Integration Test)
集成测试是在单元测试的基础上,将所有已通过单元测试的模块按照概要设计的要求组装为子系统或系统,并进行测试的过程。实践表明,一些模块虽然能够单独地工作,但并不能保证连接起来也能正常的工作。一些局部反映不出来的问题,在全局上很可能暴露出来
-
系统测试(System Test)
系统测试是将经过集成测试的系统或子系统,与系统中其他部分结合起来,在实际运行环境下对系统进行的一系列严格有效地测试,以发现软件潜在的问题,保证系统的正常运行。
-
验收测试(Acceptance Test)
验收测试是部署软件之前的最后一个测试操作。它是技术测试的最后一个阶段,也称为交付测试。验收测试的目的是确保软件准备就绪,按照项目合同、任务书、双方约定的验收依据文档,向软件购买方展示该软件系统满足原始需求。
四种测试各个方面区别
单元测试 | 集成测试 | 系统测试 | 验收测试 | |
---|---|---|---|---|
测试阶段 | 编码后或编码前 | 单元测试通过后 | 集成测试通过后 | 系统测试通过后 |
测试对象 | 最小模块 | 模块间的接口,多模块组成的子系统 | 整个系统 | 整个系统 |
测试内容 | 模块接口测试、局部数据结构、路径测试、错误处理测试、边界测试 | 模块之间数据传输、模块之间功能冲突、模块组装功能正确性、全局数据结构、单模块缺陷对系统的影响 | 功能、界面、可靠性、易用性、性能、兼容性、安全性等 | 同系统测试 |
测试人员 | 开发人员 | 开发人员 | 测试人员 | 需求方或最终用户 |
测试方法 | 白盒测试 | 黑盒测试+白盒测试 | 黑盒测试 | 黑盒测试 |
一、单元测试
1.1 基本原则
一个好的单元测试应该具备以下FIRST 原则和AIR原则中的任何一条
- FIRST 规则
- Fast 快速原则,测试的速度要比较快
- Independent/Isolated 独立/隔离原则,每个测试用例应该互不影响,不依赖于外部资源
- Repeatable 可重复原则,同一个测试用例多次运行的结果应该是相同的
- Self-validating 自我验证原则,单元测试可以自动验证,并不需要手工干预
- Thorough/Timely 及时原则 单元测试必须及时进行编写,更新,维护。保证测试用例随着业务动态变化
- AIR原则
- Automatic 自动化原则 单元测试应该是自动运行,自动校验,自动给出结果
- Independent 独立原则 单元测试应该独立运行,相互之间无依赖,对外无依赖,多次运行之间无依赖
- Repeatable 可重复原则 单元测试是可重复运动的,每次的结果都稳定可靠
一个整套完善的单元测试可以保障后续的增添功能时,程序迭代过程中,代码的逻辑正确性。验证程序的输入和输出与最初设计一致。这对后续的集成测试等会提供巨大的帮助。同时也会有利于集成测试的顺利进行
1.2 粒度
在使用 springboot 开发的项目中,可以考虑以下粒度做测试
- DAO: 对于框架提供的基本CRUD,可以考虑跳过这一部分单元测试,而一些比较复杂的动态更新、查询等操作,建议用使用H2去做模拟单元测试
- Service:基本上一个service里面肯定会依赖很多其他的service(此处也建议将成员变量通过构造方法进行注入,以便于单元测试去Mock),此时建议我们将依赖其他service的方法用Mock替代,Service里面的一些数据库的操作也进行Mock。这样可以保证service测试的独立性,不过对于逻辑复杂的方法可能要花很多时间在Mock上面。 如果发现需要Mock的方法过多,那么可能就需要考虑将要测试的方法是不是需要重构
- Controller(API):主要着重测试HTTP status在 200,400,500 等情况下的异常处理,request及response的转换等。由于其余部分的代码测试都已经在其对应的单元测试覆盖,那么此时可以Mock绝大部分Serivce层中的方法
- 工具类:一些工具类里面包含了比较多的逻辑,所以需要尽可能考虑多种情况下测试用例
1.3 JUnit
JUnit,一个由Kent Beck和Erich Gamma于1997年建立的Java单元测试框架,现在主要使用的有两个版本JUnit4 和JUnit5
-
JUnit4
-
常用注解,位于org.junit包下面
注解名 含义 @After 每个测试用例之后都将调用@After注解方法 @AfterClass 在测试类中的所有测试方法之后执行。注解在静态方法上 @Before 每个测试用例之前都将调用@Before注解方法 @BeforeClass 在测试类中的所有测试方法之前执行。注解在静态方法上 @Test 标记方法为测试方法 -
断言方法,位于org.junit.Assert类内
- assertTrue
- assertFalse
- assertEquals
- assertNotEquals
- assertSame
- assertNotSame
- assertThrows
- …
-
-
Junit5
-
常用注解,位于org.junit.jupiter.api包下面
注解名 含义 @AfterEach 每个测试用例之后都将调用@AfterEach注解方法 @AfterAll 在测试类中的所有测试方法之后执行。注解在静态方法上 @BeforeEach 每个测试用例之前都将调用@BeforeEach注解方法 @BeforeAll 在测试类中的所有测试方法之前执行。注解在静态方法上 @Test 标记方法为测试方法 @Timeout 运行超过指定的时间的话,就会自动报错 @RepeatedTest 重复执行n次 @ExtendWith 为测试类或测试方法提供扩展类引用 -
断言方法,位于org.junit.jupiter.api.Assertions
- assertTrue
- assertFalse
- assertNull
- assertNotNull
- …
-
1.4 Hamcrest
Hamcrest是一个匹配器,可以用来灵活创建、组合表达式进行断言。
在junit自带的断言中,我们会发现都比较简单,如果碰到稍微复杂的,比如判断数组中是否有某个值,就没有对应的断言方法。而Hamcrest内部提供了很多常用的匹配器,可用来实现断言
使用Hamcrest可导入静态方法
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
-
核心
-
anything
- 始终匹配,如果你不关心被测对象是什么,则很有用assertThat("123", is(anything()));
-
describedAs
- 装饰器,添加失败自定义描述assertThat("3", describedAs("a num equal to %0", equalTo(3), 3)); //Expected: a num equal to <3> // but: was "3"
-
is
- 装饰器,用来提高可读性assertThat(123, is(equalTo(123))); assertThat(123, equalTo(123)); // 两者等价
-
-
逻辑
-
allOf
- 如果所有匹配器匹配,则匹配(像 Java中的 &&)assertThat( Arrays.asList("1x", "2x", "3x", "4z"), allOf(isA(List.class), hasSize(4)));
-
anyOf
- 如果有匹配器匹配,则匹配(像Java中 ||)assertThat("123", anyOf(isA(Integer.class), equalTo("234")));
-
both
两个匹配器都匹配则匹配assertThat("123", both(isA(String.class)).and(is(equalTo("123"))));
-
either
两个匹配器任意一个匹配则匹配assertThat("123", either(isA(String.class)).or(is(equalTo("123"))));
-
not
- 如果包装的匹配器不匹配,则匹配,反之亦然assertThat("123", not(isA(Integer.class)));
-
everyItem
所有元素匹配则匹配assertThat( Arrays.asList("1x", "2x", "3x", "4z"), everyItem(endsWith("x")));
-
-
对象
-
equalTo
- 使用 对象的equals方法测试是否相等assertThat(123, equalTo(123));
-
hasToString
- 测试对象的toStringassertThat(true, hasToString("TRUE"));
-
instanceOf
- 测试是否类型为给定值assertThat("123", instanceOf(String.class));
-
isA
instanceOf的简写 -
notNullValue
,nullValue
- 测试是否为nullassertThat("123", notNullValue()); assertThat(null, nullValue());
-
sameInstance
-测试对象是否相同assertThat(127, sameInstance(127)); assertThat(128, sameInstance(128));
-
-
Beans
-
hasProperty
- 测试JavaBeans 是否有某个属性@Data class Demo { private String str; }
assertThat(new Demo(), hasProperty("str"));
-
-
集合
-
array
- 根据匹配器数组测试数组的元素assertThat(new Integer[]{1,2,3}, is(array(equalTo(1), equalTo(2), equalTo(3))));
-
hasEntry
,hasKey
,hasValue
- 测试map是否包含Entry、键或值Map map = Maps.newHashMap("foo", "bar"); assertThat(map, hasEntry("foo", "bar")); assertThat(map, hasKey("foo")); assertThat(map, hasValue("bar"));
-
hasItem
,hasItems
- 测试集合是否包含值assertThat(Arrays.asList("foo", "bar"), hasItem("bar")); assertThat(Arrays.asList("foo", "bar"), hasItem(startsWith("f"))); assertThat(Arrays.asList("foo", "bar"), hasItems("foo", "bar"));
-
hasItemInArray
- 测试数组包含元素assertThat(new Integer[] {1,2,3}, hasItemInArray(4)); assertThat(new String[] {"foo", "bar"}, hasItemInArray(startsWith("ba")))
-
hasSize
测试集合Size是否为给定值assertThat(Arrays.asList("foo", "bar"), hasSize(4));
-
empty
测试集合是否为空assertThat(Arrays.asList("foo", "bar"), empty());
-
contains
测试集合是否包含给定值assertThat(Arrays.asList("foo", "bar"), contains("f"));
-
in
测试给定值是否包含在集合里assertThat("foo", in(new String[]{"foo", "f", "bar"}));
-
-
数字
-
closeTo
- 测试浮点值是否接近给定值assertThat(1.03d, is(closeTo(1.0d, 0.03d)));
-
greaterThan
,greaterThanOrEqualTo
,lessThan
,lessThanOrEqualTo
- 测试大小assertThat(1.03, is(greaterThan(1.0d)));
-
-
字符串
-
equalToIgnoringCase
- 忽略大小写情况下,测试字符串是否相等 -
containsString
,endsWith
,startsWith
- 测试字符串是否包含给定值,结束于给定值,起始于给定值 -
blankString
测试是否空字符串 -
hasLength
测试是否长度为给定值
-
二、集成测试
集成测试可以是以下任何一种:
-
涵盖多个“单元”的测试。它测试两个或多个类之间的相互作用
-
覆盖多层的测试。这实际上是第一种情况的特殊化,可能涉及到业务层和持久层之间的交互
-
覆盖整个应用程序路径的测试。在这些测试中,我们向应用程序发送请求,并检查它是否正确响应,以及是否根据我们的期望更改了数据库状态
Spring Boot为集成测试提供了@SpringBootTest注解,我们可以使用该注解创建一个应用程序上下文,其中包含上述所有测试类型所需的所有对象。然而,请注意,过度使用@SpringBootTest可能会导致测试运行时间过长
因此,对于覆盖多个单元的集成测试,我们应该创建简单测试,非常类似于单元测试,在测试中,我们手动创建测试所需的对象,并模拟其余的对象。这样,Spring就不会在每次启动测试时触发整个应用程序上下文
有以下注解可以帮助我们不必启动整个Spring上下文
-
@WebMvcTest
测试controller层 -
@DataJpaTest
测试jpa持久层
三、Springboot中的测试框架
3.1 测试框架
当引入spring-boot-starter-test时,以下测试相关库SpringBoot都支持
-
JUnit: Java应用程序单元测试的事实上的标准
-
Spring Test & Spring Boot Test: Spring Boot应用程序的集成测试支持
-
AssertJ: 一个流式断言库
-
Hamcrest: 一个匹配器库
-
Mockito: 一个Java Mock框架
-
JSONassert: 一个JSON的断言库
-
JsonPath: 一个读取json文档的DSL
3.2 JUnit版本
由于SpringBoot版本不同,支持的JUnit版本也不同。
-
Spring Boot版本>2.4.0,那么只包含JUnit 5 Jupiter版本
-
2.2.0 <Spring Boot版本 < 2.4.0,包含了JUnit5和4,默认使用了 JUnit 5 ,需要在pom文件中去除JUnit4
-
Spring Boot版本 < 2.2.0 只包含JUnit4
3.3 Mockito
Mockito是一个用 Java 开发,功能强大的模拟测试框架, 通过 Mockito 我们可以创建和配置 Mock 对象, 进而简化有外部依赖的类的测试。
Mockito可以用来模拟方法的返回值,当service层需要调用dao层的一个方法时,可以用mockito来模拟dao层方法,而不是真正的连接数据库执行dao层方法,这就符合单元测试的原则:快速,无依赖
class UserServiceTest {
@InjectMocks
private UserServiceImpl userService;
@Mock
private UserDao userDao;
@BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
void testFindUserRoles() {
Role role1 = Role.builder().id(1).name("管理员").build();
Role role2 = Role.builder().id(1).name("前端组").build();
List<Role> roleList = Lists.list(role1, role2);
when(userDao.findUserRoles(anyInt())).thenReturn(roleList);
assertThat(userService.findUserRoles(1), is(equalTo(roleList)));
}
}