当前位置:网站首页>JUnit5单元测试
JUnit5单元测试
2022-06-21 08:26:00 【油条生煎】
JUnit5单元测试
本文章的编程环境为:使用Gradle构建的SpringBoot工程
参考资料:
- https://junit.org/junit4/
- https://junit.org/junit5/
- https://www.bilibili.com/video/BV1u4411T78k?p=2
概述
简介
JUnit 是一个开源的 Java 语言的单元测试框架
- 专门针对 Java 语言设计,使用最广泛
- JUnit 是事实上的标准单元测试框架
测试驱动开发(TDD)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xfO5dppi-1598015815152)(C:/Users/86189/AppData/Roaming/Typora/typora-user-images/image-20200821151848039.png)]
单元测试的优点
- 确保单个方法运行正常
- 如果修改了方法代码,只需确保其对应的单元测试通过
- 测试代码本身就可以作为示例代码
- 可以自动化运行所有测试并获得报告
单元测试的特点
- 使用断言(Assertion)测试期望的结果
- 可以方便地组织和运行测试
- 可以方便地查看测试结果
- 常用 IDE 都继承了 JUnit
- 可以方便地集成到 Maven
JUnit的设计
- TestCase:一个 TestCase 表示一个测试
- TestSuite:一个 TestSuite 包含一组 TestCase,表示一组测试
- TestFixture:一个 TestFixture 表示一个测试环境
- TestResult:用于手机测试结果
- TestRunner:用于运行测试
- TestListener:用于监听测试过程,收集测试数据
- Assert:用于断言测试结果是否正确
JUnit 在 Spring 中的不足
1)导致多次Spring容器初始化问题
根据JUnit测试方法的调用流程,每执行一个测试方法都会创建一个测试用例的实例并调用setUp()方法。由于一般情况下,我们在setUp()方法中初始化Spring容器,这意味着如果测试用例有多少个测试方法,Spring容器就会被重复初始化多次。虽然初始化Spring容器的速度并不会太慢,但由于可能会在Spring容器初始化时执行加载Hibernate映射文件等耗时的操作,如果每执行一个测试方法都必须重复初始化Spring容器,则对测试性能的影响是不容忽视的;
使用Spring测试套件,Spring容器只会初始化一次
2)需要使用硬编码方式手工获取Bean
在测试用例类中我们需要通过ctx.getBean()方法从Spirng容器中获取需要测试的目标Bean,并且还要进行强制类型转换的造型操作。这种乏味的操作迷漫在测试用例的代码中,让人觉得烦琐不堪;
使用Spring测试套件,测试用例类中的属性会被自动填充Spring容器的对应Bean,无须在手工设置Bean!
3)数据库现场容易遭受破坏
测试方法对数据库的更改操作会持久化到数据库中。虽然是针对开发数据库进行操作,但如果数据操作的影响是持久的,可能会影响到后面的测试行为。举个例子,用户在测试方法中插入一条ID为1的User记录,第一次运行不会有问题,第二次运行时,就会因为主键冲突而导致测试用例失败。所以应该既能够完成功能逻辑检查,又能够在测试完成后恢复现场,不会留下“后遗症”;
使用Spring测试套件,Spring会在你验证后,自动回滚对数据库的操作,保证数据库的现场不被破坏,因此重复测试不会发生问题!
4)不方便对数据操作正确性进行检查
假如我们向登录日志表插入了一条成功登录日志,可是我们却没有对t_login_log表中是否确实添加了一条记录进行检查。一般情况下,我们可能是打开数据库,肉眼观察是否插入了相应的记录,但这严重违背了自动测试的原则。试想在测试包括成千上万个数据操作行为的程序时,如何用肉眼进行检查?
只要你继承Spring的测试套件的用例类,你就可以通过jdbcTemplate(或Dao等)在同一事务中访问数据库,查询数据的变化,验证操作的正确性!
Annotations
@BeforeEach & @AfterEach
@BeforeEach: Denotes that the annotated method should be executed beforeeach
@Test,@RepeatedTest,@ParameterizedTest, or@TestFactorymethod in the current class; analogous to JUnit 4’s@Before. Such methods are inherited unless they are overridden.
@AfterEach: Denotes that the annotated method should be executed aftereach
@Test,@RepeatedTest,@ParameterizedTest, or@TestFactorymethod in the current class; analogous to JUnit 4’s@After. Such methods are inherited unless they are overridden.
- 在 @BeforeEach 方法中初始化测试资源
- 在 @AfterEach 方法中释放测试资源
@BeforeAll & @AfterAll
@BeforeAll: Denotes that the annotated method should be executed beforeall
@Test,@RepeatedTest,@ParameterizedTest, and@TestFactorymethods in the current class; analogous to JUnit 4’s@BeforeClass. Such methods are inherited (unless they are hidden or overridden) and must bestatic(unless the “per-class” test instance lifecycle is used).
@AfterAll: Denotes that the annotated method should be executed afterall
@Test,@RepeatedTest,@ParameterizedTest, and@TestFactorymethods in the current class; analogous to JUnit 4’s@AfterClass. Such methods are inherited (unless they are hidden or overridden) and must bestatic(unless the “per-class” test instance lifecycle is used).
- @BeforeAll 静态方法初始化的对象只能存放在静态字段中
- 静态字段的状态会影响到所有的 @Test
超时 —— @Timeout
@Test
@Timeout(value = 1, unit = TimeUnit.MICROSECONDS)
void test07() {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
}
禁用 —— @Disabled
Used to disable a test class or test method; analogous to JUnit 4’s
@Ignore. Such annotations are not inherited.
重复测试 —— @RepeatedTest
Denotes that a method is a test template for a repeated test. Such methods are inherited unless they are overridden.
重复测试:使用参数 value 来指定重复测试的次数
参数化测试 —— @ParameterizedTest
Denotes that a method is a parameterized test. Such methods are inherited unless they are overridden.
参数源详解
value source:最简单的参数源,通过注解可以直接指定携带的运行参数。
- String values: @ValueSource(strings = {“foo”, “bar”, “baz”})
- Double values: @ValueSource(doubles = {1.5D, 2.2D, 3.0D})
- Long values: @ValueSource(longs = {2L, 4L, 8L})
- Integer values: @ValueSource(ints = {2, 4, 8})
@ParameterizedTest
@ValueSource(ints = {
1, 2, 3, 4, 5})
void test03(int n) {
Assertions.assertTrue(n > 0);
}
Enum Source:枚举参数源,允许我们通过将参数值由给定Enum枚举类型传入。并可以通过制定约束条件或正则匹配来筛选传入参数。
@ParameterizedTest
@EnumSource(value = DayInWeekEnum.class, names = {
"MONDAY"})
void test04(DayInWeekEnum day) {
Assertions.assertTrue(EnumSet.of(DayInWeekEnum.MONDAY, DayInWeekEnum.FRIDAY).contains(day));
}
Method Source:通过其他的 Java 方法函数来作为参数源。引用的方法返回值必须是Stream、Iterator 或者 Iterable。
static Stream<String> stringStreamGenerator() {
return Stream.of("a", "b", "c", "d");
}
@ParameterizedTest
@MethodSource(value = {
"stringStreamGenerator"})
void test05(String arg) {
System.out.println(arg);
Assertions.assertNotNull(arg);
}
Argument Source:通过参数类来作为参数源。这里引用的类必须实现ArgumentsProvider接口。
CSV Source:通过指定 csv(comma-separated-values,逗号分隔值,有时也称为字符分隔值,因为分隔字符也可以不是逗号)格式的注解作为参数源。
static Map<Integer, String> map;
@BeforeAll
static void mapAdding() {
map = new HashMap<>();
map.put(1, "Tom");
map.put(2, "Lucy");
map.put(3, "Jack");
}
@ParameterizedTest
@CsvSource({
"1,Tom", "2,Lucy", "3,Jack"})
void test06(Integer id, String name) {
System.out.println(id + " -> " + name);
Assertions.assertTrue(map.containsKey(id));
Assertions.assertEquals(name, map.get(id));
}
CSV File Source:除了使用csv参数源,这里也支持使用csv文件作为参数源。
假设users.csv 文件包含如下csv格式的数据:1,Selma ‘\n’ 2,Lisa’\n’ 3,Tim(’\n’ :换行)
@ParameterizedTest
@CsvFileSource(resources = "/users.csv")
void testUsersFromCsv(long id, String name) {
assertTrue(idToUsername.containsKey(id));
assertTrue(idToUsername.get(id).equals(name));
}
参数转换
JUnit allows us to convert arguments to the target format we need in our tests.
隐式转换
JUnit 提供了很对内建的格式转化支持,特别是 string 和常用的数据类型。
以下是支持和 string 型进行转换的类型:
- Boolean
- Byte
- Character
- Short
- Integer
- Long
- Float
- Double
- Enum subclass
- Instant
- LocalDate
- LocalDateTime
- LocalTime
- OffsetTime
- OffsetDateTime
- Year
- YearMonth
- ZonedDateTime
显式转换
Junit5 中可以使用 @ConvertWith(MyConverter.class) 注解来实现 SimpleArgumentConverter。
标记与过滤 —— @Tag
可以在测试类上标记 @Tag("…") ,之后在 pom.xml 文件或 build.gradle 文件中可以对其进行过滤。
Assertions
Assertions’ basic methods
- 断言相等:assertEquals(100, x)
- 断言数组相等:assertArrayEquals({1, 2, 3}, x)
- 浮点数断言相等:assertEquals(3.1415, x, 0.0001)
- 断言为 null:assertNull(x)
- 断言为 true/false:assertTrue(x > 0) 、assertFalse(x < 0)
- 其他:assertNotEquals / assertNotNull
断言组 —— assertAll
此方法可实现1个用例中包含多个断言,遇到断言失败仍然会继续下一个断言。
assertAll 的 heading 将会在测试失败时,会在失败信息的开头,作为一系列失败点的开头信息,给予提示。
@ParameterizedTest
@CsvSource({
"1,Tom", "2,Lucy", "3,Jack"})
void test06(Integer id, String name) {
System.out.println(id + " -> " + name);
// Assertions.assertTrue(map.containsKey(id));
// Assertions.assertEquals(name, map.get(id));
Assertions.assertAll("This is the heading of method: assertAll",
() -> Assertions.assertTrue(map.containsKey(id)),
() -> Assertions.assertEquals(name, map.get(id)),
() -> Assertions.assertEquals("1", map.get(id))
);
}
超时 —— assertTimeout
@Test
void test08() {
Assertions.assertTimeout(Duration.ofMillis(1), () -> {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
list.add(i);
}
});
}
异常测试 —— assertThrows
使用 java8 中的 lambda 表达式配合 assertThrows 进行测试。
@Test
void test02() {
Exception exception = Assertions.assertThrows(ArithmeticException.class, () -> calculator.divide(2, 0));
Assertions.assertEquals("/ by zero", exception.getMessage());
}
JUnit的使用
Junit测试的执行顺序
- 实例化测试类
- 执行 @BeforeAll 方法:初始化非常耗时的资源,例如:创建数据库(针对所有测试,只执行一次,且必须为static void)
- 执行 @BeforeEach 方法:初始化测试对象,例如:I/O流
- 执行 @Test 方法:测试方法,在这里可以测试期望异常和超时时间
- 执行 @AfterEach 方法:释放资源,销毁 @Before 创建的测试对象
- 执行 @AfterAll 方法:清理 @BeforeAll 创建的资源,例如:删除数据库(针对所有测试,只执行一次,且必须为static void)
JUnit4 与 JUnit5 的不同
- 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。
- 把@Before 和@After 替换成@BeforeEach 和@AfterEach。
- 把@BeforeClass 和@AfterClass 替换成@BeforeAll 和@AfterAll。
- 把@Ignore 替换成@Disabled。
- 把@Category 替换成@Tag。
- 把@RunWith、@Rule 和@ClassRule 替换成@ExtendWith。
配合 Spring Boot 使用 JUnit5
- 在测试类上加 @SpringBootTest 注解
- 可以使用 @Autowired 注解自动注入要测试的服务等
Mock 模拟
@MockBean & @SpyBean
- @MockBean
- @SpyBean
@SpringBootTest
class JunitDemoApplicationTests {
// static Calculator calculator;
//
// @BeforeAll
// static void createCalculator() {
// calculator = new Calculator();
// }
@MockBean
Calculator1 calculator1;
@SpyBean
Calculator2 calculator2;
@Test
void test00() {
when(calculator1.add(1, 2)).thenReturn(3);
Assertions.assertEquals(3, calculator1.add(1, 2));
}
@Test
void test01() {
Assertions.assertEquals(3, calculator2.add(1, 2));
}
}
MockMvc —— 模拟 HTTP 请求
MockMvc是由spring-test包提供,实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。
接口MockMvcBuilder,提供一个唯一的build方法,用来构造MockMvc。主要有两个实现:StandaloneMockMvcBuilder和DefaultMockMvcBuilder,分别对应两种测试方式,即独立安装和集成Web环境测试(并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)。MockMvcBuilders提供了对应的创建方法standaloneSetup方法和webAppContextSetup方法,在使用时直接调用即可。
- mockMvc.perform执行一个请求。
- MockMvcRequestBuilders.get(“XXX”)构造一个请求。
- ResultActions.param添加请求传值
- ResultActions.accept(MediaType.TEXT_HTML_VALUE))设置返回类型
- ResultActions.andExpect添加执行完成后的断言。
- ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情,比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。
- ResultActions.andReturn表示执行完成后返回相应的结果。
常用的测试
- 测试普通控制器
mockMvc.perform(get("/user/{id}", 1)) //执行请求
.andExpect(model().attributeExists("user")) //验证存储模型数据
.andExpect(view().name("user/view")) //验证viewName
.andExpect(forwardedUrl("/WEB-INF/jsp/user/view.jsp"))//验证视图渲染时forward到的jsp
.andExpect(status().isOk())//验证状态码
.andDo(print()); //输出MvcResult到控制台
1234567
- 得到MvcResult自定义验证
MvcResult result = mockMvc.perform(get("/user/{id}", 1))//执行请求
.andReturn(); //返回MvcResult
Assert.assertNotNull(result.getModelAndView().getModel().get("user")); //自定义断言
1234
- 验证请求参数绑定到模型数据及Flash属性
mockMvc.perform(post("/user").param("name", "zhang")) //执行传递参数的POST请求(也可以post("/user?name=zhang"))
.andExpect(handler().handlerType(UserController.class)) //验证执行的控制器类型
.andExpect(handler().methodName("create")) //验证执行的控制器方法名
.andExpect(model().hasNoErrors()) //验证页面没有错误
.andExpect(flash().attributeExists("success")) //验证存在flash属性
.andExpect(view().name("redirect:/user")); //验证视图
123456
- 文件上传
byte[] bytes = new byte[] {
1, 2};
mockMvc.perform(fileUpload("/user/{id}/icon", 1L).file("icon", bytes)) //执行文件上传
.andExpect(model().attribute("icon", bytes)) //验证属性相等性
.andExpect(view().name("success")); //验证视图
1234
- JSON请求/响应验证
String requestBody = "{\"id\":1, \"name\":\"zhang\"}";
mockMvc.perform(post("/user")
.contentType(MediaType.APPLICATION_JSON).content(requestBody)
.accept(MediaType.APPLICATION_JSON)) //执行请求
.andExpect(content().contentType(MediaType.APPLICATION_JSON)) //验证响应contentType
.andExpect(jsonPath("$.id").value(1)); //使用Json path验证JSON 请参考http://goessner.net/articles/JsonPath/
String errorBody = "{id:1, name:zhang}";
MvcResult result = mockMvc.perform(post("/user")
.contentType(MediaType.APPLICATION_JSON).content(errorBody)
.accept(MediaType.APPLICATION_JSON)) //执行请求
.andExpect(status().isBadRequest()) //400错误请求
.andReturn();
Assert.assertTrue(HttpMessageNotReadableException.class.isAssignableFrom(result.getResolvedException().getClass()));//错误的请求内容体
12345678910111213141516
- 异步测试
//Callable
MvcResult result = mockMvc.perform(get("/user/async1?id=1&name=zhang")) //执行请求
.andExpect(request().asyncStarted())
.andExpect(request().asyncResult(CoreMatchers.instanceOf(User.class))) //默认会等10秒超时
.andReturn();
mockMvc.perform(asyncDispatch(result))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.id").value(1));
12345678910
- 全局配置
mockMvc = webAppContextSetup(wac)
.defaultRequest(get("/user/1").requestAttr("default", true)) //默认请求 如果其是Mergeable类型的,会自动合并的哦mockMvc.perform中的RequestBuilder
.alwaysDo(print()) //默认每次执行请求后都做的动作
.alwaysExpect(request().attribute("default", true)) //默认每次执行后进行验证的断言
.build();
mockMvc.perform(get("/user/1"))
.andExpect(model().attributeExists("user"));
简单的例子
创建 VO 对象:
@Data
public class TestVO {
private Integer id;
private String name;
}
创建控制器:
@RestController
public class MyController {
@GetMapping(value = {
"/test"})
public Object test() {
TestVO testVO = new TestVO();
testVO.setId(1);
testVO.setName("Test1");
return testVO;
}
}
创建测试类:
@SpringBootTest
class MyControllerTest {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext webApplicationContext;
@Test
void test() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
// 发送 get 请求
// public static org.springframework.test.web.servlet.request
// .MockHttpServletRequestBuilder get(@NotNull String urlTemplate, Object... uriVars)
RequestBuilder request = get("http://localhost:8080/test");
try {
String response = mockMvc.perform(request).andReturn().getResponse().getContentAsString();
System.out.println(response);
// jackson 解析
ObjectMapper mapper = new ObjectMapper();
TestVO testVO = mapper.readValue(response, TestVO.class);
System.out.println(testVO);
Assertions.assertEquals(1, testVO.getId());
} catch (Exception e) {
e.printStackTrace();
}
}
}
事务控制
加 @Rollback 注解即可实现在测试方法完成后的回滚
注:笔者上网查了许久,发现都是通过 @Transactional 与 @Rollback 配合使用的,但笔者在 2020/8/21 使用 Spring Boot 进行单元测试的时候,发现其已无 @Transactional 注解,而且只使用 @Rollback 注解就可以完成回滚操作。
为了测试单独使用 @Rollback 是否可以达成事务回滚的效果,笔者写了以下的测试类:
@SpringBootTest
class CalculatorServiceTest {
// 两个标有 @Test 的测试方法,会依照方法名开头字母(的ASCII码值)升序进行
private int a = 0; // a 的默认值为 0
@Test
@Rollback // 回滚注解
void test0() {
a = 1; // 将 a 的值修改为 1
System.out.println("a = " + a); // 在标记了 @Rollback 回滚注解的方法结束前,输出 a 的值,
}
@Test
void test1() {
System.out.println("a = " + a);
}
}
输出内容如下:
...(日志信息)
a = 1 // test0() 的输出结果
a = 0 // test1() 的输出结果
BUILD SUCCESSFUL in 2s
4 actionable tasks: 2 executed, 2 up-to-date
19:22:25: Task execution finished ':test --tests "xxx.yyyy.service.CalculatorServiceTest"'.
发现 a 在 test0() 方法中确实被修改为了 1,而且在 test0() 方法结束后确实也被修改回去了。姑且推测其是 “回滚成功” 了。
再看向 @Rollback 的源码(省略了开头的 Copyright):
package org.springframework.test.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** * {@code @Rollback} is a test annotation that is used to indicate whether * a <em>test-managed transaction</em> should be <em>rolled back</em> after * the test method has completed. * * <p>Consult the class-level Javadoc for * {@link org.springframework.test.context.transaction.TransactionalTestExecutionListener} * for an explanation of <em>test-managed transactions</em>. * * <p>When declared as a class-level annotation, {@code @Rollback} defines * the default rollback semantics for all test methods within the test class * hierarchy. When declared as a method-level annotation, {@code @Rollback} * defines rollback semantics for the specific test method, potentially * overriding class-level default commit or rollback semantics. * * <p>As of Spring Framework 4.2, {@code @Commit} can be used as direct * replacement for {@code @Rollback(false)}. * * <p><strong>Warning</strong>: Declaring {@code @Commit} and {@code @Rollback} * on the same test method or on the same test class is unsupported and may * lead to unpredictable results. * * <p>This annotation may be used as a <em>meta-annotation</em> to create * custom <em>composed annotations</em>. Consult the source code for * {@link Commit @Commit} for a concrete example. * * @author Sam Brannen * @since 2.5 * @see Commit * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener */
@Target({
ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Rollback {
/** * Whether the <em>test-managed transaction</em> should be rolled back * after the test method has completed. * <p>If {@code true}, the transaction will be rolled back; otherwise, * the transaction will be committed. * <p>Defaults to {@code true}. */
boolean value() default true;
}
其中有两段话较为关键:
@Rollback is a test annotation that is used to indicate whether a test-managed transaction should be rolled back after the test method has completed.
Whether the test-managed transaction should be rolled back after the test method has completed.
If true, the transaction will be rolled back; otherwise, the transaction will be committed.
Defaults to true.
第一段话是注释在整个 @Rollback 注解类上的,大意为:@Rollback 用于在测试完成之后,回滚其所标记的类/方法。
第二段话是注释的 value() 上的,该属性表示 “是否开启回滚” ,默认值为 true(即回滚,即不提交)。
结论:只使用 @Rollback 是可以完成测试回滚功能的。
总结
- 一个 TestCase 包含一组相关的测试方法
- 使用 Assert 断言测试结果(注意浮点数 assertEquals 要指定 delta)
- 每个测试方法必须完全独立
- 测试代码必须非常简单
- 不能为测试代码再编写测试
- 测试需要覆盖各种输入条件,特别是边界条件
边栏推荐
- Represent each record in the dataframe as a dictionary
- [DB written interview 220] how to back up control files in oracle? What are the ways to back up control files?
- Summary of mobile application development
- Introduction to testing - Software Test Model
- 移动应用开发总结
- Markdown rule for writing articles
- 4.5 dataset usage document
- (thinking) C. differential sorting
- Using elastic stack to analyze Olympic data (II)
- 日記(C語言總結)
猜你喜欢
随机推荐
Doc common syntax, updating
Represent each record in the dataframe as a dictionary
Mono fourni avec l'unit é 5 peut également supporter C # 6
4.8 inquirer-autocomplete-prompt
Figure neural network and cognitive reasoning - Tang Jie - Tsinghua University
MySql 过滤查询(以字母开头,以数字开头,非数字开头,非字母开头)
Unity 5 自帶的Mono也可以支持C# 6
4.9 commander. js
showCTF Web入门题系列
Diary (C language summary)
怎么搭建深度学习框架?
Fd: file descriptor
Unity development related blog collection
Leedcode 1 - sum of two numbers
Ads Filter Design Wizard tool 2
antd table长表格如何出现滚动条
2.19 simulation summary
Nodejs post request JSON type and form type
For hand drawn graphics, covering multiple topics, CVPR 2022 sketchdl workshop begins to solicit contributions!
Linux Installation of Damon database /dm8 (with client tool installation full version)









