自动化工具 我对自动化测试的一些认识

Nisir · 2017年08月09日 · 最后由 eTest 回复于 2023年07月19日 · 8561 次阅读
本帖已被设为精华帖!

前言

从 2017 年初开始,到现在差不多半年多的时间,我这边投入了一部分精力用于项目的自动化测试建设工作。目前来看收益还是比较明显的,在这个过程中也加深了对自动化测试的理解,这边就总结下自己对自动化测试的认识。

首先我想说下在开展自动化前后,我的工作状况的对比:

  • 去年 2016 年 Q3、Q4,基本上天天处于 996 甚至 997 的状态。大部分时间都花在了功能测试保障、回归测试和上线验证。因为项目在线上有多达 7,8 个不同的集群,每一次版本上线为了保险起见,开发会一个集群一个集群小心翼翼地上线,所以基本上天天都处于上线的状态。每次上线,手工测试时间顺利的话在半小时左右,如果遇到问题跟开发联调定位,会达到数个小时。除了时间上开销很大之外,每次上线带来的精神上的压力其实更严重,非常害怕哪个用户半夜跳出来反馈说调度有 Bug。

  • 在开展了自动化测试之后,现在我给 “日常版本迭代测试” 只预估了 25% 的工作量。任意集群上线,我只要负责点一下 Jenkins 的 “开始构建” 按钮,即可完成验收。如果有出现用例失败,会自动发送邮件告知开发。

显然,目前的工作状态要好很多,是因为项目工作量减少了吗?显然不是。其实今年以来整个项目组在研发的投入要比去年更多,工作量只会比以前更重,还要兼顾多个私有化部署的客户的验收和日常测试保障工作,工作量肯定是只增不减的。之所以能有更多的时间空余出来做其它更多维度的事情,这一切都得益于 “自动化测试” 的帮助,它极大地解放了我的手工测试时间,同时更加提升了上线的信心。

1. 需求和目标

在我开展自动化测试之前,其实该项目以前的测试人员也已经写了很多的接口测试用例,但是大多数用例处于 “半瘫痪” 状态,在 CI 上无人维护(听说起初是有人维护的,但是后来用例多了,维护的人每次花很长时间去定位问题,结果却发现大部分的问题都是环境问题导致,花了半天时间定位却没什么收益,久而久之便不想去维护)。看起来,自动化似乎并没有什么收益,反而维护用例会造成额外的工作负担。

我觉得,其实自动化测试跟其它任何一种测试类型(比如异常测试、稳定性测试、性能测试等)都是类似的,它也是一种测试类型而已。在开展测试之前,我们首先必须要明确自动化测试的需求是什么,要解决什么样的问题。

1.1 让 “自动化” 代替 “手动”

在我看来,初期的自动化测试,我的目标很明确,我就是要让 “自动化” 代替 “手动”,让自动化真正地跑起来,凡是 “自动化” 跑过的内容,我绝不再去手工重复执行一遍。这样至少我有一个很明确的收益:每完成一条自动化用例,我减少了一条手工用例的执行时间。

必须要提醒的是,让 “自动化” 完全替代 “手动”,其实对自动化用例的稳定性、容错都有一定的要求。你要花一定时间去思考用例执行过程中的异常场景,是否足以充分替代手工测试。因此,我在增加用例的时候都会非常谨慎,确保用例集是稳定 100% 通过的前提下才会增加新的用例。

对于正常情况下(排除环境、开发代码的问题)有时 100% 通过,有时 90% 通过的自动化用例集,我觉得它的作用和参考价值为 0。正常的用例集就应该是 100% 通过的。

1.2 让 “回归” 自动化

上节说了让 “自动化” 替代 “手动”,每完成一条自动化用例都是有明显收益的。那如何让收益最大化呢,当然是让每次回归或上线验证 “不得不” 执行的用例优先自动化。如果完成了回归用例集的全部自动化,那我就可以用它来替代我的日常回归,和上线回归工作,极大地释放我的手工验证时间。

这里必须要指出的是,我跟的项目其实是一个对系统稳定性的要求要高于新功能的引入的一个后台项目,所以它的核心功能是比较固定的,其实大多数后台项目也是类似的,核心功能聚合、对系统的稳定性要求高。这就需要保障系统的核心功能完善。所以我们可以先将 “核心功能” 的验证完成自动化。

1.3 不要让环境成为瓶颈

前面说了,旧的用例集在维护的过程中给测试人员增加了很多额外的负担,到最后发现很多都是环境的问题。当时的情形就是专门搭建了另一套测试环境专门用于自动化测试,而大数据的后台环境搭建和维护非常的复杂,如果同时维护多套环境,难免会在一些组件升级的过程中出现遗漏,导致环境不同步。因此,我们的自动化测试用例前期完全可以直接在功能测试环境执行,因为功能测试环境肯定是会一直随着版本的迭代向前不断更新的。

2. 技术选型

在明确了目标后,要开始技术选型。常见的自动化测试类型,包括

  • 接口自动化
  • UI 自动化
  • 基于 shell 交互命令执行的自动化

此外,不属于测试范畴,但是也可以实现自动化、释放手工时间的还有

  • 数据准备自动化
  • 环境编译、部署、打包自动化
  • 稳定性测试/性能测试结果指标获取、校验自动化
  • 机器资源监控、报警自动化
  • 其它所有手工重复执行的操作

在开始自动化之前,首先要分析项目的架构和状况。对于一个后端的服务,它如果是纯粹以接口的形式提供给其它组件去调用,那可以采取 “接口自动化”;对于一个 Web 产品,如果前后端都在测试的保障范围,而且前端页面相对比较稳定,可以考虑采用 “UI 自动化”(此时接口自动化其实已经不足以保障产品的端到端功能);对于更后端的组件,如果想测试组件自身的基础核心功能,可以采用 “基于 shell 交互命令执行的自动化”,通过自动化脚本的方式封装 shell 命令的调用。

此外,有些人可能还会执着于编程语言的选择,是用 Java 还是 Python 还是 Shell,或者其它语言等等。这个我觉得其实没有定论,可以根据自己对语言的偏好和熟练程度,但是必须要考虑团队成员的普遍技术栈,因为后期可能其他人来接手这个项目时需要代替你去维护测试工程。通常来说,测试框架的选择(不管是接口自动化、UI 自动化)推荐使用 Java 的 TestNG 框架;对于简单的基于命令行执行的自动化脚本的编写推荐使用 Shell(Shell 非常地强大);对于稍复杂的一些自动化的脚本的编写,推荐使用 Python,在 Python 中可以非常方便地封装 Shell 命令,同时 Python 区别于 Shell 的一个特性就是它支持面向对象的封装,可以将一些对象封装在特定的类中,增加程序的可读性和健壮性。

这里再插一段题外话:有些人可能会疑惑,现在其实有很多接口测试平台,测试人员可以直接在平台上完成接口测试,在选型时怎么抉择?——这里我不评价哪种方式更好,只想说下自己的看法:我觉得两种其实各有各的好处:

  • 编写代码的方式:

优点:提升自己的编码能力,问题定位能力,具备更高的灵活性和可操作性。 缺点:结果展示不直观,不易于协作。其他人维护代码困难,难以推动开发执行。

  • 接口平台的方式:

优点:简便,上手容易,可以在项目组间很好的协作和维护,测试记录和结果一目了然。 缺点:离开了平台,可能又要回归手动。

对于测试人员而言,如果有精力和时间的话,我建议是两种都要掌握,甚至是自己去开发接口测试平台的能力。

3. 自动化实施过程

目前我跟的项目里已经实现自动化的内容包括:基于接口的场景回归自动化测试、编译部署过程自动化、Jacoco 覆盖率统计并接入 CR 平台(代码变更分析平台)的自动化、对外/上线打包发布的自动化、稳定性测试结果校验的自动化。

下面着重介绍下项目的接口自动化框架的搭建和设计过程。

3.1 准备工作

老生常谈,开始自动化前,我仍然想再次强调一定要明确自己的需求是什么。在我的项目里,我的需求主要有以下几点:

  • 同一份代码可以在多个集群执行
  • 各个集群的测试数据相互独立,不会互相影响
  • 可以方便地与数据库进行交互
  • 当用例执行出错时,有详细的日志帮助定位
  • 较好的可维护性和集群扩展性。

3.2 框架搭建

3.2.1 环境搭建

环境搭建时,主要用了以下工具:

  • Git:管理代码工程
  • TestNG:作为测试框架
  • Maven:管理依赖包
  • Log4j:管理日志
  • Hibernate:实现数据库交互
  • HttpClient:实现请求发送

之所以没有用 MyBatis,觉得相对来说,MyBatis 是一个半 ORM 的框架,它需要自己额外维护一份 sql 映射文件,而 Hibernate 是全 ORM 的,可以省去这一步。关于它俩的比较,大家可以参考下知乎的一篇文章:MyBatis 和 Hibernate 的对比。对于 JDBC 的方式,当然它也可以访问数据库,只不过相对来说,使用 ORM 框架可以更贴近面向对象的编程方式。

3.2.2 不同集群配置管理

在实现过程中,因为不同的集群会有不同的配置,比如 webserver host、登陆后台 webserver 的用户名/密码、公共账号信息、数据库信息等等。为了让一份代码可以在不同集群去共用,就必须把这些配置信息从代码中剥离出来。可以用配置文件的形式来统一管理集群的配置信息,如图所示:

配置文件管理

每个文件代表一个集群的配置。在代码中可以通过 java.util.Properties 类读取配置文件的方式载入各项配置信息:

/**
 * 根据指定的配置文件名,初始化配置
 * @param configFile
 * @throws IOException
 */
public PropertiesUtil(String configFile) throws IOException{
    this.configFile =DEFAUL_CONFIG_FILE_DIRECTORY + configFile;
    InputStream fis = new FileInputStream(this.configFile);
    props = new Properties();
    props.load(fis);
    //关闭资源
    fis.close();
}

/**
 * 根据key值读取配置的值
 * @param key key值
 * @return key 键对应的值 
 * @throws IOException 
 */
public String readValue(String key){
    return  props.getProperty(key);
}

/**
 * 读取properties的全部信息
 * @throws FileNotFoundException 配置文件没有找到
 * @throws IOException 关闭资源文件,或者加载配置文件错误
 * 
 */
public Map<String,String> readAllProperties(){
    //保存所有的键值
    Map<String,String> map=new HashMap<String,String>();
    Enumeration<?> en = props.propertyNames();
    while (en.hasMoreElements()) {
        String key = (String) en.nextElement();
        String property = props.getProperty(key);
        map.put(key, property);
    }
    return map;
}

到这里,解决了配置读取的问题,还需要解决代码运行时如何让它自己去选择正确的集群配置文件的问题。我是将选择配置文件的逻辑全部封装到了一个工厂类 BaseConfigFactory.java 中,在实际测试使用时,我只需要通过工厂类的静态方法 BaseConfigFactory.getInstance() 去获取想要的配置信息,而不需要关心它到底是如何去选择正确的配置文件的。工厂类的实现可以参考:

public class BaseConfigFactory {
    private static final String testEnv= System.getenv("TEST_ENV") == null ? "null" : System.getenv("TEST_ENV");
    private static Logger logger = Logger.getLogger(BaseConfigFactory.class);
    private static BaseConfig baseConfig;
    private static HashMap<String, String> clusterConfigMap;
    public static synchronized BaseConfig getInstance(){
        if (null == baseConfig){
            PropertyConfigurator.configure("log4j.properties");
            initMap();
            setupConfig();
        }
        return baseConfig;
    }


    public static void initMap(){
        clusterConfigMap = new HashMap<>();
        clusterConfigMap.put("TEST-BJ", "test-bj.properties");
        clusterConfigMap.put("ONLINE-BJ", "online-bj.properties");
        clusterConfigMap.put("ONLINE-XS", "online-xs.properties");
        clusterConfigMap.put("ONLINE-LT", "online-lt.properties");
        clusterConfigMap.put("ONLINE-BEIJING", "online-beijing.properties");
        clusterConfigMap.put("ONLINE-HD", "online-hd.properties");
        clusterConfigMap.put("null", "test-local.properties");
    }

    public static void setupConfig(){
        logger.info("TEST ENV: " + testEnv);
        String propertyFile = clusterConfigMap.get(testEnv);
        logger.info("Using '" + propertyFile + "' as property file.");
        baseConfig = new BaseConfig(propertyFile);      
    }

}

即,将所有的集群的配置放入到一个 Map 中,然后通过读取环境变量 TEST_ENV 的值来选取具体的集群配置文件 clusterConfigMap.get(testEnv)。

3.2.3 log4j 日志管理

良好的日志输出是帮助定位问题的关键环节,尤其是定位服务器上执行时出现的问题。这边贴一个 log4j 的配置:

### set log levels ###
log4j.rootLogger = debug, stdout, D, E

### 输出到控制台 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender 
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss.SSS Z} %p [%c{1}] [Thread-%t] %m%n

### 输出到日志文件 ###
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File = logs/console.log
log4j.appender.D.Append = true
##输出Debug级别以上的日志##
log4j.appender.D.Threshold = INFO  
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss.SSS Z} %p [%c{1}] [Thread-%t] %m%n

### 保存异常信息到单独文件 ###
log4j.appender.E = org.apache.log4j.DailyRollingFileAppender
##异常日志文件名##
log4j.appender.E.File = logs/error.log
log4j.appender.E.Append = true
##只输出ERROR级别以上的日志##
log4j.appender.E.Threshold = ERROR
log4j.appender.E.layout = org.apache.log4j.PatternLayout
log4j.appender.E.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss.SSS Z} %p [%c{1}] [Thread-%t] %m%n

##Hibernate日志级别设置
log4j.logger.org.hibernate.ps.PreparedStatementCache=WARN
log4j.logger.org.hibernate=ERROR

# Changing the log level to DEBUG will result in Hibernate generated
# SQL to be logged.
log4j.logger.org.hibernate.SQL=ERROR

# Changing the log level to DEBUG will result in the PreparedStatement
# bound variable values to be logged.
log4j.logger.org.hibernate.type=ERROR

该配置将 INFO 级别和 ERROR 级别的日志分别定位输出到不同的文件,且日志文件会按照日期进行自动归档,输出的格式包含了日志的日期、级别、类信息、线程信息、日志内容等。

一般情况下,对于接口测试,当接口测试用例失败时,我们要打印的日志包括:请求的 url、参数、方法、实际响应、期望响应等等。

3.3 分层设计、解耦

首先看一下项目的工程目录:

项目工程目录

可以看到,项目中包含了多个 package,各个 package 的作用已经在图片中标示了。以前好多测试人员的习惯是将 api 代码的调用、测试方法的编写、data Provider 的编写、测试数据的构造全部写在一个类文件中,这样做其实会有几个问题:

  • 可读性差
  • 代码复用性低
  • 维护性差
  • 难以调试
  • 耦合带来的其它各类问题

此外,如果不同集群的测试数据不同,会有大量的 if 判断,结果是灾难性的。

下面以一个用例为例,展示代码的结构:

测试 api:

public class ScheduleApi extends BaseAzkabanApi{
    ...
    ...
    /**
     * 使用默认公共账号、email、失败策略、sla报警邮箱新增正常调度。
     * @param projectName
     * @param flow
     * @param projectId
     * @param scheduleTime
     * @param scheduleDate
     * @param period
     * @return
     */
    public ResponseCode addNormSched(String projectName, String flow, String projectId, String scheduleTime, String scheduleDate,String period){
        return scheduleFlow(projectName, flow, projectId, scheduleTime, scheduleDate, defaultProxyUser, defaultProxyEmail, period,  defaultSlaEmail);
    }
    ...
    ...
}

测试代码 test:

@Test(singleThreaded=true)
public class ScheduleTest{
    ...
    ...
    /**
     * 新增正常调度
     * @param projectName
     * @param flow
     */
    @Test(priority=1, dataProvider="addNormSched", dataProviderClass=ScheduleDataProvider.class, testName="1410356")
    public void addNormSched(String projectName, String flow, String expectedStatus, String hasScheduleId, String message){
        ResponseCode rc= scheduleApi.addNormSched(projectName, flow);
        Assert.assertEquals(rc.getStatus(), expectedStatus, message+rc.getDebugInfo("返回结果中的状态status对应值"));
        Assert.assertEquals(rc.hasProperty("scheduleId"), Boolean.parseBoolean(hasScheduleId), message+rc.getDebugInfo("返回结果中是否包含scheduleId"));
    }
    ...
    ...
}

测试用例 dataProvider:

public class ScheduleDataProvider {
    @DataProvider(name = "addNormSched", parallel=true)
    public static Object [][] addNormSched(){
        return new Object[][]{
            ScheduleTestData.validNormSchedule,
            ScheduleTestData.notExistedProject,
            ScheduleTestData.notExistedFlow
        };
    }
    ...
    ...
}

测试数据 testdata:

public class ScheduleTestData extends BaseTestData{ 
    ...
    ... 
    //Testdata for addNormSched
    public static Object[] validNormSchedule={VALID_PROJECT_NAME, VALID_NORMAL_SCHEDULE_FLOW, "success", "true", "设置有效的正常调度"};
    public static Object[] notExistedProject={NOT_EXIST_PROJECT_NAME, VALID_NORMAL_SCHEDULE_FLOW, "error", "false", "不存在的project"};
    public static Object[] notExistedFlow={VALID_PROJECT_NAME, NOT_EXIST_FLOW_NAME, "error", "fasle", "不存在的flow"};
    ...
    ...
}

可以看到,用例的测试代码 test 类是非常简洁的,只要调用 api 类封装的接口,然后进行 assert 判断即可。

关于测试数据,将 dataprovider 与 testdata 进行分离,也是为了后续可能会灵活地调整下架用例,只需要去除 dataprovider 类中的用例行即可,而 testdata 中的数据仍然可以留着复用。

另外,前面提到了不同集群测试数据的管理。再介绍下我这边的实现方式:

  • 不同测试类使用的公共数据,存放于 BaseTestData 基类中,让其它 testdata 类继承于基类
  • 不同集群可以共用的数据,尽量共用,以常量的方式存储于 testdata 类中
  • 不同集群无法共用的数据,统一存放于特定的 json 文件管理

关于 json 文件管理数据,其实跟配置文件的管理类似,如下图所示:

json数据目录

History.json:

{           
    "validTotalFetch":{
        "key":"",
        "beginTime":"2017-06-30%2015:30",
        "endTime":"2017-06-30%2015:50",
        "expectedTotal":"7"
    },

    "validImmediatelyFetch":{
        "key":"instant_execute_job",
        "beginTime":"2017-06-30%2013:30",
        "endTime":"2017-06-30%2013:40",
        "expectedTotal":"1"
    },

    "validScheduledFetch":{
        "key":"online_schedule_job",
        "beginTime":"2017-06-30%2014:30",
        "endTime":"2017-06-30%2014:40",
        "expectedTotal":"2"
    }
}

3.4 改进与提升

在自动化的实施过程中,还遇到了一些问题可能对其它项目也会有一定的借鉴意义。这边罗列下几个我觉得比较有意思的问题。

3.4.1 webserver 高可用的支持

我们的后台 webserver 是支持高可用的,所以每次运维上线后 webserver 的 host 可能会发生变化,以及在服务运行过程中也可能会发生 webserver 切换。如果每次去手动调整自动化用例的配置信息,是一件非常麻烦的事情。

解决的方式就是在配置文件中,将主从 webserver 的 host 都填写进去,在测试过程中,如果发生请求失败,则允许切换一次 host。

3.4.2 用例并发执行

由于我们的一部分用例是异步的场景用例,需要执行一个数据开发的任务,然后等待其执行完成。这些用例的执行比较费时,如果顺序执行的话会消耗非常多的时间。因此可以通过并发执行测试的方式,解决用例耗时的问题。关于 TestNG 的并发可以参考这篇文章:《简单聊聊 TestNG 中的并发

3.4.3 单例模式解决 session 问题和 host 重复切换问题

  • 问题 1: Azkaban 的每个接口,都需要一个必传参数 seesion。这个 session 可以通过/login 接口获取。如果每个接口在执行的时候都去调用一次/login 接口重新获取 session,就会显得很冗余,也可能导致旧的 session 失效。

  • 问题 2: 上述提到的对 webserver 高可用的支持,当多条用例并行执行时如果同时去切换 host,可能会造成 host 切换回原来的不可用 host。

对于问题 1,可以将 session 作为单例的方式进行存储。

对于问题 2,可以借鉴单例模式的 “双重检查” 思想,对切换 host 的代码进行部分同步,在防止 host 重复切换的同时,不会降低 httpclient 请求的并发性。关于单例模式的应用可以参考这篇 KS 文章:《“单例模式” 学习及其在优化接口自动化测试代码中的实践

3.4.4 “变” 与 “不变”

其实这也是所有设计模式的基本思想,即区分自动化测试中的 “可变因素” 和 “不变因素”。我觉得 ycwdaaaa 大神(飞哥)有两句话是非常棒的:

  • 封装"一切"可能的可控的变化因素
  • 为了稳定使尽"一切"手段

4. 结合研发过程的应用

上面介绍了一些自动化的实施过程,这边再介绍下实施之后在项目研发过程中的应用。

目前在项目中,主要有以下几方面的应用。

(1)提测后的自动化回归验收

下图是项目的一条持续集成 pipeline。在开发提测后,我会自动化地完成以下事情:

  • 编译代码
  • 将服务部署到各个机器,并完成 Jacocod Agent 的部署
  • 执行静态代码检查
  • 执行接口测试
  • 完成覆盖率统计
  • 将覆盖率统计数据接入到 CR 平台

当自动化用例全部执行通过时,说明系统的核心功能回归没有问题,然后开始版本的细粒度功能的测试。

持续集成pipeline

(2)Bug 修复后的回归验收

在测试过程中,开发肯定会经常修复 bug 重新提交代码,每次有代码重新提交时,我都可以一键完成部署、测试、覆盖率统计。

(3)上线后的回归验证

目前,项目的上线验证已经完全由自动化验证来替代。

(4)作为开发冒烟的一部分(未完成)

目前已经跟开发达成一致,开发非常欢迎将自动化用例接入到开发环境,用于他们每次变更时的环境正确性验证,可以尽早帮助他们发现研发过程中出现的问题。并且在提测前,只有 100% 通过自动化测试才可以进行提测。

(5)线上监控

目前各个线上集群,都部署了自动化测试用例,这部分用例会每隔 4 小时执行一次。用于确保线上环境的稳定性。从效果上来看,线上监控的成效是非常明显的,提前发现了很多集群的延迟问题,环境问题等,让开发可以及时地收到报警,了解线上集群的情况。

(6)关于持续集成

可能有人会发现,上述的执行过程其实不是真正意义上的持续集成,真正意义上的持续集成应该是:每次开发提交代码,自动触发构建。

必须要承认的是,确实是如此。但是不管怎么样,我觉得可以先从优化测试工作量的角度慢慢去推开整个流程,其实业界目前也并没有确切的定论说只有持续集成才是最佳的实践。相反,一味地持续集成可能会增加我们的维护成本。只要我们能切实提升自己的工作效率,达到目的就可以了。

5. 成效

当自动化做的比较完善后,你真的会发现:生活原来可以变得如此简单美好。

自动编译部署:测试过程中开发修复 bug 提交代码是非常频繁的,每次的手动编译部署可能都会耗费十几分钟,并且测试人员的关注点还不能离开。

自动打包发布:从这个版本开始,所有集群的上线都会统一使用 QA 发布的包。这样减少了以前每次上线时,开发运维人员要花费大量的时间逐一去拉取各个集群的代码再进行编译、部署。一键的打包发布,可以在上线前就提前准备好各个集群的上线包,开发只需调用部署脚本去获取这些包,然后替换就可以完成上线。此外,自动打包发布的方式极大减少了运维上线时漏操作的风险。

自动化回归测试:以前一次回归测试,需要 QA 持续地投入超过 30 分钟。现在通过一键执行,程序会自动地执行,时间控制在 5 分钟以内。且 QA 可以将注意力放到其它事情上。

自动化完成稳定性测试结果的校验:从前执行完稳定性测试,需要对着数据库的一大片数据进行人肉地校验。会耗费一个下午大半天的时间,甚至还是有遗漏。通过脚本自动校验,1 分钟内就可以出结果报告。

这里再提一下 UI 自动化。很多人会对 UI 自动化有看法,觉得投入产出比不明显、维护成本高。我认为 UI 自动化跟接口自动化其实没有区别,都是功能回归的一种形式而已,选择哪种自动化的类型应该取决于项目的实际情况需要。另外,UI 自动化的维护成本目前一个季度做下来看,真的没有比接口自动化要高,关键还在于自动化的设计上是不是做的易于维护。

6. 展望

可以看到以上的自动化都是基于环境的稳定可用为前提的。之所以没有独立分配一套环境用于自动化测试,也是因为环境维护的成本较高。但是,基于测试人员的增加,测试类型的丰富(异常、性能),在一套环境上执行所有测试显然会出现相互影响的问题。因此,如果能将测试环境搭建 docker 化,通过维护 docker 镜像的方式,自动化地使用 docker 镜像快捷地部署一套新的完整测试环境可以极大地提高我们的测试效率。

最后的最后,发自真心地希望圈中的前辈大神能给予我一些参考意见和指导,指出我的不足之处。谢谢!

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 33 条回复 时间 点赞

写的挺不错的,尤其是最后:“将测试环境搭建 docker 化,通过维护 docker 镜像的方式,自动化地使用 docker 镜像快捷地部署一套新的完整测试环境可以极大地提高我们的测试效率”
正式我们团队正在做的事情

在设计自动化场景测试用例时,应该遵循哪些原则呢?

仔细的看了楼主的文章,很不错,想了解点,楼主在持续集成的时候,做的 静态代码检查,用的是 sonar 吗?做的是增量还是全量检测?

思路特别赞,我也要去学习实践一下。
之前写过自动化测试用例,也持续集成过,自动化用例良好的维护性确实特别重要,不然一旦换人接手项目,很快就会被搁置。要设计维护性很高的自动化测试代码,对测试人员自身的要求也挺高的!!

笑哼 回复

说下我自己的看法哈。场景化的用例,我觉得用例之间一定要解耦,不要相互依赖,每个用例应该是单独可重复执行的。我是非常不建议在用 testng 写用例时使用 groups 和 dependgroups 组织依赖的,万一执行过程中一个用例出错,会导致大片出错,对调试定位很不利。其次,还是那句老话,挖掘共性,将共性封装。其它的暂时也想不到那么多,如果你实践中碰到了问题我们可以再交流。

CC 回复

是的,是用 sonar。目前其实是全量的,但是我知道 sonar 支持增量的方式。另外说下,为什么静态代码检查没有好好去推,其实我觉得这个实践起来真的比较难。需要花心思去跟开发确定好规则,明白每条检查规则的必要性,确定哪些规则检查出来的问题必须要改。只有确定了这个,然后在项目中严格去执行,才能有成效。对于存在很多无效检查规则的规则集,扫描出来一些不是很准确的问题,然后笼统地推给开发改,开发肯定是不愿意去改的。另外我是建议楼主,将自己的测试代码用 sonar 检查一下,看看自己愿不愿改,体会下开发的心情。

Nisir #14 · 2017年08月11日 Author
小敏 回复

小敏是最棒的!

受启发了!谢谢楼主

Nisir #21 · 2017年08月12日 Author
thanksdanny 回复

客气了,互相探讨

恒温 将本帖设为了精华贴 08月12日 10:07

测试数据这一块能不能再详细的讲解一下,目前工作中,我觉得测试数据是很难解决的一块。你提到尽量不使用 dependOnMethod,但是在实际工作中,很难去实现,因为很多个接口都是基于业务场景的,B 接口一定要 A 接口先实现,如果说一定要强调不使用 dependOnMethod,那么很多接口都需要操作数据库去完成,有些业务涉及的表如果很多,操作起来就很麻烦。

Nisir #12 · 2017年08月12日 Author
alwans 回复

假设 B 接口一定要依赖于 A 接口先实现的话,我在实现 B 接口的测试类里,可以显示调用 A 接口,这样可以做到单独地去调试 B 用例,而不是一定要先执行完 A 用例再去执行 B 用例。关于数据库的操作,推荐一种思路哈,可以通过自定义注解的方式,在每个标记了注解的 test 执行前备份数据库,执行完后恢复数据库,有点像面向切面的 AOP 编程。关于数据准备,我这边的数据准备主要是指往 hdfs 写数据,可能跟你的场景不一定一样,你可以具体描述下你遇到的难点么?

Nisir #13 · 2017年08月12日 Author
alwans 回复

当然也不是说非不能用 dependOnMethod 不可,只要自己能合理地组织逻辑便于维护即可。 另外可以尝试另一种组织用例的方式,就是使用 priority。对于单线程执行的类,priority 可以指定用例执行的顺序。对一些清除环境脏数据的测试方法还可以加上 always=true 的注解。

非常有深入文章,点赞。
同时也想探讨下,自动化领域,大家各自公司的长期规划的画像是什么样子的。

测试流程、程序架构感觉都大同小异。
变与不变?面向接口,对逻辑可扩展。程序的稳定性涉及很多因素,光封装太臃肿。
为什么不用 MyBatis,结合 spring mvc/springboot 自动装配挺好用的。
实体到数据库映射工具可以生成。
mapper/dao 结合 sql 更灵活些。
自己写插件,拦截器,注入 statement。
可以扩展自己的 typehandler。
连接池、一二级缓存也挺好用的。

楼主总结的好赞~虽然不太懂 java,但是实现的方式和思路蛮赞的,我是用 python 作自动化的,有些思想很是可以借鉴~我仍然还是不懂怎么去持续构建,且用 docker 去维护测试环境似乎也不好进行,目前公司都是运维去维护的,很多东西测试都不能参与其中,不知道要做些啥改变,才可以更多的接触到更底层的东西呢?

Nisir #29 · 2017年08月15日 Author
卡农Lucas 回复

谢谢指点。确实我对 mybatis 跟 hibernate 的使用不是那么地深,mybatis 只是简单地写过几个 demo。之所以用 hibernate 也是图个方便,它跟以前我使用的 python 的 django 框架里的 ORM 比较接近。不过你说的 mybatis 的缓存等这些功能 hibernate 里也都是支持的。后续有机会接触用 spring boot 或 spring mvc 去开发平台的话,再深入学习一下

Ningxw 回复

个人建议哈,测试还是要多拓宽下自己的维度、视野,脱离岗位的束缚,这样才会更有竞争力。而且现在 DevOps 非常盛行,测试要做更多开发、运维的工作。你说的你们公司不能参与其中,这确实可能是一个比较大的限制问题。我们公司还是比较自由的,问开发要产品源码权限都是 ok 的,测试环境的搭建维护都是测试自己来负责。你可以从项目出发,先搞清楚整个项目的架构,各个组件关系之间的依赖,自己尝试动手去搭建一套新的环境。另外,自己可以深入产品源码去分析业务逻辑,测试过程中碰到问题,可以尝试自己通过业务日志、数据库信息、远程调试等方式自己去定位问题。

Nisir 回复

嗯,主要还是所处的平台太小,可施展的地方太少~你给的建议很棒,我会慢慢去实现的,争取变得优秀一点,去更好更大的平台见识更多和学习更多的东西~💪

Nisir #20 · 2017年08月18日 Author

主要是配置 sonar.timemachine.period 这个参数。通过 sonar 面板的 Administration->General Settings->Differential Views 可以查看到有相关参数的介绍

Nisir 回复

你好,在微信公众号 程序员技术前沿 找到了这篇文章,没有任何转载自此原文的声明。想问下此文章你是否有授权过给 程序员技术前沿 微信公众号发送吗?

Nisir #22 · 2017年08月23日 Author
陈恒捷 回复

没有授权过,我就在 TesterHome 发过此文。

好的自动化思想永远是适用的,只不过能做到的人真心不多,所以造成一个现象,大部分人做不起来 UI 自动化的理由都是维护成本太高,而不是认真分析问题。

在路上 回复

很精辟

自动化回归测试:以前一次回归测试,需要 QA 持续地投入超过 30 分钟。现在通过一键执行,程序会自动地执行,时间控制在 5 分钟以内

我想请教一下,5 分钟以内,那自动化回归的测试用例有多少,是只做 msg,code 基本的校验吗?如果网络导致的不稳定问题有考虑,比如某个接口,第一次跑没问题,第二次跑就有问题。

null 回复

用例量级在 100 以内吧,每个接口都是触发异步的任务执行,待任务执行结束后要校验任务的执行结果。用了并发执行,所以时间控制到了 5 分钟。网络问题可以增加失败重试。

@Nisir 好的,非常感谢您的解答

你好,请问下关于自动化用例数据管理这块有没有什么好的建议?比如有的是直接维护工程里面的 csv 文件或者 yaml 文件,但是这样时间长了好像需要维护的文件会越来越多,而且如果大家不按照用例管理规范来会变得一团糟,每个人都按照自己的理解来管理自动化用例,而且不知道直接维护在工程里面是否方便结果回填;有的是将用例数据落库,但是这样感觉首先不是很直观,而且我想维护一批用例可能至少还得有一个管理页面,感觉对于这部分自动化用例还得专门配一个用例管理系统吗,如果我想既能方便维护,同时又能方便测试结果回填,有没有什么好的建议或者最佳实践

Nisir #29 · 2020年02月03日 Author
Kyle 回复

你说的就是很多实际场景中经常出现的情况,所以到最后自动化的维护成本会越来越高。建议是通过平台化的方式,用测试用例管理系统去维护

是的,楼主这篇文章写的真心不错,我现在有很多测试思想,通过不断的实践和适应,和楼主表述的基本是一致的。UI 自动化测试确实很考验框架的设计,框架设计的不好,维护成本很高。自动化测试我的理解,更趋于一种提高效率的工具,抛开公司的痛点和需求,一味地为了自动化测试而自动化测试的效果一定适得其反。受益自动化测试的公司应该不算很多,工具的使用,便捷和易于维护是基本准则,一旦把过高的要求放置于其上,复杂度是指数增长的,得不偿失。

想请教楼主一个问题,System.getenv("TEST_ENV") 配置测试环境属性,是手动把环境变量配置到测试服务器上吗?如果同一台服务器既需要测试 A 项目,又需要测试 B 项目,怎么解决呢?可以加楼主联系方式吗?有很多东西想探讨。

Nisir #32 · 2020年05月27日 Author
mengzongzhu 回复

maven 执行的时候,或者通过其它插件如 jenkins 执行的时候,都可以在进程中传入环境变量参数的。我的微信 wz63650406

大佬,我来挖坟贴了,,我这边的项目就是大数据基础设施,任务调度平台,看你的代码,感觉也是任务调度的东西,关于这块,只要是任务,他在 yarn 上面跑,我不知道他是什么时候才返回正确的结果的,比如各种 status,像这种异步的接口,有什么好的断言方式吗,只能采用轮训?还有,那些 task 之间的周期依赖,顺序依赖你也是做的自动化测试吗

兔子🐰 自动化测试合辑 中提及了此贴 02月18日 12:30
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册