目录导航
一.测试用例编写规范
1、测试用例编写目的
(1)为用例的质量负责,使用例编写工作能够有序、合理;
(2)统一测试用例编写的规范,为测试设计人员提供测试用例编写的指导,提高编写的测试用例的可读性,可执行性、合理性;
(3)能有效的提高系统所有功能点的覆盖率。
2、适用范围
适用于人员:用于测试人员阅读和执行。它们也可能会被开发人员、产品经理、项目经理等阅读审查或执行,也让新员工作为业务学习、
测试执行的参照。
适用于公司对项目的业务流程、功能(功能点)测试的测试用例编写。
3、测试用例
3.1 、用例概念
测试用例(Test Case)是为某个特殊目标而编制的一组测试输入、执行条件以及预期结果,以便测试某个程序路径或核实是否满足某个特定需求。
3.2、用例的用途
(1)指导测试工作有序进行,使实施测试的数据有据可依
(2)确保所实现的功能与预期的需求相符合
(3)跟踪测试进度,确定测试重点
(4)评估测试结果的度量标准
(5)分析缺陷的标准
3.3、用例颗粒度划分规范
用例颗粒度原则:测试用例是执行的最小实体。
用例划分基本原则是以最小功能模块来划分,为保障用例的可执行性、覆盖度,规范编写用例的粒度要求如下:
(1)一个功能正常流程,编写一个测试用例;
(2)一个功能中多个异常流程,应分开编写多个测试用例;
(3)同一功能不同入口,可合并编写一个测试用例;
(4)同一功能不同数据准备,应分开编写多个测试用例;
(5)同一个功能用例的自动化用例和功能用例要匹配,若自动化用例不能完全覆盖功能用例,自动化用例和功能用例拆分两个互补测试用例;
3.4、用例的内容格式
controller名称 | 接口名称 | 优先级 | 编号 | 用例名称 | 用例参数 | 操作步骤 | 预期结果 | 测试结果 | 测试日期 |
---|---|---|---|---|---|---|---|---|---|
AppverAppsController | findAppsPageByChannel | testFindAppsPageByChannelSuccess | [[1, 10,1],[2, 10,5] | 1 | 1 | 2019-08-21 | |||
AppverAppsController | findAppsPageByChannel | testFindAppsPageByChannelFaild | [1, ,1],[1,10,],[1,null,1],[1,10,null] | MissingServletRequestParameterException | MissingServletRequestParameterException | 2019-08-21 | |||
AppverAppsController | findAppsPageByChannel | testFindAppsPageByChannelWhenUrlIsError | [1,10,1] | No mapping for GET faild/v1/appver/apps/page | No mapping for GET faild/v1/appver/apps/page | 2019-08-21 | |||
AppverAppsController | addApps | testAddAppsSuccess | [app2, key2,1333] | The channel not exist | The channel not exist | 2019-08-21 | |||
AppverAppsController | addApps | testAddAppsWhenChannelIdIsZero | [app1, key1,0],[" , key1,1],[app1, ,1] | channelId must more than or equal to 1;name must hava a value;appKey must hava a value | channelId must more than or equal to 1;name must hava a value;appKey must hava a value | 2019-08-21 | |||
AppverAppsController | addApps | testAddAppsWhenChannelNotExist | [app1, key1,1] | No mapping for POST faild/v1/appver/apps/add | No mapping for POST faild/v1/appver/apps/add | 2019-08-21 |
(1)controller名称:测试的controller
(2)接口名称:测试的controller的接口名称
(3)优先级:测试用例的优先级别,分为高、中、低;
(4)编号:用例编号
(5)用例名称:测试用例的名称,体现测试要点;常用的结构“主、谓、宾”,名称简洁易懂,不要包括具体操作步骤;
(6)用例参数:测试用例对应接口传递的参数。
(7)操作步骤:完成该测试点所需的操作步骤;具体有以下5点要求:
a、操作步骤描述清晰。如:在什么页面,点击什么链接或按钮;页面入口、链接、按钮名称都要写清楚;
b、操作和结果是一一对应的,但操作中不要包含结果的检查;
c、用例描述中不允许存在连词、介词,比如:而且,和,还(这种情况可以拆分为多个点);
d、用例描述中不允许出现假设性词汇,比如:假如,或许,可能,…的时候等;
e、用例描述中不允许出现二义性语句;
(8)预期结果:执行完成操作后,程序预期表现的结果;具体有以下3点要求:
a、原则上每个用例必需要有预期结果,结果不能为空;
b、结果中只能包含结果,不能有步骤;
c、一个结果有多个检查点时,确保检查点完整;
(9)测试结果:
与预期结果是否相符,相符实际结果内显示Pass(表明用例通过)
与预期结果不一致显示Failed(表明执行有偏差/错误)
(10)测试日期:执行测试用例的日期
4、用例设计方法
4.1、等价类划分法
将所有可能的输入数据划分成若干个子集,在每个子集中,如果任意一个输入数据对于揭露程序中潜在错误都具有同等效果,那么这样的子
集就构成了一个等价类。后续只要从每个等价类中任意选取一个值进行测试,就可以用少量具有代表性的测试输入取得较好的测试覆盖结果。
4.2、边界值分析法
选取输入、输出的边界值进行测试。因为通常大量的软件错误是发生在输入或输出范围的边界上,所以需要对边界值进行重点测试,通常
选取正好等于、刚刚大于或刚刚小于边界的值作为测试数据。从方法论上可以看出来,边界值分析是对等价类划分的补充,所以这两种测
试方法经常结合起来使用。
4.3、错误推测法
在很大程度上是凭经验进行的,是凭人们对过去所作的测试工作结果的分析,对所揭示的缺陷的规律性作直觉的推测来发现缺陷的。
5、测试用例设计的原则
5.1、全面性
(1)应尽可能覆盖程序的各种路径。
(2)应考虑存在跨年、跨月的数据。
(3)大量数据并发测试的准备。
5.2、正确性
(1)输入界面后的数据应与测试文档所记录的数据一致;
(2)预期结果应与测试数据发生的业务吻合。
5.3、符合正常业务惯例
(1)测试数据应符合用户实际工作业务流程。
(2)兼顾各种业务变化的可能。
5.4、系统性
(1)对于系统业务流程要能够完整说明整个系统的业务需求、系统由几个子系统组成以及它们之间的关系。
(2)对于模块业务流程要能够说明清楚子系统内部功能、重要功能点以及它们之间的关系。
5.5、连贯性
(1)对于系统业务流程来说,各个子系统之间是如何连接在一起,如果需要接口,各个子系统之间是否有正确的接口;如果是依靠页面链
接,页面链接是否正确。
(2)对于模块业务流程来说,同级模块以及上下级模块是如何构成一个子系统,其内部功能接口是否连贯。
5.6、仿真性
人名、地名、电话号码等应具有模拟功能,符合一般的命名惯例。
5.7、可操作性
测试用例中应写清测试的操作步骤,不同的操作步骤相对应的操作结果。
6、用例设计步骤
6.1、测试需求分析
从项目需求分析文档/概要设计/详细设计/原型图中,了解出项目的需求。通过测试人员自己的分析、 理解,整理成为测试需求,使测试人
员能清楚被测项目包含的功能点。
6.2、业务流程分析
分析了解被测试项目所属的行业及其业务知识。对被测项目的业务流程要全部梳理出来(可画出项目的流程图,也可用头脑风暴)。
项目的流程:主线流程、分支流程、数据流转,流转过程中关键点的判断条件以及完成操作的一些非必要条件。
6.3、测试用例设计
主要包括功能测试、界面测试、兼容性测试、易用性测试、异常测试、性能测试、压力测试等,在设计用例时要尽量考虑录入正常、边界、
异常值等系统的处理情况。
6.4、测试用例评审
由测试用例设计者发起,参加的人员需包括测试负责人、项目经理、 开发人员及其他相关的测试人员。
6.5、测试用例完善
测试用例编写完成后,应对测试用例进行持续的维护:
(1)新项目需求变更,应及时对测试用例进行修改;
(2)维护期项目,可根据项目组情况周期对用例进行维护;
(3)所有发现的bug和故障,基于测试用例无法发现,需转化为测试用例。
二.测试规范
一般来说,单元测试任务如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3BhEvDQZ-1577356061365)(.attachments/itemContent%20(2)]-61176e75-1e95-4082-a222-0b3fecccbb61.png)
1、接口功能测试:用来保证接口功能的正确性
2、局部数据结构测试(不常用):用来保证接口中的数据结构是正确的
(1)比如变量有无初始值
(2)变量是否溢出
3、边界条件测试
(1)变量没有赋值(即为NULL)
(2)变量是数值(或字符)
a.主要边界:最小值,最大值,无穷大(对于DOUBLE等)
b.溢出边界(期望异常或拒绝服务):最小值-1,最大值+1
c.临近边界:最小值+1,最大值-1
(3)变量是字符串
a.引用"字符变量"的边界
b.空字符串
c.对字符串长度应用"数值变量"的边界
(4)变量是集合
a.空集合
b.对集合的大小应用"数值变量"的边界
c.调整次序:升序、降序
(5)变量有规律
a.比如对于Math.sqrt,给出n^2-1,和n^2+1的边界
4、代码覆盖率
a.语句覆盖:保证每一个语句都执行到了
b.判定覆盖(分支覆盖):保证每一个分支都执行到
c.条件覆盖:保证每一个条件都覆盖到true和false(即if、while中的条件语句)
d.路径覆盖:保证每一个路径都覆盖到
举例:
/**
* 优惠券已购买或已验证加一
*
* @param couponBaseinfoBo
* @return
*/
@TccTransaction(propagation = DTXPropagation.SUPPORTS, executeClass = CouponBaseInfoController.class)
@Transactional(rollbackFor = Exception.class)
@Override
public ApiResult<Boolean> updateCount(CouponBaseinfoBo couponBaseinfoBo, Integer number) {
try {
lockforUpdateCount.lock();
CouponBaseInfoValidateBo validateBo = new CouponBaseInfoValidateBo();
BeanTool.copyProperties(couponBaseinfoBo, validateBo);
ValidateUtils.validate(validateBo, UpdateCountValidation.class);
log.info("---updateCount---begin,{}", couponBaseinfoBo);
CouponBaseinfoPo couponBaseinfoPo = couponBaseinfoService.getById(couponBaseinfoBo.getId());
if (couponBaseinfoPo == null || couponBaseinfoPo.getStatus() == CouponStatusEnum.CREATED.getStatus()) {
return ApiResult.failed(CouponErrorCode.FAILED);
}
CouponBaseinfoPo po = new CouponBaseinfoPo();
po.setId(couponBaseinfoBo.getId());
//action为1是给购买数量加1,为2是个验证数量加1
if (couponBaseinfoBo.getAction() == CouponConstant.ONE) {
int count = couponBaseinfoPo.getSaledCount() + number;
if (count == couponBaseinfoPo.getTotalStock()) {
po.setStatus(CouponStatusEnum.SOUDOUT.getStatus());
}
po.setSaledCount(count);
} else if (couponBaseinfoBo.getAction() == CouponConstant.TWO) {
int count = couponBaseinfoPo.getVerifiedCount() + CouponConstant.ONE;
if (count > couponBaseinfoPo.getSaledCount()) {
return ApiResult.failed(CouponErrorCode.FAILED);
}
po.setVerifiedCount(count);
} else {
return ApiResult.failed(CouponErrorCode.FAILED);
}
couponBaseinfoService.updateById(po);
//放入redis,事务失败时回滚
TracingContext tracing = TracingContext.tracing();
if (tracing.hasGroup()) {
String groupId = tracing.groupId();
String key = tccKeyUtil.buildTccKey(RedisConfig.PREFIX, "CouponBaseInfoController.updateCount", groupId);
if (StringUtils.isNotBlank(key)) {
redisService.setBean(key, couponBaseinfoPo, 10, TimeUnit.MINUTES);
}
log.info("updateCount" + groupId);
log.info(port);
}
log.info("---updateCount---result,{}", po);
return ApiResult.ok(true);
} finally {
lockforUpdateCount.unlock();
}
}
@Data
@EqualsAndHashCode(callSuper = false)
public class CouponBaseInfoValidateBo {
/**
* 优惠券id
*/
@Min(groups = {QueryValidation.class}, value = 1, message = "id must more than or equal to {value}")
@NotNull(groups = {UpdateCountValidation.class, UpdateValidation.class}, message = "ID must have a value!")
@Null(groups = {InsertValidation.class}, message = "ID must is null")
private Integer id;
/**
* 优惠券名称
*/
@NotBlank(groups = {InsertValidation.class},message = "name must have a value")
@Length(groups = {InsertValidation.class},min = 1,max = 100,message = "name length must between {min} and {max}")
private String name;
/**
* 操作,1为购买,2为验证
*/
@NotNull(groups = {UpdateCountValidation.class},message = "action must have a value")
@Min(groups = {InsertValidation.class}, value = 1, message = "action must more than or equal to {value}")
private Integer action;
/**
* 原价
*/
@Digits(groups = {InsertValidation.class},integer = 8, fraction = 2, message = "originalPrice must less than 8 digits in length and up to 2 decimal places!")
@NotNull(groups = {InsertValidation.class}, message = "originalPrice must have a value!")
private BigDecimal originalPrice;
/**
* 售价
*/
@Digits(groups = {InsertValidation.class},integer = 8, fraction = 2, message = "sellingPrice must less than 8 digits in length and up to 2 decimal places!")
@NotNull(groups = {InsertValidation.class}, message = "sellingPrice must have a value!")
private BigDecimal sellingPrice;
/**
* 总库存
*/
@NotNull(groups = {InsertValidation.class},message = "totalStock must have a value")
@Min(groups = {InsertValidation.class}, value = 1, message = "totalStock must more than or equal to {value}")
@Max(groups = {InsertValidation.class}, value = 10000, message = "totalStock must less than or equal to {value}")
private Integer totalStock;
/**
* 每个订单购买的最大个数
*/
@Min(groups = {InsertValidation.class}, value = 0, message = "maxPurchasesPerOrder must more than or equal to {value}")
private Integer maxPurchasesPerOrder;
/**
* 每人购买的最大个数
*/
@Min(groups = {InsertValidation.class}, value = 0, message = "maxPurchasesPerPerson must more than or equal to {value}")
private Integer maxPurchasesPerPerson;
/**
* 详情内容
*/
@NotBlank(groups = {InsertValidation.class},message = "content must have a value")
private String content;
/**
* 图片url
*/
@NotBlank(groups = {InsertValidation.class},message = "imgUrl must have a value")
@Length(groups = {InsertValidation.class},min = 1,max = 255,message = "imgUrl length must between {min} and {max}")
private String imgUrl;
/**
* 链接url
*/
@Length(groups = {InsertValidation.class},min = 1,max = 255,message = "linkUrl length must between {min} and {max}")
private String linkUrl;
/**
* 优惠券类型:1,普通优惠券 2,拼团优惠券
*/
@NotNull(groups = {InsertValidation.class},message = "type must have a value")
@Min(groups = {InsertValidation.class}, value = 1, message = "type must more than or equal to {value}")
private Integer type;
/**
* 拼团人数
*/
@Min(groups = {InsertValidation.class}, value = 0, message = "groupNum must more than or equal to {value}")
private Integer groupNum;
/**
* 短链接
*/
@Length(groups = {InsertValidation.class},min = 1,max = 255,message = "shortLinks length must between {min} and {max}")
private String shortLinks;
/**
* 短链接
*/
private Object multipleImgUrl;
}
注:a.action为0,1,2和其它。
b.构造成功的case时要考虑中间可能出现的异常,比如CouponErrorCode.FAILED
c.校验BO中的所有校验器(包括自定义的)必须覆盖,除了一些难以构造的,
比如说@Length(groups = {InsertValidation.class},min = 1,max = 255,message = “shortLinks length must between {min} and {max}”)
5、各条错误处理测试:保证每一个异常都经过测试
三.实施方案
1、idea安装junit插件
idea整合junit5之前,先安装配置junit插件:https://blog.csdn.net/nicolas12/article/details/81223938
2、添加pom依赖:
<!--junit5依赖-->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.3.2</version>
<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
@Test
void testDisabled(){
log.info("test Disabled");
}
D、@BeforeEach 表示方法在每个测试方法运行前都会运行
E、@AfterEach 表示方法在每个测试方法运行之后都会运行
F、@BeforeAll 表示方法在所有测试方法之前运行(类级别方法,必须位静态方法)
G、@AfterAll 表示方法在所有测试方法之后运行 (类级别方法,必须位静态方法)
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@BeforeAll
public static void BeforeEach() throws Exception {
//在所有测试方法运行前运行
log.info("Run before all test methods run");
}
@BeforeEach
public 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 {
}
@TestOnMac
void testOnMac() {
// ...
}
J、@EnabledOnJre(基于哪个版本的jre执行)如 @EnabledOnJre({ JAVA_9, JAVA_10 })
5、断言
A、assertEquals 断言预期值和实际值相等(参数可以是int double string等等)
B、assertFalse 断言条件为假
C、assertNotNull 断言不为空
D、assertTrue 断言条件为真 举例:
//参数为int
assertEquals(Math.round(Double.valueOf(gson.fromJson(result, Map.class).get("code").toString())),0);
//参数位字符串
assertEquals(result,"");
//参数为boolean
assertTrue(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、几个方法的简单说明:
方法名 | 作用 | 举例 |
---|---|---|
param | 请求的参数设置方法 | .param(“channelId”, channelId) |
post | 请求方法 | .post("/v1/appver/apps/add") |
content | 请求参数 | .content(newRequestBody) |
contentType | 参数类型 | .contentType(MediaType.APPLICATION_JSON) |
accept | 接收返回值类型 | .accept(MediaType.APPLICATION_JSON) |
andExpect | 添加执行完成后的断言 | .andExpect(status().isOk()) |
andReturn | 表示执行完成后返回相应的结果 | .andReturn() |
最后注意:因为测试不能影响数据库的数据,所以测试类上都要加事务回滚 如下:
@Slf4j
@SpringBootTest
@ExtendWith(SpringExtension.class)
@Rollback
@Transactional
public class AppverAppsControllerTest {
测试用例得出的某些问题:
1、添加数据时,要考虑重名判断,用assumeTrue判断返回的校验提示语。
2、修改数据时,要考虑参数Id是否在表中存在,不存时候mybatis-plus会将其作为插入数据处理,插入数据时要保证某些数据不能为空,所以建议加个自定义校验,用assumeTrue判断返回的校验提示语,专门处理junit或者postMan请求的情况。也要和添加数据一样考虑各种校验问题。
9、增加app服务的验证签名之后的junit修改
a.注入appId和appSecret,当然配置文件中已经配置此属性。
@Value("${app.id}")
private String appId;
@Value("${app.secret}")
private String appSecret;
b.在before方法中得到需要添加到header中的值
@BeforeEach
public void before() throws Exception {
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);
bys_signature = SignUtils.sign(params);
bys_timestamp=ts.toString();
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
c.mockMvc调用接口时添加header签名
String result = mockMvc.perform(post("/v1.0/coupon/store/add")
.header("bys_appId",appId)
.header("bys_signature",bys_signature)
.header("bys_timestamp",bys_timestamp)
.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 StoreConfig
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:h2test;DB_CLOSE_DELAY=-1;MODE=MySQL
username: sa
password: sa
h2:
console:
enabled: true
# 如果使用了flyway
flyway:
enabled: false
b) **如果使用了redis,应将 database 设置与真实环境不一样的库
c) 其他应用中用到的信息,比如我们使用的鉴权信息,有应用配置的电话号码国家区号等等
例:
# 应用授权信息
bys:
app:
# appId
id: ${APP_ID:10000003}
# secret
secret: ${APP_SECRET:99F11D010F20A896FED5D0FEEEFE031C}
3、测试代码中profile的使用
a) 一般情况下我们均使用 test profile,添加类注解 @ActiveProfiles(“test”) 比如:
@ExtendWith(SpringExtension.class)
@ActiveProfiles("test")
@SpringBootTest
class UserControllerTest {
b) 如有特殊测试需要测试真实数据中的数据,请使用注解 ActiveProfiles 引入相应环境的 profile,该profile必须是在 java/resources 或者 test/resources 中真实存在的
4、测试数据的规范
a) 上传类的测试文件应放置在 test/resources/data/upload 中
b) 由于测试过程中需要mock大量数据进行测试,可放置 json 在 test/resources/data/mock 中,测试类在 before 中引入
六.集成方案
使用Jenkins集成单元测试,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UXZgoVNd-1577356061366)(.attachments/itemContent%20(3)]-ba9d6bd3-ced6-47a4-99a4-44c49baefa97.png)
1、安装JDK
a.下载安装包
b. 配置环境变量
2、安装Jenkins
a.安装Git插件
b.安装Junit插件
3、配置Jenkins
a.设置JDK路径
b.设置Git路径
c.设置maven路径
d.设置Junit路径
4、新建测试项目
源码管理
1.添加tfs地址
2.选择分支
构建触发器
1.触发远程构建
2.再其他项目完成后执行该构建
4.当源码更新推送至tfs后执行构建
5.定期检查源码是否更新
构建测试项目
1.使用maven编译项目
2.使用Junit运行项目
来源:CSDN
作者:huihui-6020
链接:https://blog.csdn.net/weixin_41700804/article/details/103720530