这次的主题跟上篇《自动化测试介绍》一样,主要给大家介绍一些经验,最后简单介绍下如何开发一个测试小工具。

浅谈测试的行业发展

在互联网兴起之前,测试这个岗位很单纯,基本都是手工测试为主,少量需要编写测试脚本辅助测试,那个时候软件架构也很简单,对于测试人员的要求并不高。

互联网公司兴起后,测试随着整个软件行业一起蓬勃发展,单一个测试角色就衍生出了很多不同岗位,如性能测试,自动化测试,安全测试等等,在一些大公司的测试部门,这些基本都是专门设置的岗位,职责分工很明确。

后来移动互联网占据主流,测试行业又有了一些新变化,比如:


  • 增量方面,进入行业的门槛越来越高。存量方面,进入洗牌阶段,跟不上发展的逐渐被淘汰;
  • 软件测试这个称呼叫的人越来越少,逐渐被 QA 所取代,称呼的转变也代表了行业对测试的期望有所改变,不仅仅是软件测试,更是质量保障;
  • 性能、自动化、安全等等这些测试专岗开始消失,被测试开发取代;

恩,测试行业越来越难混了。

如今的测试行业,可以简单分为两个职业发展方向,业务专家和测试开发。业务专家不仅要精通软件测试专业技能,还要熟练掌握项目管理,敏捷和流程改进等等方面的知识。测试开发,就是从那几个逐渐淘汰的专岗发展而来,当然还不止这些,目前大部分公司给测试开发的定义是负责测试工具和平台,以及其它测试技术的创新和研发,最终目标是为了提升测试效率。

像自动化测试,性能测试等也都可以提升测试效率,这两个之前讲过一些,今天主要讲一下如何开发一个测试工具,来提升效率。

然后给大家说下我本人的真实经历,我回想了下,发现跟整个行业的趋势非常吻合。

以我自身举例

我的职业发展路线基本如下:

功能测试(2013 年)

性能测试(2014 年)

自动化测试(2014 年~2015 年)

测试开发(2016~现在)

性能测试阶段

我在 2013 年进入软件测试行业,入行前两年,沉迷于 LoadRunner 和 QTP 无法自拔,把两个工具学的非常溜。当时 51Testing 是人气最高的测试论坛,论坛上超过 50% 的技术帖子都是关于 LoadRunner 和 QTP,可见这俩工具当时有多火。当时觉得会了这俩工具,测试技术也已经 666 了。

但是后来有件事对我打击非常大,我们公司做了一个企业云盘产品,准备采购一批硬件服务器跟云盘产品捆绑一起卖,为了对外宣传,需要验证出每台服务器的性能瓶颈,然后技术部就安排了我负责对硬件服务器做压测。然后我就用自己拿手的 LoadRunner 开始搞起来,录制、调试脚本、压测、采集性能指标、出报告,一顿操作猛如虎。最后把报告发给了服务器厂商。

对方拿到报告以后,表示想复测一遍,让我提供压测脚本和数据给他们,而且不要录制的脚本,因为录制脚本扩展性和兼容性太差,必须要手写的 Java Vuser。这就搞了我一个措手不及,我当时 Java 水平菜得很,好不容易 Google、Baidu 胡乱搜了一遍,勉勉强强写出来了。发给对面之后,对面又表示还是他们自己写吧。

最后他们的性能测试结果,虽然跟我的结果基本一致,但我还是非常尴尬,认识到只玩工具是没有前途的。然后痛定思痛,开始恶补 Java 编程。

自动化测试阶段

闷头学习了一年 Java 之后,我的性能测试尽量都会用 Java 去编写脚本,但是写的机会还是很少,后来趁着一个机会从性能测试转型到了自动化测试。

领导把我招进来之后跟我说,业务测试要正常做,自动化测试先抽空研究,有头绪了再开展。当时因为担心自己转型失败,就非常慌,忙完业务之后天天啃资料,找文档学习。

从编写第 1 个 case 开始,一直到编写到第 100 个 case 左右,发现编写效率越来越低,然后开始开发了我真正意义上第一个测试工具,就是下面图里的自动化测试框架,框架第一个版本只有 driver 管理、工具类、断言、日志、报告等,后来新增了 IO 操作、数据创建和清理、网络请求处理、数据库操作、重试、截图等等。

写完这个框架后,领导专门成立了自动化测试小组,又招来三个人跟我一块搞。因为产品包括 Android、iOS、H5 和 Web 四个端,我们四个正好每人负责一个端。后来又出现一个协作问题,我们四个每个人都会修改框架部分,但是四个端是完全不同的四个代码库,完全隔离的,没有办法同步更改。后来引入了 maven 依赖包管理才解决了这个问题,就是把框架跟测试用例完全分离,做成了一个通用 SDK,然后在业务测试用例的 maven 工程引入一个依赖。

测试框架的结构如下

如何开发一个测试小工具_测试工具

用例库结构

如何开发一个测试小工具_测试工具_02

用例库在 maven 配置引入自动化测试框架的依赖

如何开发一个测试小工具_测试工具_03

做完这个框架之后,又继续做了接口自动化测试,并搭建了持续集成体系。

测试开发阶段

测试开发成长阶段

根据我这些年的总结和观察,大部分测试开发人员,成长路线大概分为三个阶段


  1. 初学阶段 从 0 到 1,属于技术学习阶段,更多是锻炼自己的技术能力,此阶段大多会聚焦一个领域持续学习,比如自动化测试,从写用例到写框架,到持续集成等等。我的自动化测试阶段就对应了初学阶段。
  2. 盲干阶段 从 1 到 N,属于扩充知识边界的阶段,看见什么火写什么,喜欢重复造轮子,不考虑成本和收益,以及能否落地,也不考虑是否能解决实际问题。此阶段技术成长非常快,但容易陷入技术面虽广但研究不深的尴尬境地。
  3. 理智阶段 开始追求投入产出比,以解决业务和技术难题为出发点。

这三个阶段循序渐进,前两个阶段决定了成长的高度,而且这两个阶段产出更高,但收益却可能并不理想。进入了最后一个阶段,则具备了成长为负责人的必备条件。

善于发现问题,挖掘业务痛点,这一点非常重要,是成为测试开发的基本条件。因为大多数时候没有人给你提需求,需要主动出击。

重要提醒:开发测试工具只是测试开发人员的一项工作,并不是全部。因为今天的主题是开发测试工具,所以只介绍工具开发部分的经验。

客户端工具篇

我转型成测试开发,也是一个很偶然的事件。在做自动化测试时,为了方便自动化测试执行,需要预先准备大量的测试数据,所以封装了很多造数据的测试脚本,供自动化测试调用。有一天突然就想到,我们做自动化测试有造测试数据这个痛点,那做业务测试的同事,他们肯定也会碰到类似问题。所以就考虑如何做成一个小工具供他们使用。

翻了下代码,大约是在 2014 年~2015 年左右写的下面这个工具,试了下还能跑起来,不过背景色出了点问题。

如何开发一个测试小工具_测试数据_04

把这个工具给业务测试同事之后,没想到非常受欢迎,比我们辛辛苦苦写的自动化测试受欢迎的多。后面随着这个工具的功能越来越多,因为客户端工具交付非常不方便,每次都得编译打包,然后发给他们才能使用。又学习了 Java Web 相关的开发知识,把这个工具还有自动化测试都迁移到了得到 Web 上。

在后面还做了一些客户端形式的测试小工具,比如下面这个。做这个的背景是,业务测试和一些客户端研发经常找我们自动化测试小组跑稳定性测试,后来就基于 Android Monkey 开发了一些比较好用的功能,但是它一开始命令行形式的,而且在执行过程中看不到实时的性能和日志,只能在最后会生成一份报告。给了他们使用之后反响不太好。吐槽使用不方便,命令行输入参数也比较麻烦,每次还是找我们小组执行。然后收集了一波意见和需求后,最终做成了下面这个图形化界面的测试小工具。收获了一大波好评,也解放了我们小组在这块长期占用的人力。

如何开发一个测试小工具_测试数据_05

以上的 GUI 使用 Java Swing 开发

图表使用 JFreeChart 开发

Web 工具篇

我是在发奋图强学 Java 的那一年,接触到的 Web 开发,本来一开始只打算学到 Java 多线程那里,没想到没刹住车,一直学到了 SSH 框架部分。但刚学完以后,Web 开发技术并没有用到,逐渐也忘的差不多了。

为了把难以交付的客户端工具,迁移到 Web 平台上,又重新复习了一遍 Java SSH 框架,然后动手写了第一个 Web 平台。这个平台包括用户管理、生成测试数据、自动化测试执行和查看报告等功能。当时完全不知道网上有很多很酷的开源方案,所有功能逻辑都是自己一行行代码写出来的,包括用美图秀秀画了很多图当 icon,以至于非常丑,写这篇文章的时候找了下代码,不过实在没找到,可能是后来觉得难看给删掉了。。

下面这个是我写的第二个 Web 平台,大概是在 2016 年上半年左右。这个时间点对于我来说是一个分水岭,之前更多是性能测试、自动化测试的工作。自动化测试做了两年以后,我的编码水平和技术能力相比两年前有了比较大的进步,这时候就不再限制自己只搞自动化测试,开始尝试写一些其它的测试工具和平台。

这个平台最初的定位是接口测试管理平台,为了替代我们当时正在使用的 Jmeter,因为 Jmeter 在多人协作场景比较差,所以自研平台准备换掉它。开发完接口测试管理平台后,这时候就进入了盲干阶段,又开发了 N 多个功能,实际大多数都没有用到,不过非常锻炼技术能力。

如何开发一个测试小工具_自动化测试_06

如何开发一个测试小工具_自动化测试_07

如何开发一个测试小工具_自动化测试_08

如何开发一个测试小工具_测试工具_09

前端使用 Ace Admin 开源模板开发

后端使用 Java SSH 框架

在这个时间点之前,我的技术栈都是 Java 相关,在 2016 年底来到咱们公司之后,逐渐换到了 Python。当时有两个原因:


  1. 陆续招进来的测试人员基本都是以 Python 为主,为了适应大家;
  2. Python 的开发效率更高,效率对于测试工具非常重要。像高并发、高可用、高性能等等,是属于可忽略的一部分。

开发一个测试工具

最后给新同学演示下如何开发一个小测试工具,使用咱们的开发框架来介绍。

开发一个对外服务的 API 测试工具

我们今天基于这个测试服务,开发一个可以模拟生成测试数据,并对外提供服务的 API 测试工具。

先来看下目录结构,红框中是我创建的一个示例模块

代码库目录结构如下

如何开发一个测试小工具_测试工具_10

只需要新增 2 个代码文件,修改 1 个代码文件即可

业务逻辑开发

新增 example/services/testdata.py

此部分是业务代码,这里使用 faker 库生成测试数据

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Author : JUNE
@Time : 2021-06-02 16:23
@describe:
"""
from faker import Fakerimport json
class TestData(object):def __init__(self):self.fake = Faker(locale='zh_CN')
def get_address(self):"""
模拟生成地址
:return:
"""return self.fake.address()
def get_job(self):"""
模拟生成工作
:return:
"""return self.fake.job()
def get_free_email(self):"""
模拟生成邮箱
:return:
"""return self.fake.free_email()
def get_phone_number(self):"""
模拟生成手机号
:return:
"""return self.fake.phone_number()
def get_name_male(self):"""
模拟生成姓名
:return:
"""return self.fake.name_male()

新增 example/exampleviews.py

此部分是路由文件,声明对外暴露的 API

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""

@Author : JUNE
@Time : 2021-06-02 16:21
@describe:

"""
from flask import make_response, jsonify, Blueprint, abort
import json
from flask import request
from flask import Flask

from .. lib.logger import logger
from .. lib.tojson import AlchemyEncoder
from .services.testdata import TestData


example = Blueprint('example', __name__, url_prefix='/example')
app = Flask(__name__, instance_relative_config=True)


@example.route('/getaddress', methods=['POST', 'GET'])
def get_address():
try:
params = request.get_data()
logger.info(params.decode('utf-8'))
info = TestData()
results = info.get_address()
response = make_response(json.dumps({'code': 0, 'data': results}))
return response
except Exception as e:
logger.error(e)
logger.error('发生了错误,error: {}'.format(e))
abort(500)


@example.route('/getjob', methods=['POST', 'GET'])
def get_job():
try:
params = request.get_data()
logger.info(params.decode('utf-8'))
info = TestData()
results = info.get_job()
response = make_response(json.dumps({'code': 0, 'data': results}))
return response
except Exception as e:
logger.error(e)
logger.error('发生了错误,error: {}'.format(e))
abort(500)


@example.route('/getphonenumber', methods=['POST', 'GET'])
def get_phone_number():
try:
params = request.get_data()
logger.info(params.decode('utf-8'))
info = TestData()
results = info.get_phone_number()
response = make_response(json.dumps({'code': 0, 'data': results}, cls=AlchemyEncoder))
return response
except Exception as e:
logger.error(e)
logger.error('发生了错误,error: {}'.format(e))
abort(500)


@example.route('/getnamemale', methods=['POST', 'GET'])
def get_name_male():
try:
params = request.get_data()
logger.info(params.decode('utf-8'))
info = TestData()
results = info.get_name_male()
response = make_response(json.dumps({'code': 0, 'data': results}, cls=AlchemyEncoder))
return response
except Exception as e:
logger.error(e)
logger.error('发生了错误,error: {}'.format(e))
abort(500)


@example.route('/getprofile', methods=['POST', 'GET'])
def get_profile():
try:
params = request.get_data()
logger.info(params.decode('utf-8'))
info = TestData()
results = info.get_profile()
response = make_response(json.dumps({'code': 0, 'data': results}, cls=AlchemyEncoder))
return response
except Exception as e:
logger.error(e)
logger.error('发生了错误,error: {}'.format(e))
abort(500)

main 函数注册路由

测试服务的主函数文件,需要在此注册新模块的路由,新增两行代码

from .example import exampleviews
app.register_blueprint(exampleviews.demo)

python run.py server 启动服务

通过 HTTP 请求发起访问,获得模拟的测试数据

如何开发一个测试小工具_测试数据_11

开发一个 Web 可视化的测试工具

上面已经完成了 API 的开发,接下来咱们开发一个简单的 Web 页面,把 API 返回的测试数据显示到页面上,方便数据的查看。这里使用 iview-admin 提供的项目模板。

Github: https://github.com/iview/iview-admin

# clone the project
git clone https://github.com/iview/iview-admin.git

// 安装依赖
npm install

// 启动 develop
npm run dev

页面开发

在 src/view/下新增 get-data.vue 文件,这个就是测试数据显示的页面。

代码文件分两部分

template:页面布局

script:数据处理

<template>
<div>
<Card shadow title="测试数据">
<row :gutter="32">
<i-col span="24">
<row type="flex" justify="center">
<p>姓名:{{ this.formBody.name }}</p>
</row>
<row type="flex" justify="center">
<p>手机:{{ this.formBody.phone }}</p>
</row>
<row type="flex" justify="center">

<p>邮箱:{{ this.formBody.mail }}</p>
</row>
<row type="flex" justify="center">
<p>工作:{{ this.formBody.job }}</p>
</row>
<row type="flex" justify="center">
<p>住址:{{ this.formBody.address }}</p>
</row>
</i-col>
<i-col span="24">
<row type="flex" justify="center">
<Button size="large" type="primary" @user12="getData">
刷新
</Button>
</row>
</i-col>
</row>
</Card>
</div>
</template>

<script>
import api from '../api/get-data/get-data'

export default {
name: 'get_data',
data () {
return {
formBody: {}
}
},
mounted () {
this.$Loading.start()
this.getData()
this.$Loading.finish()
},
methods: {
getData () {
api.getprofile('', (body) => {
this.formBody = body.data
}, (error) => {
if (error.response.status === 401) {
this.$router.push({
name: '401'
})
} else if (error.response.status === 403) {
this.$router.push({
name: '401'
})
} else if (error.response.status === 404) {
this.$router.push({
name: '404'
})
} else {
this.$Modal.error({
title: '出现了错误',
content: '数据获取失败',
okText: '确定'
})
}
})
}
}
}
</script>

在 src/api 下新增 base.js 文件,这个文件是前端发起网络请求的基类

import axios from 'axios'

let func = {}
const baseUrl = process.env.NODE_ENV === 'development' ? 'http://192.168.137.46:5000' : '线上域名'

func.base_url = axios.create({
baseURL: baseUrl,
timeout: 10000,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
withCredentials: true
})

func.message = function (error) {
if (error.response === undefined) {
console.error(error)
return
}

}

export default func

在 src/api 下新增 get-data.js 文件,这个文件定义了跟后端交互的具体 API 请求

import client from './base.js'
import base from './base'

const URLs = {
getprofile: '/example/getprofile'
}

let api = {}

api.getprofile = function (params, success, error) {
base.base_url.get(URLs.getprofile, params).then((response) => {
if (success !== undefined) {
success(response.data)
}
}).catch((e) => {
if (error !== undefined) {
error(e)
} else {
client.message(e)
}
})
}

export default api

配置页面路由

在 src/router/router.js 文件配置功能菜单和页面路由

下面代码可以放在任意位置

{
path: '/data',
name: 'data',
component: Main,
meta: {
hideInBread: true
},
children: [
{
path: 'get_data',
name: 'get_data',
meta: {
icon: '_qq',
title: '测试数据'
},
component: () => import('@/view/get-data.vue')
}
]
},

在 src/locale/lang/zh-CN.js 文件配置本地化数据

get_data: '测试数据',

最终展示效果如下

刷新页面,可以获取新的测试数据

页面有点简陋,iview 还有一套强大的 UI 组件库可以使用,地址:https://www.iviewui.com/docs/guide/install

如何开发一个测试小工具_测试工具_12

这样我们就完成了一个生成测试数据小工具的开发,基于 Web 平台,可以很方便的分享给其他人使用。