├── 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 | 
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 |
--------------------------------------------------------------------------------