├── test process.jpg └── README.md /test process.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyneck/unit-test-specification/HEAD/test process.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 单元测试规范和mock进阶使用实例 2 | --------------------------- 3 | 4 | 开发、测试流程示例: 5 | 6 | ![img](https://github.com/cyneck/unit-test-specification/blob/master/test%20process.jpg) 7 | 8 | ------------------------ 9 | 10 | ## 一、单元测试目的 11 | 12 | ​ 单元测试是编写测试代码,用来检测特定的、明确的、细颗粒的功能。单元测试并不一定保证程序功能是正确的,更不保证整体业务是准备的。单元测试不仅仅用来保证当前代码的正确性,更重要的是用来保证代码**修复**、**改进**或**重构**之后的正确性。 13 | 14 | 1. 接口功能测试:用来保证接口功能的正确性。 15 | 2. 局部数据结构测试(不常用):用来保证接口中的数据结构是正确的 16 | 1. 比如变量有无初始值 17 | 2. 变量是否溢出 18 | 3. 边界条件测试 19 | 1. **变量没有赋值**(即为NULL) 20 | 2. 变量是数值(或字符) 21 | 1. **主要边界**:最小值,最大值,无穷大(对于DOUBLE等) 22 | 2. **溢出边界**(期望异常或拒绝服务):最小值-1,最大值+1 23 | 3. **临近边界**:最小值+1,最大值-1 24 | 3. 变量是字符串 25 | 1. 引用“字符变量”的边界 26 | 2. **空字符串** 27 | 3. 对字符串长度应用“数值变量”的边界 28 | 4. 变量是集合 29 | 1. **空集合** 30 | 2. **对集合的大小应用“数值变量”的边界** 31 | 3. **调整次序**:升序、降序 32 | 5. 变量有规律 33 | 1. 比如对于Math.sqrt,给出n^2-1,和n^2+1的边界 34 | 4. 所有独立执行通路测试:保证每一条代码,每个分支都经过测试 35 | 1. 代码覆盖率 36 | 1. **语句覆盖**:保证每一个语句都执行到了 37 | 2. 判定覆盖(分支覆盖):保证每一个分支都执行到 38 | 3. 条件覆盖:保证每一个条件都覆盖到true和false(即if、while中的条件语句) 39 | 4. 路径覆盖:保证每一个路径都覆盖到 40 | 2. 相关软件 41 | 1. Cobertura:语句覆盖 42 | 2. Emma: Eclipse插件Eclemma 43 | 5. 各条错误处理通路测试:保证每一个异常都经过测试 44 | 45 | ------------- 46 | 47 | ## **二、基本原则** 48 | 1. 【强制】好的单元测试必须遵守 AIR 原则。 49 | 说明:单元测试在线上运行时,感觉像空气(AIR)一样并不存在,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。 50 | - A:Automatic(自动化) 51 | - I:Independent(独立性) 52 | - R:Repeatable(可重复) 53 | 2. 【强制】单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的, 54 | 执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。 55 | 单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。 56 | 3. 【强制】保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之 57 | 间决不能互相调用,也不能依赖执行的先后次序。 58 | 反例:method2 需要依赖 method1 的执行,将执行结果作为 method2 的输入。 59 | 4. 【强制】单元测试是可以重复执行的,不能受到外界环境的影响。 60 | 说明:单元测试通常会被放到持续集成中,每次有代码 check in 时单元测试都会被执行。如果单测对外部 61 | 环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。 62 | 正例:为了不受外界环境影响,要求设计代码时就把 SUT 的依赖改成注入,在测试时用 spring 这样的 DI 63 | 框架注入一个本地(内存)实现或者 Mock 实现。 64 | 5. 【强制】对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类 65 | 级别,一般是方法级别。 66 | 说明:只有测试粒度小才能在出错时尽快定位到出错位置。单测不负责检查跨类或者跨系统的交互逻辑, 67 | 那是集成测试的领域。 68 | 6. 【强制】核心业务、核心应用、核心模块的增量代码确保单元测试通过。 69 | 说明:新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正。 70 | 7. 【强制】单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下。 71 | 说明:源码编译时会跳过此目录,而单元测试框架默认是扫描此目录。 72 | 8. 【推荐】单元测试的基本目标:语句覆盖率达到 70%;核心模块的语句覆盖率和分支覆盖率 73 | 都要达到 100% 74 | 说明:在工程规约的应用分层中提到的 DAO 层,Manager 层,可重用度高的 Service,都应该进行单元 75 | 测试。 76 | 9. 【推荐】编写单元测试代码遵守 BCDE 原则,以保证被测试模块的交付质量。 77 | - B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。 78 | - C:Correct,正确的输入,并得到预期的结果。 79 | - D:Design,与设计文档相结合,来编写单元测试。 80 | - E:Error,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果。 81 | 10. 【推荐】对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的, 82 | 或者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据。 83 | 反例:删除某一行数据的单元测试,在数据库中,先直接手动增加一行作为删除目标,但是这一行新增数 84 | 据并不符合业务插入规则,导致测试结果异常。 85 | 11. 【推荐】和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。或者 86 | 对单元测试产生的数据有明确的前后缀标识。 87 | 正例:在企业智能事业部的内部单元测试中,使用 ENTERPRISE_INTELLIGENCE _UNIT_TEST_的前缀来 88 | 标识单元测试相关代码。 89 | 12. 【推荐】对于不可测的代码在适当的时机做必要的重构,使代码变得可测,避免为了达到测 90 | 试要求而书写不规范测试代码。 91 | 13. 【推荐】在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,单元测试最好 92 | 覆盖所有测试用例。 93 | 14. 【推荐】单元测试作为一种质量保障手段,在项目提测前完成单元测试,不建议项目发布后 94 | 补充单元测试用例。 95 | 15. 【参考】为了更方便地进行单元测试,业务代码应避免以下情况: 96 | 构造方法中做的事情过多。 97 | - 存在过多的全局变量和静态方法。 98 | - 存在过多的外部依赖。 99 | - 存在过多的条件语句。 100 | 说明:多层条件语句建议使用卫语句、策略模式、状态模式等方式重构。 101 | 16. 【参考】不要对单元测试存在如下误解: 102 | - 那是测试同学干的事情。本文是开发手册,凡是本文内容都是与开发同学强相关的。 103 | - 单元测试代码是多余的。系统的整体功能与各单元部件的测试正常与否是强相关的。 104 | - 单元测试代码不需要维护。一年半载后,那么单元测试几乎处于废弃状态。 105 | - 单元测试与线上故障没有辩证关系。好的单元测试能够最大限度地规避线上故障。 106 | 107 | ------------------------- 108 | 109 | ## 三、Spring boot单元测试框架 110 | 111 | ​ 统一使用spring boot对应版本的单元测试框架,避免junit3、junit4、junit5各种测试框架混合使用,当前spring boot主要的单元测试框架是junit4,通过`mvn test`命令运行所有单元测试模块。同时,spring boot官方集成了mockito框架用于mock测试。(spring boot测试框架参考[spring boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-testing),mockito测试框架参考[mockito官方文档](https://site.mockito.org)) 112 | 113 | ​ 首先引入依赖如下依赖。 114 | 115 | ``` 116 | 117 | org.springframework.boot 118 | spring-boot-starter-test 119 | test 120 | 121 | ``` 122 | 123 | ### 1.测试注解使用 124 | 125 | 目前支持的主要注解有: 126 | 127 | - `@BeforeClass` 全局只会执行一次,而且是第一个运行 128 | - `@Before` 在测试方法运行之前运行 129 | - `@Test` 测试方法 130 | - `@After` 在测试方法运行之后允许 131 | - `@AfterClass` 全局只会执行一次,而且是最后一个运行 132 | - `@Ignore` 忽略此方法 133 | 134 | ​ 执行次序是`@BeforeClass` -> `@Before` -> `@Test` -> `@After` -> `@Before` -> `@Test` -> `@After` -> `@AfterClass`。`@Ignore`会被忽略。 135 | 136 | ### 2.Assert类(断言的使用) 137 | 138 | ​ Junit4都提供了一个Assert类。Assert类中定义了很多静态方法来进行断言。 139 | 140 | - assertTrue(String message, boolean condition) 要求condition == true 141 | - assertFalse(String message, boolean condition) 要求condition == false 142 | - fail(String message) 必然失败,同样要求代码不可达 143 | - assertEquals(String message, XXX expected,XXX actual) 要求expected.equals(actual) 144 | - assertArrayEquals(String message, XXX[] expecteds,XXX [] actuals) 要求expected.equalsArray(actual) 145 | - assertNotNull(String message, Object object) 要求object!=null 146 | - assertNull(String message, Object object) 要求object==null 147 | - assertSame(String message, Object expected, Object actual) 要求expected == actual 148 | - assertNotSame(String message, Object unexpected,Object actual) 要求expected != actual 149 | - assertThat(String reason, T actual, Matcher matcher) 要求matcher.matches(actual) == true 150 | 151 | ### 3.Mock测试 152 | 153 | ​ Mock和Stub是两种测试代码功能的方法。Mock测重于对功能的模拟。Stub测重于对功能的测试重现。强烈建议优先选择Mock方式,因为Mock方式下,模拟代码与测试代码放在一起,易读性好,而且扩展性、灵活性都比Stub好。Spring boot提供了MockMvc有关的mock测试 154 | 155 | #### (1)、MockMvc测试注解 156 | 157 | (详细使用,参考:[官方指南](https://spring.io/guides/gs/testing-web/)、[文档指导](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/servlet/MockMvc.html))需要在测试类中增加如下注解。 158 | 159 | ```java 160 | @RunWith(SpringRunner.class) 161 | @SpringBootTest 162 | @AutoConfigureMockMvc 163 | public class MockXXXTest { 164 | 165 | } 166 | ``` 167 | 168 | - **@RunWith(SpringRunner.class)** 169 | 170 | 就是指用SpringRunner来运行,其中SpringJUnit4ClassRunner 和 SpringRunner 区别是什么?在官方文档中有如下这句话:“SpringRunner is an alias for the SpringJUnit4ClassRunner”。 171 | 172 | - **@SpringBootTest** 173 | 174 | 该注解是SpringBoot的一个用于测试的注解,通过SpringApplication在测试中创建ApplicationContext。 175 | 176 | - **@AutoConfigureMockMvc** 177 | 178 | 该注解是用于自动配置MockMvc。 179 | 180 | 附加:@Transactional:增加该注解实现数据回滚,可以避免数据污染; 181 | 182 | ​ @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT),可以防止spring boot单元测试中的端口冲突。 183 | 184 | ​ 依赖于application context的测试,还需要模拟WebApplicationContext,HttpServletRequest,HttpServletResponse,session等。 185 | 186 | ```java 187 | @RunWith(SpringRunner.class) 188 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 189 | @AutoConfigureMockMvc 190 | public class MockXXXTest { 191 | @Resource 192 | private WebApplicationContext webApplicationContext; 193 | 194 | private MockMvc mockMvc; 195 | private MockHttpServletRequest mockHttpServletRequest; 196 | private MockHttpServletResponse mockHttpServletResponse; 197 | protected MockHttpSession session; 198 | 199 | @Before 200 | public void setup() { 201 | mockHttpServletRequest = new MockHttpServletRequest(webApplicationContext.getServletContext()); 202 | mockHttpServletResponse = new MockHttpServletResponse(); 203 | MockHttpSession mockHttpSession = new MockHttpSession(webApplicationContext.getServletContext()); 204 | mockHttpServletRequest.setSession(mockHttpSession); 205 | mockMvc = MockMvcBuilders 206 | .webAppContextSetup(webApplicationContext) 207 | .build(); 208 | } 209 | } 210 | ``` 211 | 212 | #### (2)、使用MockMvc发送请求 213 | 214 | ​ mockmvc测试,主要测试是Controller层单元测试,可以模拟数据从浏览器请求到后端整个mvc过程逻辑。 215 | 216 | ```java 217 | //配置MockMvc 218 | @Autowired 219 | protected MockMvc mockMvc; 220 | 221 | @Test 222 | public void TestXXX() throws Exception { 223 | MvcResult result = mockMvc.perform( 224 | MockMvcRequestBuilders.get("/xxxController/xxx_query") 225 | .contentType(MediaType.APPLICATION_JSON_UTF8) 226 | .param("xxx","xxx") 227 | ).andExpect(MockMvcResultMatchers.status().isOk()) 228 | .andDo(MockMvcResultHandlers.print()) 229 | .andReturn(); 230 | 231 | } 232 | } 233 | ``` 234 | 235 | #### (3)、MockBean(@SpyBean)模拟相应对象 236 | 237 | ​ 这里的主要作用是:使用mock对象代替原来spring的bean,然后模拟底层数据的返回,而不是调用原本真正的实现。 238 | 239 | ​ **注**:与@MockBean 对应的还有@SpyBean。 @SpyBean与 @Spy 的关系类似于 @MockBean 与 @Mock 的关系。和 @MockBean 不同的是,它不会生成一个 Bean 的替代品装配到类中,而是会监听一个真正的 Bean 中某些特定的方法,并在调用这些方法时给出指定的反馈。基于@SypBean的特性,可以构建依赖其他类、服务等的真实模拟,比如远程调用服务中的依赖。 240 | 241 | ​ 如下示例,SpringBoot 中, @MockBean 会将mock的bean替换掉 SpringBoot 管理的原生bean,从而达到mock的效果。: 242 | 243 | ```java 244 | public class MockXXXTest { 245 | 246 | @MockBean 247 | private XXXDao xxxtDao; 248 | 249 | } 250 | ``` 251 | 252 | 或者采用XXXDao mockBean = Mockito.mock(XXXDao.class)的代码方式模拟创建 253 | 254 | ​ 下面是一个完整示例。 255 | 256 | ```java 257 | @RunWith(SpringRunner.class) 258 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 259 | public class TenantServiceTest { 260 | @Test 261 | public void mockitoExampleGetTest() { 262 | //构造数据 263 | //设置期望数据 264 | TenantDetailRspDTO expectedResult = new TenantDetailRspDTO(); 265 | expectedResult.setTenantDTO(new TenantDTO().setTenantId("0000")); 266 | expectedResult.setTenantParameterDTO(new TenantParameterDTO().setTenantId("0000")); 267 | expectedResult.setTenantConfigDTO(new TenantConfigDTO().setTenantId("0000")); 268 | 269 | //创建一个mock对象,或者通过属性注解的方式SpringBoot封装框架使用@MockBean,当对ApplicationContext有依赖时可以使用;原生mockito框架使用@Mock 270 | TenantService mockBean = Mockito.mock(TenantService.class); 271 | 272 | //创建一个测试桩stub,定义在service层该方法的返回值定义。 273 | Mockito.when(mockBean.getTenantDetail("0000")).thenReturn(expectedResult); 274 | 275 | //执行方法,使用模拟对象 276 | TenantDetailRspDTO actualResult = mockBean.getTenantDetail("0000"); 277 | 278 | //验证调用方法是否执行过 279 | Mockito.verify(mockBean).getTenantDetail("0000"); 280 | 281 | //断言 282 | Assert.isTrue(actualResult.getTenantDTO().getTenantId() == "0000", ApiErrorCode.FAILED); 283 | 284 | } 285 | } 286 | ``` 287 | 288 | #### (4)、单元测试覆盖率 289 | 290 | ##### 1.代码覆盖率的意义。 291 | 292 | 代码覆盖率统计是用来发现没有被测试覆盖的代码;代码覆盖率统计不能完全用来衡量代码质量。 293 | 294 | - 分析未覆盖部分的代码,从而反推在前期测试设计是否充分,没有覆盖到的代码是否是测试设计的盲点,为什么没有考虑到?需求/设计不够清晰,测试设计的理解有误,工程方法应用后的造成的策略性放弃等等,之后进行补充[**测试用例**](javascript:;)设计。 295 | - 检测出程序中的废代码,可以逆向反推在代码设计中思维混乱点,提醒设计/开发人员理清代码逻辑关系,提升代码质量。 296 | - 代码覆盖率高不能说明代码质量高,但是反过来看,代码覆盖率低,代码质量不会高到哪里去,可以作为测试自我审视的重要工具之一。 297 | 298 | ##### 2.使用工具 299 | 300 | ​ 开发人员可以通过idea自身的测试代码覆盖率功能进行自测。在代码覆盖率测试中,构造一个高质量的测试数据,可以覆盖80%~90%以上的逻辑,开发人员可以导出测试覆盖率详细数据,根据核心功能的要求,构造合适的数据,进行逻辑功能的覆盖。 301 | 302 | --------------------------------- 303 | 304 | ## 四、Junit测试代码编写命名规范 305 | 306 | 使用测试idea提供的自动生成测试模板为样例。 307 | 308 | **1.测试类的命名定义规范** 309 | 310 | ​ Junit自动生成测试类的命名如下:被测试的业务+Test、被测试的接口+Test、被测试的类+Test 311 | 312 | **2.测试用例的命名定义规范** 313 | 314 | ​ 测试用例的命名规则是:test+例操作名。避免使用test1、test2没有含义的名称。其次需要有必要的函数方法注释。 315 | 316 | **3.测试程序的包名定义规范** 317 | 318 | - 测试程序包的命名规则是:<公司域名>.<子级组织部门缩写>.<项目名>.<模块名>; 319 | - 测试公共类包的命名规则是:<公司域名>.<子级组织部门缩写>.<项目名>.common; 320 | - java包的名称都是由小写字母组成。 321 | - 测试开发包,测试包保持和被测包一致。 322 | 323 | **4.变量的命名规范** 324 | 325 | ​ 保持跟开发规范一致 326 | 327 | **5.常量的命名规范** 328 | 329 | ​ 保持跟开发规范一致 330 | --------------------------------------------------------------------------------