目录导航
一.测试用例编写规范及概念 1、单元测试主要任务2、单元测试的步骤3、测试数据制作4、评价维度5、评价手段6、代码覆盖率 二.实施方案 1、idea安装junit插件2、添加pom依赖:3、命名4、几种常用的注解(导org.junit.jupiter包)5、断言6、参数化测试7、MockMvc使用(模拟controller请求接收)8、几个方法的简单说明:9、增加app服务的验证签名之后的junit修改 三.CI流程中需要增加的项目 1、pom依赖2、profile3、测试代码中profile的使用4、测试数据的规范一.测试用例编写规范及概念
1、单元测试主要任务
2、单元测试的步骤
3、测试数据制作
4、评价维度
5、评价手段
6、代码覆盖率
二.实施方案
1、idea安装junit插件
idea整合junit5之前,先安装配置junit插件:/nicolas12/article/details/81223938
2、添加pom依赖:
<!--junit5依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId><version>5.3.2</version><scope>test</scope></dependency><dependency><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId><version>5.3.2</version><scope>test</scope></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-params</artifactId><version>5.3.2</version><scope>test</scope></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><version>5.3.2</version><scope>test</scope></dependency>
3、命名
工具生成的名字一般是在测试的方法名前面加test及首字母大写,比如addApps -> testAddApps
但是一般测试不同的条件场景情况较多,所以一般建议不同的入参命名规则为:前面的名字不变,后面加入参的条件,举例为testAddAppsWhenChannelIdIsZero和testAddAppsWhenChannelNotExist就是不同的情况用When连接不同的条件。
4、几种常用的注解(导org.junit.jupiter包)
A、@Test 表示方法是一种测试方法B、@Disabled 表示会跳过此测试方法C、@DisplayName 为测试类或者测试方法自定义一个名称,举例
@DisplayName("test Disabled")@Disabled@Testvoid testDisabled(){log.info("test Disabled");}
D、@BeforeEach 表示方法在每个测试方法运行前都会运行
E、@AfterEach 表示方法在每个测试方法运行之后都会运行
F、@BeforeAll 表示方法在所有测试方法之前运行(类级别方法,必须位静态方法)
G、@AfterAll 表示方法在所有测试方法之后运行 (类级别方法,必须位静态方法)
@Autowiredprivate WebApplicationContext wac;private MockMvc mockMvc;@BeforeAllpublic static void BeforeEach() throws Exception {//在所有测试方法运行前运行log.info("Run before all test methods run");}@BeforeEachpublic void before() throws Exception {mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();}
H、@RepeatedTest(重复测试,测试高并发用)
I、@EnabledOnOs(在什么环境执行),如 @EnabledOnOs({ LINUX, MAC }),也可以自定义举例:
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Test@EnabledOnOs(MAC)@interface TestOnMac {}@TestOnMacvoid testOnMac() {// ...}
J、@EnabledOnJre(基于哪个版本的jre执行)如 @EnabledOnJre({ JAVA_9, JAVA_10 })
5、断言
A、assertEquals 断言预期值和实际值相等(参数可以是int double string等等)
B、assertFalse 断言条件为假
C、assertNotNull 断言不为空
D、assertTrue 断言条件为真 举例:
//参数为intassertEquals(Math.round(Double.valueOf(gson.fromJson(result, Map.class).get("code").toString())),0);//参数位字符串assertEquals(result,"");//参数为booleanassertTrue(Math.round(Double.valueOf(gson.fromJson(result, Map.class).get("code").toString()))==0);
E、assumeTrue 假设为true时才会执行,如果为false,那么将会直接停止执行 举例:
assumeTrue(!gson.fromJson(result, Map.class).get("msg").toString().contains("channelId must more than or equal to 1"));assumeTrue(!gson.fromJson(result, Map.class).get("msg").toString().contains("name must hava a value"));assumeTrue(!gson.fromJson(result, Map.class).get("msg").toString().contains("appKey must hava a value"));
6、参数化测试
@ParameterizedTest(替代@Test)
@CsvSource(多个参数的多组测试)
@ValueSource(单个参数的多组测试)
@EnumSource 其实跟@ValueSource差不多,只不过可以复用枚举类。举例:
public enum ActivityLimitEnum {LIMIT(1,"封顶"),UNLIMIT(0,"上不封顶");}@ParameterizedTest@EnumSource(ActivityLimitEnum.class)@DisplayName("封顶和不封顶")void test(ActivityLimitEnum activityLimitEnum) {if (ActivityLimitEnum.LIMIT.equals(activityLimitEnum)) {assertFalse(false);} else if (ActivityLimitEnum.UNLIMIT.equals(activityLimitEnum)) {assertTrue(true);}}
@MethodSource(将一个方法的返回值作为测试方法的入参,引用的方法返回值必须是Stream, Iterator 或者Iterable) 如:
@ParameterizedTest@MethodSource("stringGenerator")public void test(String str){System.out.println(str);}static Stream<String> stringGenerator(){return Stream.of("hello", "world", "let's", "test");}
7、MockMvc使用(模拟controller请求接收)
A、GET请求
/*** Method: findAppListByChannelId(Integer channelId)*/@DisplayName("query apps information by channelId")@ParameterizedTest@ValueSource(strings = {"1", "2", "3"})public void testFindAppListByChannelId(String channelId) throws Exception {String result = mockMvc.perform(get("/v1/appver/apps/list-by-channel") //请求的url,请求的方法是get.param("channelId", channelId) //添加参数.contentType(MediaType.APPLICATION_JSON_UTF8)).andExpect(status().isOk()) //返回的状态是200.andReturn().getResponse().getContentAsString(); //将相应的数据转换为字符串assertTrue(Math.round(Double.valueOf(gson.fromJson(result, Map.class).get("code").toString()))==0);}
注意:get请求的参数全部写String类型,由param方法自动装配成需要的类型。
B、POST请求
/*** Method: addApps(AppsBo appsBo)*/@DisplayName("add apps information success")@ParameterizedTest@CsvSource({"app1, key1,1", "app2, key2,1"})public void testAddAppsSuccess(String name, String appKey, int channdlId) throws Exception {log.info("test testAddAppsSuccess end");Date date = new Date();AppsBo appsBo = new AppsBo();appsBo.setName(name);appsBo.setChannelId(channdlId);appsBo.setAppKey(appKey);String requestBody = gson.toJson(appsBo);Map map = gson.fromJson(requestBody, Map.class);map.put("createTime", date.getTime());String newRequestBody = gson.toJson(map);String result = mockMvc.perform(MockMvcRequestBuilders.post("/v1/appver/apps/add").content(newRequestBody).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()).andReturn().getResponse().getContentAsString();|-----|-----|-----| //assumeTrue的参数为true时才继续往下走,否则就停止到此assumeTrue(!gson.fromJson(result, Map.class).get("msg").toString().contains("The channel not exist"));assertEquals(Math.round(Double.valueOf(gson.fromJson(result, Map.class).get("code").toString())),0);log.info("test testAddAppsSuccess finished");}
8、几个方法的简单说明:
最后注意:因为测试不能影响数据库的数据,所以测试类上都要加事务回滚 如下:
@Slf4j@SpringBootTest@ExtendWith(SpringExtension.class)@Rollback@Transactionalpublic class AppverAppsControllerTest {
测试用例得出的某些问题:
1、添加数据时,要考虑重名判断,用assumeTrue判断返回的校验提示语。
2、修改数据和删除时,要考虑参数Id是否在表中存在,存在和不存的返回结果不一样,所以建议加个自定义校验,用assumeTrue判断返回的校验提示语或者code码。
9、增加app服务的验证签名之后的junit修改
a.注入appId和appSecret,当然配置文件中已经配置此属性。
@Value("${app.id}")private String appId;@Value("${app.secret}")private String appSecret;
b.在before方法中得到需要添加到header中的值
@BeforeEachpublic void before() throws Exception {header=buildSignatureHeaders();mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();}
/*** 创建鉴权头文件信息* @return header信息*/private HttpHeaders buildSignatureHeaders() {Long ts = System.currentTimeMillis();TreeMap<String, String> params = new TreeMap<>();params.put("bys_appId", appId);params.put("bys_timestamp", ts.toString());params.put("bys_secret", appSecret);String sign = SignUtils.sign(params);HttpHeaders header = new HttpHeaders();header.set("bys_appId", appId);header.set("bys_timestamp", Long.toString(ts));header.set("bys_signature", sign);return header;}
c.mockMvc调用接口时添加header签名
String result = mockMvc.perform(post("/v1.0/coupon/store/add").headers(header).content(newRequestBody).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
三.CI流程中需要增加的项目
1、pom依赖
a) 由于ci流程中需要搜集单元测试的代码覆盖率,并合并到SonarQube平台以供查看,应此需要在build.plugins中增加:
<plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.5</version><executions><execution><id>prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><execution><id>report</id><phase>test</phase><goals><goal>report</goal></goals></execution></executions></plugin><plugin><groupId>org.sonarsource.scanner.maven</groupId><artifactId>sonar-maven-plugin</artifactId><version>3.7.0.1746</version></plugin>
b) 为了防止ci流程中执行单元测试时对真实数据库操作影响,引入h2 db,并在2. profile中设置。因此需要在dependencies中增加:
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><version>1.4.197</version><scope>compile</scope></dependency>
2、profile
由于ci流程中执行单元测试时在一个独立的环境,因此需要提供单独的profile配置。在test/resources中增加
application-test.yml
application-test.yml中应包含如下信息
a) 如果有数据库,则应包含
spring:# DataSource StoreConfigdatasource:driver-class-name: org.h2.Driverurl: jdbc:h2:mem:h2test;DB_CLOSE_DELAY=-1;MODE=MySQLusername: sapassword: sah2:console:enabled: true# 如果使用了flywayflyway: enabled: false
b) **如果使用了redis,应将database设置与真实环境不一样的库
c) 其他应用中用到的信息,比如我们使用的鉴权信息,有应用配置的电话号码国家区号等等
例:
# 应用授权信息bys:app:# appIdid: ${APP_ID:10000003}# secretsecret: ${APP_SECRET:99F11D010F20A896FED5D0FEEEFE031C}
3、测试代码中profile的使用
a) 一般情况下我们均使用testprofile,添加类注解@ActiveProfiles(“test”)比如:
@ExtendWith(SpringExtension.class)@ActiveProfiles("test")@SpringBootTestclass UserControllerTest {
b) 如有特殊测试需要测试真实数据中的数据,请使用注解ActiveProfiles引入相应环境的 profile,该profile必须是在java/resources或者test/resources中真实存在的
4、测试数据的规范
a) 上传类的测试文件应放置在test/resources/data/upload中
b) 由于测试过程中需要mock大量数据进行测试,可放置json在test/resources/data/mock中,测试类在before中引入
static List<MembershipCardTypePo> list;//SpyBean不会影响其他的case,但是写法不同如下:Mockito.doReturn(true).when(tracingContext).hasGroup();@SpyBeanIMembershipCardTypeService iMembershipCardTypeService;@MockBeanIMembershipCardTypeService iMembershipCardTypeService;@AutowiredGson gson;@BeforeAllpublic static void BeforeEach() throws Exception {MembershipCardTypePo membershipCardTypePo=new MembershipCardTypePo();list=new ArrayList<>();list.add(membershipCardTypePo);//在所有测试方法运行前运行log.info("Run before all test methods run");}
/*** Method: findMembershipCardTypeListByStore(List<Integer> storeIdList)*/@DisplayName("find membership card type list by store success")@Testpublic void testFindMembershipCardTypeListByStoreSuccess() throws Exception {given(this.iMembershipCardTypeService.findMembershipCardTypeListByStore(any())).willReturn(list);//Mockito.when(iMembershipCardTypeService.findMembershipCardTypeListByStore(any())).thenReturn(list);log.info("test testFindMembershipCardTypeListByStoreSuccess start");String result = mockMvc.perform(get("/v1.0/membership/card/type/list-by-store").headers(header).param("storeIdList","1,2").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()).andReturn().getResponse().getContentAsString();log.info("result:{}", result);//assumeTrue的参数为true时才继续往下走,否则就停止到此assertEquals(Math.round(Double.valueOf(gson.fromJson(result, Map.class).get("code").toString())), 0);log.info("test testFindMembershipCardTypeListByStoreSuccess finished");}