单元测试规范流程

混江龙づ霸主 提交于 2019-12-26 23:56:02

目录导航


一.测试用例编写规范

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();

四.验收方法

UnitTest检查单 编号 检查项 第一次检查 第二次检查 第三次检查 备注 检查結果 检查日期 检查結果 检查日期 检查結果 检查日期 1 运行TEST是否都pass 2 无法请求或者难以模拟的方法,要用mock function$this->getMock 3 相关程序初始化的工作在setUp进行 4 正面测试:确保一切都是期望的工作 5 负面测试:确保每个失效或异常都被测试到 6 正面序列测试:确保按照正确顺序调用可以像期望一样工作 7 负面序列:确保不按正确顺序调用时失败 8 负载测试:确保测试的性能在期望范围之内。如2,000 次调用应该在2秒内完成 9 资源测试:正确分配释放大资源 10 为每一个业务逻辑模块(尤其是涉及到数据的的方法)编写测试驱动 11 方法的每一个参数都要有测试方法,不要忽略带有默认值的参数 12 为方法的返回结果跟期望结果做断言 13 为返回结果数量做断言 14 为返回值类型做断言 [UnitTest检查单.xls](.attachments/UnitTest检查单-f1f9c796-514b-4d83-a4e6-141a8d5d6c06.xls)

五.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大量数据进行测试,可放置 jsontest/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运行项目
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!