前言

博客里需要添加很多图片作为内容的补充,但是把图片放在本地博客文件夹里,上传到网上后,加载这些图片就是一个很大的问题,他们会拖累网页加载的速度,所以建议把图片放图床里,通过外链来访问和加载这些图片。

云对象储存服务商

国内免费的云对象储存的服务商(网易云、七牛云、腾讯云)

1.网易云

官网:https://www.163yun.com/

网易NOS(Netease Object Storage)网易对象存储为你提供基于互联网的数据存取服务,通过使用 NOS,你可以随时通过网络将你的文本、图片、音视频等各类文件存储到 NOS 系统中,并随时可以通过网络进行安全访问。

NOS 对象存储从三个维度进行计量收费:存储容量、流量、接口调用次数。存储容量0-50 GB是免费,下载流量在0-20 GB免费,每月前 10 万次 Put 请求免费,每月前 100 万次 Get 请求免费。

2.七牛云

官网:https://www.qiniu.com/

七牛云存储提供云存储、云处理、云加速分发一站式服务,注册成为标准用户后可获得10GB免费存储空间、每月10GB下载流量、每月10万次Put请求、每月100万次Get请求。

缺点:
临时域名仅有三个月,三个月后没有自己的备案域名,所有图片均会失效

3.腾讯云

官网:https://cloud.tencent.com/

腾讯云对象存储服务COS,全称为Cloud Object Service,主要是为开发者提供安全、稳定、高效、实惠的对象存储服务,开发者可以将任意动态、静态生成的数据,存放到COS上,再通过HTTP的方式进行访问。

缺点:
自2019年后新建用户可以领取一个6月有效期,50G的存储量。6月过后,按照存储量和流量收费。

在线图床选择

https://sm.ms SM图床

https://tu.aixinxi.net/ 爱信息图床

https://imgchr.com/ 路过图床

总结

云储存选择网易云,因为网易云对新用户的限制,免费的云储存有待选择

图床选择路过图床

目前还在用路过图床,缺点是图片链接不能看到任何图片名称等图片信息,不方便插入在博客里

hexo-tag-cloud插件介绍

hexo-tag-cloud插件是作者写的一个Hexo博客的标签云插件,旨在直观的展示标签的种类,美观大方且非常优雅。

安装插件

进入到 hexo 的根目录,然后在 package.json 中添加依赖: "hexo-tag-cloud": "2.1.*"

使用命令行进行安装

npm install

Git clone 下载

使用命令行安装插件包的过程中可能会出现问题,安装失败,安装不完全。可以直接克隆插件到博客的插件文件夹blog/node_modules里。或者克隆到桌面,复制到博客的插件文件夹blog/node_modules里。

1
git clone https://github.com/MikeCoder/hexo-tag-cloud

配置插件

插件的配置需要对应的环境,可以在主题文件夹里找一下,有没有对应的渲染文件,然后根据渲染文件的类型,选择对应的插件配置方法。

##swig 用户 (Next主题为例)
在主题文件夹找到文件 theme/next/layout/_macro/sidebar.swig, 然后添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
{% if site.tags.length > 1 %}
<script type="text/javascript" charset="utf-8" src="/js/tagcloud.js"></script>
<script type="text/javascript" charset="utf-8" src="/js/tagcanvas.js"></script>
<div class="widget-wrap">
<h3 class="widget-title">Tag Cloud</h3>
<div id="myCanvasContainer" class="widget tagcloud">
<canvas width="250" height="250" id="resCanvas" style="width=100%">
{{ list_tags() }}
</canvas>
</div>
</div>
{% endif %}

代码添加到后面即可,添加示意图如下:

对于ejs的用户 (默认主题landscape为例)

在主题文件夹找到文件hexo/themes/landscape/layout/_widget/tagcloud.ejs,将这个文件修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
<% if (site.tags.length) { %>
<script type="text/javascript" charset="utf-8" src="<%- url_for('/js/tagcloud.js') %>"></script>
<script type="text/javascript" charset="utf-8" src="<%- url_for('/js/tagcanvas.js') %>"></script>
<div class="widget-wrap">
<h3 class="widget-title"><%= __('tagcloud') %></h3>
<div id="myCanvasContainer" class="widget tagcloud">
<canvas width="250" height="250" id="resCanvas" style="width=100%">
<%- tagcloud() %>
</canvas>
</div>
</div>
<% } %>

对于jade用户 (Apollo主题为例)

找到 apollo/layout/archive.jade 文件,并且把 container 代码块修改为如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
block container
include mixins/post
.archive
h2(class='archive-year')= 'Tag Cloud'
script(type='text/javascript', charset='utf-8', src=url_for("/js/tagcloud.js"))
script(type='text/javascript', charset='utf-8', src=url_for("/js/tagcanvas.js"))

#myCanvasContainer.widget.tagcloud(align='center')
canvas#resCanvas(width='500', height='500', style='width=100%')
!=tagcloud()
!=tagcloud()
+postList()

主题配置

在博客根目录,找到 _config.yml配置文件然后在最后添加如下的配置项,可以自定义标签云的字体和颜色,还有突出高亮:

1
2
3
4
5
6
7
# hexo-tag-cloud
tag_cloud:
textFont: Trebuchet MS, Helvetica
textColor: '#333'
textHeight: 25
outlineColor: '#E2E1D1'
maxSpeed: 0.1

效果预览

本地预览
hexo clean && hexo g && hexo s
推荐使用 && 作为组合命令的串联符号

构建
hexo clean && hexo g && hexo d

参考文章:Hexo Tag Cloud

思路和框架

正常的接口测试流程
确定测试接口的工具 —> 配置需要的接口参数 —> 进行测试 —> 检查测试结果(有的需要数据库辅助) —> 生成测试报告(html报告)
根据这样的过程来一步步搭建我们的框架。在这个过程中,我们需要做到业务和数据的分离,这样才能灵活,达到我们写框架的目的。
接下来,我们来进行结构的划分。

-w296

  • common:存放一些共通的方法
  • result:执行过程中生成的文件夹,里面存放每次测试的结果
  • testCase:用于存放具体的测试case
  • gitignore:Git忽略提交规则(不是必要)
  • testFile:存放测试过程中用到的文件,包括上传的文件,测试用例以及数据库的sql语句
  • caselist:txt文件,配置每次执行的case名称
  • config:配置一些常量,例如数据库的相关信息,接口的相关信息等
  • readConfig: 用于读取config配置文件中的内容
  • runAll:用于执行case
  • token.txt:本地换持有token

config

配置一些常量,例如数据库的相关信息,接口的相关信息等
config.ini内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[EMAIL]
mail_host = smtp.qq.com
mail_user = xxx@qq.com
mail_pass = xxx
mail_port = 465
mail_replyto = xxx@ruguoapp.com
sender = xxx@qq.com
receiver = xxx@qq.com
subject = "测试报告"
content = "今天测试一下python是否可以发送邮件成功。"
user = xx
on_off = on

[HTTP]
scheme =https
base_url =xxx
timeout = 10.0

[DATABASE]
host = localhost
username = root
password = root
port = 3306
database = test

readConfig.py

用来读取config配置文件中的内容,定义的方法,根据名称取对应的值
内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import os
import codecs
import configparser

ProDir = os.path.split(os.path.realpath(__file__))[0] # 该文件的绝对路径
configPath = os.path.join(ProDir, "config.ini")


class ReadConfig:
def __init__(self):
fd = open(configPath)
data = fd.read()

# remove BOM
if data[:3] == codecs.BOM_UTF8:
data = data[3:]
file = codecs.open(configPath, "w")
file.write(data)
file.close()
fd.close()

self.cf = configparser.ConfigParser()
self.cf.read(configPath)

def get_email(self, name):
value = self.cf.get("EMAIL", name)
return value

def get_http(self, name):
value = self.cf.get("HTTP", name)
return value

def get_db(self, name):
value = self.cf.get("DATABASE", name)
return value

if __name__ == '__main__': # 测试一下,我们读取配置文件的方法是否可用
print('HTTP中的base_url值为:', ReadConfig().get_http('base_url'))

common

存放一些共通的方法
-w235
conmmontest:操作本地case
configEmail:发送邮件
configHttp:配置接口文件
getHeades:获取请求头
HTMLTestRunner:测试报告
Token:token
Log:输出的日志的所有操作,主要是对输出格式的规定,输出等级的定义以及其他一些输出的定义等等
url:接口列表

Log

对于这个log文件呢,我给它单独启用了一个线程,这样在整个运行过程中,我们在写log的时候也会比较方便,看名字大家也知道了,这里就是我们对输出的日志的所有操作了,主要是对输出格式的规定,输出等级的定义以及其他一些输出的定义等等。总之,你想对log做的任何事情,都可以放到这里来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import os
import logging
import threading

localReadConfig = readConfig.ReadConfig()

class Log:
logging.basicConfig()

def __init__(self):
global logPath, resultPath, path, logger_name
path = readConfig.ProDir
resultPath = os.path.join(path, "result")
logger_name = 'logs'

if not os.path.exists(resultPath): # 判断该文件是否存在
os.mkdir(resultPath) # 创建目录

logPath = os.path.join(resultPath, str(datetime.now().strftime("%Y%m%d%H%M%S")))
if not os.path.exists(logPath):
os.mkdir(logPath)

self.logger = logging.getLogger(logger_name) # 获得一个logger对象,默认是root
self.logger.setLevel(logging.INFO) # 设定INFO级别,所有等级大于等于INFO的信息都会输出

# 定义handler
handler = logging.FileHandler(os.path.join(logPath, "output.log")) # 向文件output.log输出日志信息
# 定义格式
formatter = logging.Formatter(
'[%(asctime)s] <%(levelname)s> %(message)s') # 定义日志输出格式('%(asctime)s] %(levelname)s [%(funcName)s: %(filename)s, %(lineno)d] %(message)s')
handler.setFormatter(formatter) # 选择一个格式

self.logger.addHandler(handler) # 增加指定的handler

现在,我们创建了上面的Log类,在__init__初始化方法中,我们进行了log的相关初始化操作。具体的操作内容,注释已经写得很清楚了,这样,log的基本格式已经定义完成了,至于其他的方法,就靠大家自己发挥了,毕竟每个人的需求也不同,我们就只写普遍的共用方法啦。接下来,就是把它放进一个线程内了,请看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyLog:
log = None
mutex = threading.Lock() # 多线程

def __init__(self):
pass

@staticmethod
def get_log():
if MyLog.log is None:
MyLog.mutex.acquire() # 获取互斥锁后,进程只能在释放锁后下个进程才能进来
MyLog.log = Log()
MyLog.mutex.release() # 互斥锁必须被释放掉

return MyLog.log

configHttp.py

配置接口文件,主要内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import requests
import readConfig as readConfig
from common import getHeaders
from common.Log import MyLog as Log


localReadConfig = readConfig.ReadConfig()


class ConfigHttp:

def __init__(self):
global scheme, host, timeout
scheme = localReadConfig.get_http("scheme")
host = localReadConfig.get_http("base_url")
timeout = localReadConfig.get_http("timeout")
self.log = Log.get_log()
self.logger = self.log.get_logger()
self.params = {}
self.headers = None
self.data = {}
self.url = None
self.files = None

def set_url(self, url):
self.url = scheme +'://'+host+url

def set_headers(self):
"""
set headers
"""
self.headers = getHeaders.get_headers()

def set_local_headers(self):
"""
set local headers
"""
self.headers = getHeaders.local_headers()

def set_params(self, param):
"""
set params
:param param:
:return:
"""
self.params = param

def set_data(self, data):
"""
set data
:param data:
:return:
"""
self.data = data

def get(self):
"""
defined get method
:return:
"""
try:
response = requests.get(self.url, headers=self.headers, params=self.params, data=self.data)
if response.status_code == 200:
return response
elif response.status_code == 401:
getHeaders.refresh_tokens()
response = requests.get(self.url, headers=self.headers, params=self.params, data=self.data)
return response
else:
return response
except TimeoutError:
self.logger.error("Time out!")
return None

def post(self):
"""
defined post method
:return:
"""
try:
response = requests.post(self.url, headers=self.headers, params=self.params, data=self.data)
if response.status_code == 200:
return response
elif response.status_code == 401:
getHeaders.refresh_tokens()
response = requests.post(self.url, headers=self.headers, params=self.params, data=self.data)
return response
else:
return response
except TimeoutError:
self.logger.error("Time out!")
return None

def post_with_file(self):
"""
defined post method
:return:
"""
try:
response = requests.post(self.url, headers=self.headers, data=self.data, files=self.files, timeout=float(timeout))
return response
except TimeoutError:
self.logger.error("Time out!")
return None

def post_with_json(self):
"""
defined post method
:return:
"""
try:
response = requests.post(self.url, headers=self.headers, json=self.data, timeout=float(timeout))
return response
except TimeoutError:
self.logger.error("Time out!")
return None

用python自带的requests来进行接口测试,,拿get和post两个方法来说吧。(平时用的最多的就是这两个方法了,其他方法,大家可以仿照着自行扩展)

  • get方法
      接口测试中见到最多的就是get方法和post方法,其中,getget方法在传递参数后,url的格式是这样的:http://接口地址?key1=value1&key2=value2,
    

对于requests提供的get方法,有几个常用的参数:
url:显而易见,就是接口的地址url啦
headers:定制请求头(headers),例如:content-type = application/x-www-form-urlencoded
params:用于传递测试接口所要用的参数,这里我们用python中的字典形式(key:value)进行参数的传递。
timeout:设置接口连接的最大时间(超过该时间会抛出超时错误)

举个栗子:

1
2
3
4
5
url=‘http://api.shein.com/v2/member/logout’
header={‘content-type’: application/x-www-form-urlencoded}
param={‘user_id’: 123456,‘email’: 123456@163.com}
timeout=0.5
requests.get(url, headers=header, params=param, timeout=timeout)
  • post方法
    与get方法类似,只要设置好对应的参数,post方法中的参数,我们不在使用params进行传递,而是改用data进行传递了。

常用的返回值的操作。
text:获取接口返回值的文本格式
json():获取接口返回值的json()格式
status_code:返回状态码(成功为:200)
headers:返回完整的请求头信息(headers[‘name’]:返回指定的headers内容)
encoding:返回字符编码格式
url:返回接口的完整url地址

举个栗子:

1
2
3
4
5
url=‘http://api.shein.com/v2/member/login’
header={‘content-type’: application/x-www-form-urlencoded}
data={‘email’: 123456@163.com,‘password’: 123456}
timeout=0.5
requests.post(url, headers=header, data=data, timeout=timeout)

以上这些,就是常用的方法啦,大家可自行取之。

关于失败请求抛出异常,我们可以使用“raise_for_status()”来完成,那么,当我们的请求发生错误时,就会抛出异常。在这里提醒下各位朋友,如果你的接口,在地址不正确的时候,会有相应的错误提示(有时也需要进行测试),这时,千万不能使用这个方法来抛出错误,因为python自己在链接接口时就已经把错误抛出,那么,后面你将无法测试期望的内容。而且程序会直接在这里当掉,以错误来计。

common.py

内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import readConfig
import os
from xlrd import open_workbook
from common import configHttp
from common.Log import MyLog as Log
import json

localReadConfig = readConfig.ReadConfig()
testFilePath = os.path.join(readConfig.ProDir,'testFile')
localConfigHttp = configHttp.ConfigHttp()
log = Log.get_log()
logger = log.get_logger()

caseNo = 0


def get_value_from_return_json(response_json, name1, name2):
"""
get value by key
:param response_json:
:param name1:
:param name2:
:return:
"""
group = response_json[name1]
value = group[name2]
return value

def get_value_dict_keys(dict):
"""
:param dict:
:return:
"""
list = [i for i in dict]
return list


def show_return_msg(response):
"""
show msg detail
:param response:
:return:
"""
url = response.url
msg = response.text
code=str(response.status_code)
print("请求地址:"+url)
print("请求结果:"+code)
print("请求返回值:"+'\n'+json.dumps(json.loads(msg), ensure_ascii=False, sort_keys=True, indent=4))
# print('Response HTTP Response Body:', json.dumps(self.response.json(), indent=2, sort_keys=True, ensure_ascii=False))

# indent: 缩进空格数,indent = 0输出为一行
# sort_keys = True: 将json结果的key按ascii码排序
# ensure_ascii = False: 不确保ascii码,如果返回格式为utf - 8包含中文,不转化为\u...
# ****************************** read testCase excel ********************************


# 从excel文件中读取测试用例
def get_xls_case(xls_name, sheet_name):
"""
get interface data from xls file
:return:
"""
cls = []
# 获取用例文件路径
xls_path = os.path.join(testFilePath, 'case', xls_name)
# 打开用例Excel
file = open_workbook(xls_path)
# 获得打开Excel的sheet
sheet = file.sheet_by_name(sheet_name)
# 获取这个sheet内容行数
rows = sheet.nrows
for i in range(rows):#根据行数做循环
if sheet.row_values(i)[0] != u'case_name':#如果这个Excel的这个sheet的第i行的第一列不等于case_name那么我们把这行的数据添加到cls[]
cls.append(sheet.row_values(i))
return cls

上面就是common主要内容。主要利用xlrd来操作excel文件,注意啦,我们是用excel文件来管理测试用例的。

configEmail.py

发送邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# coding:utf-8
import os
import smtplib
from email.mime.text import MIMEText
from email.header import Header

from email.mime.application import MIMEApplication #主要类型的MIME消息对象应用
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
from datetime import datetime
import threading
import readConfig as readConfig
from common.Log import MyLog

localReadConfig = readConfig.ReadConfig()


class Email:
def __init__(self):
global host, user, password, port, sender, title,receivers

sender = localReadConfig.get_email("sender") # 发件人
receivers = ['8463299@qq.com'] # 收件人
# receivers = ['test@163.com','test@vip.qq.com'] # 接收多个邮件,可设置为你的QQ邮箱或者其他邮箱
host = localReadConfig.get_email("mail_host")# 设置服务器
port = localReadConfig.get_email("mail_port") # 设置服务器
user = localReadConfig.get_email("mail_user")# QQ邮件登录名称
password = localReadConfig.get_email("mail_pass")# QQ邮箱的授权码

title = localReadConfig.get_email("subject")#邮件主题

# 定义邮件主题
date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.subject = "接口测试报告" + " " + date

self.log = MyLog.get_log()
self.logger = self.log.get_logger()

self.msg = MIMEMultipart('related')

def config_header(self):
"""
defined email header include subject, sender and receiver
:return:
"""
self.msg['Subject'] = Header(self.subject) # 邮件主题
self.msg['From'] = Header(sender) # 发件人
self.msg['To'] = Header(str(";".join(receivers))) # 收件人

def config_content(self):
"""
write the content of email
:return:
"""
self.config_file_html_img()
self.config_file()

def config_file_html_img(self):
file_path = os.path.join(readConfig.ProDir, 'testFile', 'emailStyle.html') # 文件路径
with open(file_path, 'rb') as fp: # 读取文件内容
msg_text = MIMEText(fp.read(), 'html', 'utf-8') # 创建Text对象,包括文本内容
self.msg.attach(msg_text) # 构建HTML格式的邮件内容

image2_path = os.path.join(readConfig.ProDir, 'testFile', 'img', 'smile.jpg') # 图片路径
self.msg.attach(self.add_image(image2_path, '<image2>')) # 构建HTML格式的邮件内容

def config_file_html(self):
report_file_path = self.log.get_report_file_path()
with open(report_file_path, encoding='utf-8') as f: # 打开html报告
email_body = f.read() # 读取报告内容
self.msg = MIMEMultipart() # 混合MIME格式
self.msg.attach(MIMEText(email_body, 'html', 'utf-8'))

def config_file(self):

if self.check_file():
report_folder_path = self.log.get_result_folder_path()
filename = [os.path.join(report_folder_path, 'output.log'),
os.path.join(report_folder_path, 'report.html')]
for tmp in filename:
with open(tmp, 'rb') as f:
attach_files = MIMEApplication(f.read())
attach_files.add_header('Content-Disposition', 'attachment', filename=tmp)
self.msg.attach(attach_files)

def add_image(self,src, img_id):
# xml中添加图片
with open(src, 'rb') as f:
msg_image = MIMEImage(f.read()) # 读取图片内容
msg_image.add_header('Content-ID', img_id) # 指定文件的Content-ID,<img>,在HTML中图片src将用到
return msg_image

def check_file(self):
"""
check test report
:return:
"""
report_path = self.log.get_report_file_path()
if os.path.isfile(report_path) and not os.stat(report_path) == 0:
return True
else:
return False

def send_email(self):
"""
send email
:return:
"""
global smtp
self.config_content()
self.config_header()
try:
smtp = smtplib.SMTP_SSL(host,port)
smtp.login(user, password)
smtp.sendmail(sender, receivers, self.msg.as_string())
self.logger.info("测试报告已通过电子邮件发送给开发人员")
except Exception as ex:
self.logger.error(str(ex))
return "邮件发送失败"
finally:
smtp.quit()

class MyEmail:
email = None
mutex = threading.Lock()

def __init__(self):
pass

@staticmethod
def get_email():

if MyEmail.email is None:
MyEmail.mutex.acquire()
MyEmail.email = Email()
MyEmail.mutex.release()
return MyEmail.email


if __name__ == "__main__":
email = MyEmail.get_email()

HTMLTestRunner.py

HTMLTestRunner.py这个文件从网上下载的,大神写好的,用于生成html格式的测试报告
生成报告如下
-w1142

runAll.py

这是整个框架运行的入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import os
import unittest
from common.Log import MyLog as Log
import readConfig as readConfig
from common import HTMLTestRunner
from common.configEmail import MyEmail

localReadConfig = readConfig.ReadConfig()


class AllTest:
def __init__(self):
global log, logger, resultPath, on_off
log = Log.get_log()
logger = log.get_logger()
resultPath = log.get_report_file_path()
on_off = localReadConfig.get_email("on_off")
self.caseListFile = os.path.join(readConfig.ProDir, "caseList.txt")
self.caseFile = os.path.join(readConfig.ProDir, "testCase")
self.caseList = []
self.email = MyEmail.get_email()

def set_case_list(self):
"""
set case list
:return:
"""
fb = open(self.caseListFile)
for value in fb.readlines():
data = str(value)
if data != '' and not data.startswith("#"):
self.caseList.append(data.replace("\n", ""))
fb.close()

def set_case_suite(self):
"""
set case suite
:return:
"""
self.set_case_list()
test_suite = unittest.TestSuite()
suite_module = []

for case in self.caseList:
case_name = case.split("/")[-1]
print(case_name + ".py")
discover = unittest.defaultTestLoader.discover(self.caseFile, pattern=case_name + '.py', top_level_dir=None)
suite_module.append(discover)

if len(suite_module) > 0:

for suite in suite_module:
for test_name in suite:
test_suite.addTest(test_name)
else:
return None

return test_suite

def run(self):
"""
run test
:return:
"""
try:
suit = self.set_case_suite()
if suit is not None:
logger.info("================================== 测试开始 ==================================")
fp = open(resultPath, 'wb')
runner = HTMLTestRunner.HTMLTestRunner(stream=fp, title='即刻接口测试报告', description='用例执行情况')
runner.run(suit)
fp.close()

else:
logger.info("没有需要测试的案例")
except Exception as ex:
logger.error(str(ex))
finally:
# 通过电子邮件发送测试报告
if on_off == 'on':
self.email.send_email()
elif on_off == 'off':
logger.info("不向开发人员发送报告电子邮件")
else:
logger.info("未知状态")

logger.info("================================== 测试结束 ==================================")


if __name__ == '__main__':
obj = AllTest()
obj.run()

上面贴出了runAll里面的主要部分,首先我们要从caselist.txt文件中读取需要执行的case名称,然后将他们添加到python自带的unittest测试集中,最后执行run()函数,执行测试集。

result

result文件夹会在首次执行case时生成,并且以后的测试结果都会被保存在该文件夹下,同时每次测试的文件夹都是用系统时间命名,里面包含了两个文件,log文件和测试报告

testCase

testCase文件夹下,存放我们写的具体的测试case。注意,所有的case名称都要以test开头来命名,因为unittest在进行测试时会自动匹配testCase文件夹下面所有test开头的.py文件
-w345

testFile

testFile文件夹下,放置我们测试时用来管理测试用例的excel文件和用于数据库查询的sql语句的xml文件。
-w238

caselist.txt

凡是没有被注释掉的,都是要被执行的case名称啦。在这里写上你要执行的case名称就可以啦。
-w326

参考
https://my.oschina.net/u/3041656/blog/820023?p=7
参考源码
https://gitee.com/null_534_6629/interfacetest/tree/master

关于

随着时间的推移,个人站点的博客文章会越来越多,那怎么样才能快速找到你印象中的文章呢?增加一个站点内的搜索功能是非常有必要和方便的。

安装本地搜索插件 hexo-generator-search

在博客根目录下执行下列命令

1
npm install hexo-generator-search --save

安装之后,会在站点目录的 public 文件夹下创建一个 search.xml 文件。

修改全局配置文件_config.yml(博客根目录)

在站点配置文件 _config.yml 中添加如下内容:

1
2
3
4
5
6
# Search 
search:
path: ./public/search.xml
field: post
format: html
limit: 10000

path:索引文件的路径,相对于站点根目录
field:搜索范围,默认是 post,还可以选择 page、all,设置成 all 表示搜索所有页面
limit:限制搜索的条目数

hexo主题配置文件(/themes/next/_config.yml)

在主题配置文件 _config.yml 中找到如下内容:

1
2
3
4
local_search:
enable: true
trigger: auto
top_n_per_article: 1

修改local_searchenabletrue

top_n_per_article 字段表示在每篇文章中显示的搜索结果数量,设成 -1 会显示每篇文章的所有搜索结果数量。

重新部署

然后执行以下命令,重新部署网站即可愉快的使用本地搜索功能了

1
hexo clean && hexo g && hexo d

Charles 简介

Charles 是一个支持多平台的 HTTP 代理器、HTTP 监控、反向代理器。它能够让开发者查看本地机器与互联网之间的所有 HTTP 以及 SSL/HTTPS 传输数据。包括请求数据、响应数据 以及 HTTP 头部信息(包括 Cookie 和缓存信息)。

Charles 是收费软件,可以免费试用 30 天。试用期过后,未付费的用户仍然可以继续使用,但是每次使用时间不能超过 30 分钟,并且启动时将会有 10 秒种的延时。因此,该付费方案对广大用户还是相当友好的,即使你长期不付费,也能使用完整的软件功能。只是当你需要长时间进行封包调试时,会因为 Charles 强制关闭而遇到影响.

Charles 主要的功能包括:

  • 截取 Http 和 Https 网络封包。
  • 支持重发网络请求,方便后端调试。
  • 支持修改网络请求参数。
  • 支持网络请求的截获并动态修改。
  • 支持模拟慢速网络。
  • 支持SSL代理。可以截取分析SSL的请求。
  • 支持端口映射。
  • 支持反向代理。
  • 支持SOCKS

下载安装 Charles

charles

  1. 打开浏览器访问 Charles 官网 ,下载相应系统的 Charles 安装包,然后安装即可
    Mac版Charles 4.2.7破解版,百度网盘地址,安装密码:www.ifunmac.com
  2. 安装Charles
    打开 Charles for Mac 4.2.7.dmg 包,后将 Charles 拖到 Application 目录下即完成安装。
  3. 安装完毕后,进行破解,方法很简单:
    Mac 将 charles.jar 覆盖到 安装包Contents/Java 下;

Charles 主界面介绍

Charles 的主界面视图如下图所示:
主界面

工具导航栏

Charles 顶部为菜单导航栏,菜单导航栏下面为工具导航栏。视图如下图所示:
工具栏
工具导航栏中提供了几种常用工具,依次为:

  1. 清除捕获到的所有请求
  2. 红点状态说明正在捕获请求,灰色状态说明目前没有捕获请求。
  3. 灰色状态说明是没有开启网速节流,绿色状态说明开启了网速节流。
  4. 灰色状态说明是没有开启断点,绿色状态说明开启了断点。
  5. 编辑修改请求,点击之后可以修改请求的内容。
  6. 重复发送请求,点击之后选中的请求会被再次发送。
  7. 验证选中的请求的响应。
  8. 常用功能,包含了 Tools 菜单中的常用功能。
  9. 常用设置,包含了 Proxy 菜单中的常用设置。

主界面视图

Charles 主要提供两种查看封包的视图,分别名为 Structure 和 Sequence。

  • Structure: 此视图将网络请求按访问的域名分类。
  • Sequence: 此视图将网络请求按访问的时间排序。

使用时可以根据具体的需要在这两种视图之前来回切换。请求多了有些时候会看不过来,Charles 提供了一个简单的 Filter 功能,可以输入关键字来快速筛选出 URL 中带指定关键字的网络请求。

对于某一个具体的网络请求,你可以查看其详细的请求内容和响应内容。如果请求内容是 POST 的表单,Charles 会自动帮你将表单进行分项显示。如果响应内容是 JSON 格式的,那么 Charles 可以自动帮你将 JSON 内容格式化,方便你查看。如果响应内容是图片,那么 Charles 可以显示出图片的预览。

Charles 菜单介绍

Charles 的主菜单包括:File、Edit、View、Proxy、Tools、Window、Help。用的最多的主菜单分别是 Proxy 和 Tools。

Proxy 菜单

Charles 是一个 HTTP 和 SOCKS 代理服务器。代理请求和响应使 Charles 能够在请求从客户端传递到服务器时检查和更改请求,以及从服务器传递到客户端时的响应。下面主要介绍 Charles 提供的一些代理功能。Proxy 菜单的视图如下图所示:
Prroxy

Proxy 菜单包含以下功能:

  • Start/Stop Recording:开始/停止记录会话。
  • Start/Stop Throttling:开始/停止节流。
  • Enable/Disable Breakpoints:开启/关闭断点模式。
  • Recording Settings:记录会话设置。
  • Throttle Settings:节流设置。
  • Breakpoint Settings:断点设置。
  • Reverse Proxies Settings:反向代理设置。
  • Port Forwarding Settings:端口转发。
  • Windows Proxy:记录计算机上的所有请求。
  • Proxy Settings:代理设置。
  • SSL Proxying Settings:SSL 代理设置。
  • Access Control Settings:访问控制设置。
  • External Proxy Settings:外部代理设置。
  • Web Interface Settings:Web 界面设置。

Recording Settings(记录会话设置)

Recording Settings 和 Start/Stop Recording 配合使用,在 Start Recording 的状态下,可以通过 Recording Settings 配置 Charles 的会话记录行为。Recording Settings 的视图如下图所示:
Recording
Recording Settings 有 Options、Include、Exclude 三个选项卡:

  • Options:通过 Recording Size Limits 限制记录数据的大小。当 Charles 记录时,请求、响应头和响应体存储在内存中,或写入磁盘上的临时文件。有时,内存中的数据量可能会变得太多,Charles 会通知您并停止录制。在这种情况下,您应该清除 Charles 会话以释放内存,然后再次开始录制。在录制设置中,您可以限制 Charles 将记录的最大大小; 这根本不会影响你的浏览,Charles 仅会停止录制。
  • Include:只有与配置的地址匹配的请求才会被录制。
  • Exclude:只有与配置的地址匹配的请求将不会被录制。
    Include 和 Exclude 选项卡的操作相同,选择 Add,然后填入需要监控的Procotol、Host 和 Port等信息,这样就达到了过滤的目的。如下图所示:
    WechatIMG102
    WechatIMG103

Throttle Settings(节流设置)

Throttle Settings 和 Start/Stop Throttling 配合使用,在 Start Throttling 的状态下,可以通过 Throttle Settings 配置 Charles 的网速模拟配置。Throttle Settings 的视图如下图所示:
WechatIMG104

勾选 Enable Throttling 启用网速模拟配置,在 Throttle Preset 下选择网络类型即可,具体设置可以根据实际情况自行设置。如果只想模拟指定网站的慢速网络,可以再勾选上图中的 Only for selected hosts 项,然后在对话框的下半部分设置中增加指定的 hosts 项即可。

Throttle Settings 视图中的选项含义如下:

  • Bandwidth:带宽
  • Utilistation:利用百分比
  • Round-trip:往返延迟
  • MTU:字节

Breakpoint Settings(断点设置)

Breakpoint Settings 和 Enable/Disable Breakpoints 配合使用,在 Enable Breakpoints 的状态下,可以通过 Breakpoint Settings 配置 Charles 的断点模式。Breakpoint Settings 的视图如下图所示:
-w540
勾选 Enable Breakpoints 启用断点模式,选择 Add,然后填入需要监控的Scheme、Procotol、Host 和 Port 等信息,这样就达到了设置断点的目的。然后可以来观察或者修改请求或者返回的内容,但是在这过程中需要注意请求的超时时间问题。或者可以在某个想要设置断点的请求网址上右击选择 Breakpoints 来设置断点。

Reverse Proxies Settings(反向代理设置)

反向代理在本地端口上创建 Web 服务器,该端口透明地将请求代理给远程 Web 服务器。反向代理上的所有请求和响应都可以记录在 Charles 中。
如果您的客户端应用程序不支持使用 HTTP 代理,或者您希望避免将其配置为使用代理,那么反向代理很有用。创建原始目标 Web 服务器的反向代理,然后将客户端应用程序连接到本地端口; 反向代理对客户端应用程序是透明的,使您可以查看 Charles 以前可能无法访问的流量。
有关反向代理的更多信息,请访问 Reverse proxy

Port Forwarding Settings(端口转发)

可以将任何 TCP/IP 或 UDP 端口配置为使用 Port Forwarding 工具从 Charles 转发到远程主机。这样可以调试 Charles 中的任何协议。
在 Macromedia Flash 中调试 XMLSocket 连接时,这尤其有用。
还可以使用 Charles 作为 SOCKS 代理,因此无需设置端口转发。

macOS Proxy(记录计算机上的所有请求)

如果想要抓取电脑端的请求,勾选 Windows Proxy 选项即可;如果只需要抓取手机请求,则取消勾选这个选项。

Proxy Settings(代理设置)

Proxy Settings 的视图如下图所示:
-w592
代理端口默认为 8888(可以修改),并且勾上 Enable transparent HTTP proxying 就完成了在 Charles 上的代理设置。

SSL Proxy Settings(SSL 代理设置)

SSL Proxy Settings 的视图如下图所示:
-w592
勾上 Enable SSL proxying 就完成了在 Charles 上的 SSL 代理设置。之后也可以选择 Add,然后填入需要监控的 Host 和 Port 信息,这样就达到了针对某个域名启用 SSL 代理的目的。

Access Control Settings(访问控制设置)

Access Control Settings 表示访问控制设置。访问控制列表确定谁可以使用此 Charles 实例。通常,您在自己的计算机上运行 Charles,并且您只打算自己使用它,因此 localhost 始终包含在访问控制列表中。也可以选择 Add,然后填入允许访问的 IP,这样就达到了允许某个 IP 访问 Charles 的目的。

External Proxy Settings(外部代理设置)

External Proxy Settings 表示外部代理设置。可能在网络上有一个代理服务器,必须使用该代理服务器才能访问 Internet。在这种情况下,需要将 Charles 配置为在尝试访问 Internet 时使用现有代理。

可以配置单独的代理地址和端口:

  • HTTP
  • HTTPS
  • SOCKS

Web Interface Settings(Web 界面设置)

Web Interface Settings 表示 Web 界面设置。Charles 有一个 Web 界面,可以让您从浏览器控制 Charles,或使用 Web 界面作为 Web 服务使用外部程序。

在 External Proxy Settings 视图中勾选 Enable the web interface 选项启用 Web 界面。可以允许匿名访问,也可以配置用户名和密码。还可以通过在配置使用 Charles 作为其代理的 Web 浏览器中访问 http://control.charles/ 来访问 Web 界面。
Web界面提供对以下功能的访问:

  • 节流控制
    • 激活或停用任何已配置的限制预设
  • 录音控制
    • 开始和停止会话录制
  • 工具
    • 激活和停用工具
  • 会话控制
    • 清除当前会话
    • 以任何支持的格式导出当前会话
    • 以 Charles 的本机会话格式下载当前会话
      退出Charles

通过检查 Web 界面 HTML ,您可以推导出如何将其用作 Web 服务来自动化 Charles。

Tools 菜单

Charles 是一个 HTTP 和 SOCKS 代理服务器,所有的请求都会经过 Charles。下面主要介绍 Charles 提供的一些实用工具。Tools 菜单的视图如下图所示:
Tools
Tools 菜单包含以下功能:

  • No Caching Settings:禁用缓存设置。
  • Block Cookies Settings:禁用 Cookie设置。
  • Map Remote Settings:远程映射设置。
  • Map Local Settings:本地映射设置。
  • Rewrite Settings:重写设置。
  • Black List Settings:黑名单设置。
  • White List Settings:白名单设置。
  • DNS Spoofing Settings:DNS 欺骗设置。
  • Mirror Settings:镜像设置。
  • Auto Save Settings:自动保存设置。
  • Client Process Settings:客户端进程设置。
  • Compose:编辑修改。
  • Repeat:重复发包。
  • Repeat Advanced:高级重复发包。
  • Validate:验证。
  • Publish Gist:发布要点。
  • Import/Export Settings:导入/导出设置。
  • Profiles:配置文件。
  • Publish Gist Settings:发布要点设置。

No Caching Settings(禁用缓存)

No Caching 工具可防止客户端应用程序(如 Web 浏览器)缓存任何资源。因此,始终向远程网站发出请求,您始终可以看到最新版本。

  • 适用范围
    该工具可以作用于每个请求(选中 Enable No Caching 即可),也可以仅对你配置的请求启用(启用 No Caching 的同时,请选中 Only for selected locations)。当用于选定的请求时,可以使用简单但功能强大的模式匹配将工具的效果限制为指定的主机和路径。
  • 工作原理
    No Caching 工具通过操纵控制响应缓存的 HTTP 请求头来防止缓存。从请求中删除 If-Modified-Since 和 If-None-Match 请求头,添加 Pragma:no-cache 和 Cache-control:no-cache。从响应中删除 Expires,Last-Modified 和ETag 请求头,添加 Expires:0 和 Cache-Control:no-cache。

Block Cookies 工具阻止了 Cookie 的发送和接收。它可用于测试网站,就像在浏览器中禁用了 Cookie 一样。 请注意,网络爬虫(例如 Google)通常不支持 Cookie,因此该工具还可用于模拟网络爬虫网站的视图。

  • 适用范围
    该工具可以作用于每个请求(选中 Enable Block Cookies 即可),也可以仅对你配置的请求启用(启用 Block Cookies 的同时,请选中 Only for selected locations)。当用于选定的请求时,可以使用简单但功能强大的模式匹配将工具的效果限制为指定的主机和路径。
  • 工作原理
    Block Cookies 工具通过操纵控制响应 Cookies 的 HTTP 请求头来禁用 Cookies。从请求中移除 Cookie 请求头,防止 Cookie 值从客户端应用程序(例如 Web 浏览器)发送到远程服务器。从响应中删除 Set-Cookie 请求头,防止请求设置客户端应用程序从远程服务器接收的 Cookie。

Map Remote Settings(远程映射)

Map Remote 工具根据配置的映射更改请求站点,以便从新站点透明地提供响应,就好像这是原始请求一样。

通过此映射,您可以从另一个站点提供全部或部分站点。例如:

  • 可以把 xk72.com/charles/ 映射到 localhost/charlesdev/ 来为 xk72.com 提供一个子目录;
  • 可以把 xk72.com/*.php 这种指定后缀的所有文件映射到 localhost/charlesdev/。

使用建议

如果您拥有站点的开发版本并且希望能够通过开发提供的某些请求浏览实时站点,则 Map Remote 非常有用。例如,您可能希望从开发服务器提供 css 和 images 目录。使用 live.com/css/ 等映射到 dev.com/css/ 或 live.com/*.css 到 dev.com 。

映射类型

  • 可以将目录映射到目录,如 xk72.com/charles/ 映射到 localhost/charlesdev/;
  • 可以将文件映射到文件,如 xk72.com/charles/dow… 映射到 abc.com/testing/tes…
  • 可以将带有文件模式的目录映射到目录,如 xk72.com/charles/*.p… 到 localhost/charlesdev/;
  • 如果在目标映射中未指定路径,则 URL 的路径部分将不会更改。如果要映射到根目录,请在目标路径字段中已 / 结尾。

HTTPS

Map Remote 工具可以将 HTTP 请求映射到 HTTPS 目标,反之亦然,因此您可以将 HTTP 或 HTTPS 站点映射到其对立面。

站点匹配

每个站点匹配可能包含协议、主机、端口和路径模式,以匹配特定的 URL。站点可能包括通配符。当您向此工具添加新站点时,可能会找到有关创建站点匹配的更多帮助。

Map Local Settings(本地映射)

Map Local 工具使您可以使用本地文件,就像它们是远程网站的一部分一样。您可以在本地开发文件,并像在线上一样测试它们。本地文件的内容将返回给客户端,就像它是正常的远程响应一样。
Map Local 可以大大加快开发和测试速度,否则您必须将文件上传到网站以测试结果。使用 Map Local,您可以在开发环境中安全地进行测试。

动态文件

动态文件(例如包含服务器端脚本的文件)不会由 Map Local 执行,因此如果文件中有任何脚本,脚本将按原样返回到浏览器,这可能不是预期的结果。如果您想使用动态文件,就好像它们是远程网站的一部分一样,请参阅 Map Remote 工具。

工作原理

当请求与 Map Local 映射匹配时,它会检查与路径匹配的本地文件。它不包括查询字符串(如果有)。如果在本地找到所请求的文件,则将其作为响应返回,就好像它是从远程站点加载的一样,因此它对客户端是透明的。如果在本地找不到所请求的文件,那么该请求会像平常一样由网站提供,返回由真正的服务器提供的数据。

站点匹配

每个站点匹配可能包含协议、主机、端口和路径模式,以匹配特定的 URL。站点可能包括通配符。当您向此工具添加新站点时,可能会找到有关创建站点匹配的更多帮助。

Rewrite Settings(重写)

Rewrite 工具允许创建请求和响应在通过 Charles 时修改他们的规则。如:添加或更改头信息、搜索和替换响应内容中的某些文本等。

重写集

重写集可以单独激活和停用。每个集合包含站点和规则的列表。这些站点选择规则将要运行的请求和响应。

重写规则

每个规则都描述了一次重写操作。规则可能会影响请求URL的 Header,正文或部分内容;它可以根据请求或响应来操作;它可以定义搜索、替换或者仅替换样式重写。

站点匹配

每个站点匹配可能包含协议、主机、端口和路径模式,以匹配特定的 URL。站点可能包括通配符。当您向此工具添加新站点时,可能会找到有关创建站点匹配的更多帮助。

调试

当重写操作未按预期工作时,重写工具可能难以调试。如果您遇到问题,请尝试添加一个非常基本的规则,例如添加明显头信息的规则,以便您可以查看规则是否与请求完全匹配。同时打开错误日志中的调试,以获取从 Charles 中的 Window 菜单访问的错误日志中打印的一些调试信息。

Black List Settings(黑名单)

Black List 工具允许输入应该被阻止的域名。当 Web 浏览器尝试从被列入黑名单的域名请求任何页面时,该请求将被 Charles 阻止。您还可以输入通配符来阻止其子域名。

White List Settings(白名单)

Black List 工具允许输入仅仅被允许的域名。Black List 工具将阻止除被列入白名单的域名之外的所有请求。

白名单工具用于仅允许指定的域名;黑名单工具,用于仅屏蔽指定的域名。
如果一个请求与“黑名单”和“白名单”都匹配,则该请求会被阻止。

DNS Spoofing Settings(DNS 欺骗)

DNS Spoofing 工具允许通过将自己的主机名指定给远程地址映射来欺骗 DNS 查找。 当请求通过 Charles 时,您的 DNS 映射将优先。
Charles 包含配置的域名到 IP 地址映射的列表。当针对列出的域名发出请求时,Spoof DNS 插件会发现欺骗 IP 将请求重定向到该地址。主机HTTP标头保持不变,因此就像您的 DNS 服务器返回欺骗性 IP一样。

虚拟主机

虚拟主机是指单个IP地址上有多个站点,Web 服务器根据浏览器中键入的名称确定要请求的站点。更准确地说,它查看请求中发送的主机头。
如果没有为您的站点设置 DNS,那么您通常无法测试它,因为您不能只输入 IP 地址,因为服务器无法获取名称,因此无法将请求与网站。使用 DNS 欺骗工具来克服此问题。

Mirror Settings(镜像)

Mirror 工具会在浏览指定站点时,把接收到的响应内容克隆一份,并保存在磁盘上指定的路径下。
保存文件的路径会与浏览站点的目录结构相同,并且 Charles 会为主机名创建一个根目录。文件名从 URL 导出并转换为适合的数据进行保存。查询字符串包含在文件名中。如果收到相同 URL 的两个响应,则后面一个文件会覆盖前面的同名文件,因此保存在镜像中在的响应内容将始终为最新的。

选定站点

可以为每个请求启用该工具,也可以仅为指定站点启用该工具。当用于选定的站点时,可以使用简单但功能强大的模式匹配将工具的效果限制为指定的主机和/或路径。

副作用

如果为请求启用镜像工具,它将导致任何压缩或编码的响应被解码。因此,如果服务器提供了压缩响应,Charles 将在传递给客户端之前对其进行解压缩,这通常不会产生任何影响。但是如果您已经构建了自己的客户端,或者客户端希望得到压缩响应,此时将会产生影响。使用 web 浏览器则没有任何影响。

Auto Save Settings(自动保存)

Auto Save 工具会按设定的时间间隔自动保存和清除记录会话。

如果您让 Charles 长时间监控网络活动,并希望将记录分解为可管理的单元,或者避免因数据量过大而可能出现的内存不足情况,这将非常有用。

输入以分钟为单位的保存间隔以及保存会话文件的目录。您可以选择是否在每次运行 Charles 时启动 Auto Save 工具,否则在 Charles 启动时将始终禁用 Auto Save 工具。

会话文件的名称中保存时间戳,格式为 yyyyMMddHHmm,即年月日时分,以便按字母顺序排序时,它们以正确的顺序显示。

Client Process Settings(客户端进程)

Client Process 工具显示负责发出每个请求的本地客户端进程的名称。客户端进程通常是您的 Web 浏览器(例如 firefox.exe),但客户端进程工具可以帮助您发现许多可能未知的 HTTP 客户端。
客户端进程名称显示在每个请求的 Notes 区域中。
如果您可以在 Charles 中看到不确定原始进程的请求,则客户端进程工具很有用。 它仅适用于在运行 Charles 的计算机上发出的请求。
在 Charles 接受每个连接之前,该工具将引入一个短暂的延迟。 延迟通常不明显或不显著。

选定站点

可以为每个请求启用该工具,也可以仅为指定站点启用该工具。当用于选定的站点时,可以使用简单但功能强大的模式匹配将工具的效果限制为指定的主机和/或路径。

Compose(编辑修改)

Compose 工具允许在原有的请求基础上修改。

Repeat(重复)

Repeat 工具允许选择一个请求并重复它。Charles 将请求重新发送到服务器,并将响应显示为新请求。如果您正在进行后端更改并希望在浏览器(或其他客户机)中重复请求的情况下测试这些更改,那么这将非常有用。特别是如果重新创建请求需要花费一些精力,例如在游戏中获得分数,这将节省大量精力。
重复请求是在 Charles 内部完成的,因此无法在浏览器或其他客户端中查看响应,响应只能在 Charles 中查看。

Repeat Advanced(高级重复)

Repeat Advanced 工具扩展了 Repeat 工具,提供了迭代次数和并发数的选项。这对于负载测试非常有用。

Validate(验证)

Validate 工具允许 Charles 通过将它们发送到 W3C HTML 验证器、W3C CSS 验证器和 W3C Feed 验证器来验证记录的响应。
验证报告在 Charles 中显示,其中包含与响应源中相应行相关联的任何警告或错误(双击错误消息中的行号可以切换到源视图)。
因为 Charles 测试它记录的响应,所以它可以测试不易测试的场景,例如在提交表单后呈现错误消息。

重新验证

验证后,可以从验证结果中选择响应并 Repeat,重复原始请求,然后重新验证结果。

Publish Gist(发布要点)

Publish Gist 工具可以将将所选请求和响应作为要点发布。默认情况下,这个要点将匿名发布,这意味着你将无法做到
删除它。可以在 Tools 菜单的 Publish Gist Settings 中授权 Charles 使用您的 GitHub 帐户进行发布。

Import/Export Settings(导入/导出)

Import/Export 工具允许导入/导出 Charles 的 Proxy、Tools、Preferences 等设置。

Profiles(配置)

Profiles 包含所有配置设置的完整副本。
每次更改当前设置时,系统都会更新当前活动的配置文件,当您更改活动配置文件时,所有设置都将恢复为上次使用该配置文件时的状态。
请注意,如果导入已保存的配置,则会覆盖当前配置文件的设置。建议使用导入/导出来备份或创建当前配置和配置文件的快照,以维护多个并行工作区。

Charles 使用教程

通过 Charles 进行 PC 端抓包

Charles 会自动配置浏览器和工具的代理设置,所以说打开工具直接就已经是抓包状态了。只需要保证一下几点即可:

确保 Charles 处于 Start Recording 状态。
勾选 Proxy | macOS Proxy。

通过 Charles 进行移动端抓包

  1. 电脑端配置:
    • 打开 Charles 的代理功能:
    • 通过主菜单打开 Proxy | Proxy Settings 弹窗,查看代理端口(端口默认为 8888,不用修改)
  2. 手机端配置:
    • 打开 iPhone 设置 - 无线局域网,将手机网络连接到与电脑相同WiFi
    • 点击WiFi详情按钮设置HTTP代理,将其改为手动,然后填写Charles所在电脑的代理IP地址,端口号默认为8888。
  3. 设置好之后,我们打开手机上的任意需要网络请求的程序,就可以看到 Charles 弹出手机请求连接的确认菜单(只有首次弹出),点击 Allow 即可完成设置。

完成以上步骤,就可以进行抓包了。

通过 Charles 进行 HTTPS 抓包

HTTPS 的抓包需要在 HTTP 抓包基础上再进行设置。需要完成步骤:

  1. 电脑端安装 Charles 证书:通过 Charles 菜单栏找到 Help - SSL Proxying - Install Charles Root Certificate 安装证书,然后给电脑安装证书,信任证书
    -w694
    -w774
  1. 设置 SSL 代理:通过主菜单打开 Proxy - SSL Proxy Settings 弹窗,勾选 Enable SSL proxying。并添加域名端口,匹配想要监听的域名端口,这里可以添加*:443或*:*匹配全部:
    -w768

  2. 移动端安装 Charles 证书:通过 Charles 的主菜单Help - SSL Proxying - Install Charles Root Certificate on a Mobile Device or Remote Browser.. ,按照弹出的提示会让你将手机切换为手动代理到电脑的Charles,然后用浏览器打开提示上面的地址下载安装证书
    iPhone需要信任证书。设置 - 通用 - 描述文件与设备管理 - 点击证书信任

  3. 设置好之后,我们打开手机上的任意需要网络请求的程序,就可以进行 HTTPS 抓包了。

Focus 功能

使用 Focus 功能指定想要查看的域名,可以避免这个域名相关的请求淹没在茫茫请求当中。当然使用“Structure”视图也可以避免这个问题。
对想要Focus的域名,选中抓取的接口-右键菜单-选择Focus,这个域名就会添加到Focused列表,可以在这里找到:
-w951

-w540
-w1115

重复请求功能Repeat

-w953
使用Repeat Advanced 还可以指定请求次数
-w544
-w1116

查找功能

选中抓取的接口-右键菜单-选择Find In 即可打开查找面板
-w1004

-w751
双击查找结果会跳到想要的结果

设置断点拦截请求响应

首先开启断点功能,然后再到想要设置断点的请求上,右键设置一个断点,如图所示:
-w1000
查看是否加入断点设置面板
-w1168
-w540
默认拦截请求和响应,如果只需要两者之一,可以选择设置
-w440
再次发起请求,就会看到拦截了请求和响应
-w1167

-w1167

常见接口测试工具

  • Postman: 简单方便的接口调试工具,便于分享和协作。具有接口调试,接口集管理,环境配置,参数化,断言,批量执行,录制接口,Mock Server, 接口文档,接口监控等功能
  • JMeter: 开源接口测试及压测工具,支持Linux及无界面运行
  • LR: 商业版接口性能测试工具,简单易用,功能强大
  • SoupUI: 开源,WebService接口常用测试工具,也可以测试Rest接口及接口安全

新版Postman使用简介

Postman 官网地址:https://www.getpostman.com/pricing
Postman主界面
Postman用法介绍

工具栏

  • New: 新建,可以新建Request请求,Collection请求集,环境等等
  • Import: 导入,可以导入别人导出的请求集
  • Runner: 运行一个请求集(批量执行)
  • Invite: 邀请(需要注册,邀请进行协作)
  • 同步图标: (需要注册,同步你的项目到云端)
  • 抓包图标: 抓包/捕获请求,用于开启Postman代理, 手动设置代理(或手机代理)后可抓包/录制请求
  • 设置图标: Postman设置
  • 消息图标: 官方及协助消息
  • 收藏图标: 我的收藏(需要注册)
  • 云端图标: 用户云端数据(需要注册)

接口管理区

  • History: 请求历史记录,可以查询到之前的请求记录
  • Collections: 接口集,相当于一个接口项目或测试计划,接口集中可以建立无限极子文件夹,用于对接口进行分组管理

环境管理区

  • 环境切换:用于切换环境
  • 环境预览:用于快速预览环境中的所有变量
  • 环境管理:用于添加修改环境及环境变量,以及全局变量

什么是环境

接口完整地址 = 服务地址 + 接口地址, 如
https://app.jike.ruguoapp.com + /1.0/users/login
环境是一套配置,包含许多环境变量。在接口测试中,根据部署在不同的服务器上,服务器地址有可能不同,而同一个接口,接口地址是不变的。为了测试部署在不同服务器上的同一套接口,我们可以建立不同的环境,不同环境中host变量使用不同的地址

接口设计区

可以通过上方tab边上的+号,新建多个请求。接口设计区从上到下分为请求区和响应区

  • 请求区

    • 请求地址行:可以选择请求方法(GET/POST/…),填写请求地址,发送请求和保存请求到测试集
    • 请求数据区:分为授权,请求头,请求数据,请求发送前执行的脚本(用于准备数据),请求结束后执行的脚本(用于断言)
  • 响应区:

    • 响应内容: 可以查看Pretty(美化格式),Raw(原始格式),Preview(HTML预览格式)
    • 响应Cookie
    • 响应头
    • 测试结果,对应请求中Tests中设置的断言

      Collection请求集

      测试集是Postman中接口管理的一个“整体”单位,运行、导出、分享等都是基于测试集的。
  • 新建测试集: New按钮->Collection 或 直接点击测试集列表上方的新建测试集按钮
    新建测试集

    • 授权: 测试集及其子文件夹下的接口统一使用该授权,不用每个接口再都单独设置一遍
    • 请求前脚本: 测试集的每个接口公用的请求前脚本
    • 请求后断言: 测试集每个接口公用的请求后脚本
    • 请求集变量: 请求集中公用的一些变量
  • 子文件夹:子文件夹的属性中同样拥有描述,授权,请求前脚本,和请求后断言(没有变量,一个请求集的变量统一管理),实现了不同范围(Scope)的Fixture功能。

  • 请求集导出:请求集可以导出并发送给别人(不携带环境信息),别人通过导入来使用你的接口

  • 请求集分享: 请求集直接分享给别人(双方都需要注册)

  • 环境管理

    • 我们可以环境中设置多个变量,以供在请求中使用
    • 环境变量使用方法:
      • 选择环境,在请求URL或者请求Body里使用来使用环境变量
      • 变量可以在请求Body的各种格式中使用
      • 不能直接在请求前脚本(Pre-request Script)和请求后脚本(Tests)中使用
    • 环境管理中还可以点击“Global”添加全局变量,环境变量只有当选择了该环境时生效,全局变量在任何环境中生效,测试集中的变量只在当前测试集生效,当测试集变量,环境变量,全局变量有重复的变量名时,优先级为:环境变量>全局变量>测试集变量
      设置变量
      使用变量

Params使用

当请求URL中参数很多时,不方便进行添加和查看,可以点击Params按钮,以表格的方式添加变量及值,从表格添加后,变量和值会自动添加到URL中

请求设计

  • 授权:如果接口需要授权,可以在该页面设置授权方式(type)和授权信息
  • Header: 请求头,可以设置请求类型(Content-Type)和Cookie
  • Body: 请求数据
    • form-data:混合表单,支持上传文件
    • x-www-form-urlencoded:文本表单
    • raw:原始格式,支持JSON/XML格式(后面可选择)
    • binary: 二进制格式,用于发送二进制数据流
  • Pre-request Script: 请求前脚本,Javascript语法,用于在发送请求前生成一些动态数据或做一些处理
  • Tests:请求后脚本,Javascript语法,用于请求返回后做一些处理或断言结果

Postman发送各种格式请求的方法:

注意:选择不同的请求,会自动在Header中添加Content-Type信息

Tests断言

一个Postman的test本质上是JavaScript的代码可以用来为一些特殊的test设置值。你可以在对象中设置一个描述性的键作为一个元素,然后声明他如果是true或false。
tests[“Body contains user_id”] = responseBody.has(“user_id”)
这回核对body中是否包含了user_id这个字符串。如果你需要,你可以增加更多的键,这取决于你要用test做多少的事情。

  • HTTP状态码断言:
1
tests["HTTP状态码200"]=responseCode.code == 200;
  • 响应包含内容断言:
1
tests["状态码200"] = responseBody.has("登录成功"|"你已经登录,无需重复登录");

接口样例
POST https://demo.fastadmin.net/admin/index/login.html 用户名/密码: username:admin password:123456

  • JSON响应断言
1
2
var jsonData = JSON.parse(responseBody); //获取body中返回的所有参数
tests["code为100000表示成功"] = jsonData.code == "100000"

接口样例 GET http://www.tuling123.com/openapi/api?key=ec961279f453459b9248f0aeb6600bbe&info=你好

tests

Postman尝试通过列出编辑器旁边常用的片段来简化过程。您可以选择要添加的代码段,并将相应的代码添加到测试编辑器中。

Runner: 测试集批量执行

  • 支持设置迭代次数
  • 支持加载csv或json类测试数据
  • 操作方法:https://demo.fastadmin.net/admin/index/login.html接口
  • 新建一个Collection,比如名称Demo2
  • 填入URL:https://demo.fastadmin.net/admin/index/login.html, 选择POST方法
  • 请求数据(Body)格式选x-www-form-urlecoded,请求数据填写username password ,这里使用了两个变量来做参数化
  • 保存请求到Demo2中
  • 在电脑上新建一个data.csv文件,第一行为变量名
  • 点击Postman工具栏的Runner按钮,Collection选择Demo2, Data选择数据文件data.csv, 点击运行Demo2

环境变量的用法详解

在postman中,可以利用tests将接口返回的response设置为环境变量,供后续接口使用(类似参数化的概念)

1
2
3
4
5
var jsonData =JSON.parse(responseBody); //获取body中返回的所有参数
postman.setEnvironmentVariable("appKey",jsonData.data.appKey);//把返回参数中的keys设置为环境变量

var data = postman.getResponseHeader("Access-Token"); //获取响应头所有参数
postman.setEnvironmentVariable("Access-Token",data); //把返回参数中的Access-Token设置为环境变量

postman常用方法集合

https://learning.getpostman.com/docs/postman/scripts/test_examples/

环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//设置环境变量
pm.environment.set("variable_key", "variable_value");

//将一个嵌套的对象设置为一个环境变量
var array = [1, 2, 3, 4];
pm.environment.set("array", JSON.stringify(array, null, 2));

var obj = { a: [1, 2, 3, 4], b: { c: 'val' } };
pm.environment.set("obj", JSON.stringify(obj))

//获取环境变量
pm.environment.get("variable_key");

//获取环境变量(其值是字符串化对象)
// 如果数据来自未知源,则应该将这些语句包装在try-catch块中
var array = JSON.parse(pm.environment.get("array"));
var obj = JSON.parse(pm.environment.get("obj"));

//清除环境变量
pm.environment.unset("variable_key"); //清除一个环境变量

全局变量

1
2
3
pm.globals.set("variable_key", "variable_value"); //设置一个全局变量
pm.globals.get("variable_key"); //获取一个全局变量
pm.globals.unset("variable_key"); //清除一个全局变量

获取一个变量

该函数在全局变量和活动环境中搜索变量

1
pm.variables.get("variable_key");

检查response body中是否包含某个string

1
2
3
4
5
tests["Body matches string"] = responseBody.has("string_you_want_to_search");

pm.test("Body matches string", function () {
pm.expect(pm.response.text()).to.include("string_you_want_to_search");
});

检查response body是否与某个string相等

1
2
3
4
5
tests["Body is correct"] = responseBody === "哈哈哈"; //必须完全匹配

pm.test("Body is correct", function () {
pm.response.to.have.body("response_body_string");
});

检测JSON中的某个值是否等于预期的值

1
2
3
4
5
6
7
8
9
10
var data = JSON.parse(responseBody);
tests["Your test name"] = data.value === 100;
tests["状态码为301"] = data["status"] == "301";
tests["message"] = data["message"] == "购买商品库存不足";
tests["list"] = data["lists"][0] == "11";

pm.test("Your test name", function () {
var jsonData = pm.response.json();
pm.expect(jsonData.value).to.eql(100);
});

JSON.parse()方法,把json字符串转化为对象。parse()会进行json格式的检查是一个安全的函数。
如:检查json中某个数组元素的个数(这里检测programs的长度)

1
2
var data = JSON.parse(responseBody);
tests["program's lenght"] = data.programs.length === 5;

测试response Headers中的某个元素是否存在(如:Content-Type)

1
2
3
4
5
6
7
//getResponseHeader()方法会返回header的值,如果该值存在的话
tests["Content-Type is present"] = postman.getResponseHeader("Content-Type"); //不区分大小写
tests["Content-Type is present"] = responseHeaders.hasOwnProperty("Content-Type"); //要区分大小写

pm.test("Content-Type is present", function () {
pm.response.to.have.header("Content-Type");
});

验证响应时间小于xxx毫秒

1
2
3
4
5
tests["Response time is less than 200ms"] = responseTime < 200;

pm.test("Response time is less than 200ms", function () {
pm.expect(pm.response.responseTime).to.be.below(200);
});

响应时间在特定范围内(包含下限,上限不包括)

1
tests["Response time is acceptable"] = _.inRange(responseTime, 100, 1001); 

验证状态码的值为200

1
2
3
4
5
tests["Status code is 200"] = responseCode.code === 200;

pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});

代码名包含一个字符串

1
2
3
4
5
tests["Status code name has string"] = responseCode.name.has("Created");

pm.test("Status code name has string", function () {
pm.response.to.have.status("Created");
});

成功的POST请求状态代码

1
2
3
4
5
tests["Successful POST request"] = responseCode.code === 201 || responseCode.code === 202;

pm.test("Successful POST request", function () {
pm.expect(pm.response.code).to.be.oneOf([201,202]);
});

使用TinyValidator获取JSON数据

1
2
3
4
5
6
7
8
9
var schema = { "items": { "type": "boolean" }};
var data1 = [true, false];
var data2 = [true, 123];
pm.test('Schema is valid', function() {
pm.expect(tv4.validate(data1, schema)).to.be.true;
pm.expect(tv4.validate(data2, schema)).to.be.true;
});
//data1和data2是测试数据,schema相当于验证json的规范
//示例中验证data1和data2中的value是否都是boolean类型

获取request值:

  • data {object}:
    这是一个用于请求的表单数据字典 (request.data[“key”]==”value”)
  • headers {object}:
    这是请求头的字典 (request.headers[“key”]==”value”)
  • method {string}:
    GET/POST/PUT 等.
  • url {string}:
    请求的url
1
var Json = JSON.parse(request.data); 

假设requestBody中有”version”:”1.0”;这个值,如果想获取到version的value值,代码如下

1
2
var Json = JSON.parse(request.data); 
var version = Json["version"];

JSON.parse()和JSON.stringify()

1
2
3
4
5
6
7
8
9
10
JSON.parse() //【从一个字符串中解析出json对象】--把string转对象
JSON.stringify() //【从一个对象中解析出字符串,主要针对[object object]类型数据的转换】--把对象转String

var data={name:'goatling'}

JSON.parse(data)
//结果是: '{"name":"goatling"}'

JSON.stringify(data)
//结果是:name:"goatling"

发送异步请求。此功能既可用作预请求脚本,也可用作测试脚本。

1
2
3
pm.sendRequest("https://postman-echo.com/get", function (err, response) {
console.log(response.json());
});

转换XML body为JSON对象

1
var jsonObject = xml2Json(responseBody);

判断字段值是否为空typeof()

1
2
var Jsondata = JSON.parse(responseBody);
if( typeof(Jsondata.data) != "undefined" )

分支和循环

1
2
3
4
//设置下一个要执行的请求
postman.setNextRequest("request_name");
//停止工作流程执行
postman.setNextRequest(null);

postman.setNextRequest()一些重点:
* 指定后续请求的名称或ID,集合运行器将负责其余的请求。
* 它可以用在预请求或测试脚本中。如果有多个赋值,则考虑最后一个赋值。
* 如果postman.setNextRequest()请求中不存在,则集合运行器默认为线性执行并移至下一个请求

服务端返回异常 Status Code:

HTTP状态码

  • 1** 信息,服务器收到请求,需要请求者继续执行操作
  • 2** 成功,操作被成功接收并处理
  • 3** 重定向,需要进一步的操作以完成请求
  • 4** 客户端错误,请求包含语法错误或无法完成请求
  • 5** 服务器错误,服务器在处理请求的过程中发生了错误

常见HTTP响应码

  • 200:成功
  • 301、302:请求重定向到另外一个接口
  • 400:请求格式不对/服务端故意失败
  • 403:禁止访问(被封禁/没有权限)
  • 404:资源不存在(有可能是请求url错误或参数不正确)
  • 405:请求方法不被允许(比如接口只允许Post,使用Get请求接口)
  • 429:请求频率过高
  • 500:服务器内部错误(通常是服务器挂了或接口Bug)
  • 502、503:服务发布间隙没衔接好/挂了
  • 504:网关请求超时(Bug,挂了)
  • 409、406:正常返回,不应该被记录

首先检测自己电脑是否存在 SSH key

在生成SSH密钥之前,您可以检查是否有任何现有的SSH密钥。

  1. 打开终端

  2. 输入ls -al ~/.ssh以查看是否存在现有SSH密钥:

    1
    2
    ls -al ~/.ssh
    # 列出.ssh目录中的文件(如果存在的话)
  3. 检查目录列表以查看您是否已拥有公共SSH密钥。

    • 如果你本地没有生成的话
      终端上面会显示:No such file or directory
    • 如果已经存在的话,
      则会显示 id_rsa 和 id_rsa.pub

如果本地没有的话,我们要自己生成新的SSH秘钥

  1. 开放式终端。

  2. 输入下面的命令,记得替换您的GitHub电子邮件地址。

    1
    2
    ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
    #"your_email@example.com"为示例邮箱,此处应填写为你在github上面注册的邮箱。

    这将使用提供的电子邮件作为标签创建一个新的ssh密钥。

    Generating public/private rsa key pair.

  3. 当系统提示您“输入要保存密钥的文件”时,按Enter键使用默认文件位置。

    Enter a file in which to save the key (/Users/you/.ssh/id_rsa): [Press enter]

  4. 在提示符下,键入安全密码。可以连续Enter,不设置密码。

    Enter passphrase (empty for no passphrase): [Type a passphrase]
    Enter same passphrase again: [Type passphrase again]

成功后,终端显示

Your identitification has been saved in /Users/xxx/.ssh/id_rsa.
Your public key has been saved in /user/xxx/id_rsa.pub.
the key fingerprint is xxxxxxxxxx…….xxxxxxxx your_email@example.com
The key’s randomart image is:
-w465

将SSH密钥添加到ssh-agent

  1. 在后台启动ssh-agent

    1
    2
    eval "$(ssh-agent -s)"
    > Agent pid 59566
  2. 终端输入命令:
    ssh-add -K ~/.ssh/id_rsa
    如果设置密码,此时会要求输入passphrase(输入步骤 二 中的 passphrase就好 )

成功后,中选显示:

Identity added: /Users/xxx/.ssh/id_rsa (your_email@example.com)

添加SSH key 到Github 上

  1. 将SSH密钥复制到剪贴板。

    1
    2
    pbcopy < ~/.ssh/id_rsa.pub
    # 复制id_rsa.pub的内容到剪贴板
    提示:如果pbcopy不起作用,您可以找到隐藏.ssh文件夹,在文本编辑器中打开id_rsa.pub文件,然后将文件内容复制到剪贴板。
    
  1. 在任意页面的右上角,单击您的个人资料照片,然后单击“Settings”。

  2. 在用户设置侧栏中,单击“SSH and GPG keys”。

  3. 单击“ New SSH key”

  4. 在“Title”字段中添加描述性标签,Key字段中粘贴您复制的id_rsa.pub内容。最后点击“Add SSH key”

  5. 最后请确认您的GitHub密码。

如何检测SSH key

  1. 终端输入命令:ssh git@github.com

  2. 验证SSH key是否有权限访问 你的 github,输入“yes”

    The authenticity of host ‘github.com (13.250.177.223)’ can’t be established.
    RSA key fingerprint is SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8.
    Are you sure you want to continue connecting (yes/no)? yes

  3. 成功会显示为

    Hi you name! You’ve successfully authenticated, but GitHub does not provide shell access.

简介

A library to communicate with services of Apple iOS devices using native protocols.

命令

  • 打印app列表

    1
    ideviceinstaller -l
  • 查看当前已连接的设备的UUID

    1
    idevice_id -l
  • 获取设备信息

    1
    ideviceinfo
  • 获取设备时间

    1
    idevicedate
  • 重启设备

    1
    idevicediagnostics restart
  • 安装ipa包,卸载应用

    1
    2
    3
    4
    5
    ideviceinstaller -i xxx.ipa
    //命令安装一个ipa文件到手机上,如果是企业签名的,非越狱机器也可以直接安装了。

    ideviceinstaller -U [bundleID]
    //命令卸载应用,需要知道此应用的bundleID
  • 查看系统日志

    1
    2
    3
    4
    5
    6
    7
    8
    9
    idevicesyslog
    //屏幕上即可看见手机上所有的日志

    idevicesyslog >> iphone.log &
    //该命令是将日志导入到iphone.log这个文件,并且是在后台执行。
    //然后用tail -f和grep查看log

    tail -f iphone.log
    tail -f iphone.log | grep 'WeChat’ # 查看包含WeChat的行
  • 截图

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    idevicescreenshot
    //如果在使用截图的时候出现报错信息,那么就去把相应版本的DeveloperDiskImage的两个文件复制到libimobiledevice文件下面。

    路径:
    /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/对应版本/

    获取版本号命令:
    ideviceinfo -k ProductVersion

    安装DeveloperDiskImage命令:
    ideviceimagemounter DeveloperDiskImage.dmg
    //然后就可以正常截图了
  • 遇到报错

    1
    2
    3
    4
    5
    解决方法:卸载重装
    brew uninstall ideviceinstaller
    brew uninstall libimobiledevice
    brew install --HEAD libimobiledevice
    brew install ideviceinstaller

简介

RVM是一个命令行工具,它允许您轻松地安装,管理和使用从解释器到多组宝石的多个ruby环境。

安装Ruby原因

虽然 macOS 自带了一个 ruby 环境,但是是系统自己使用的,所以权限很小,只有 system。而/Library 目录是 root 权限,所以很多会提示无权限。

安装RVM

  1. 安装mpapis公钥。但是,正如安装页面所记录的,您可能需要gpg。Mac OS X不附带gpg,因此在安装公钥之前,您需要安装gpg。

    Homebrew安装gpg :

    1
    brew install gnupg 
  2. 安装完gpg之后,你可以安装mpapis公钥:

    1
    gpg2 --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
  3. 安装最新版本的Ruby的RVM

    1
    \curl -sSL https://get.rvm.io | bash -s stable
  4. 安装完载入 RVM 环境:

    1
    source ~/.rvm/scripts/rvm
  5. 检查版本

    1
    rvm -v

(如果想卸载RVM,执行以下命令)

1
rvm implode

使用RVM&Ruby

ruby rvm 常用指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ruby -v # 查看ruby 版本
rvm list known # 列出已知的 ruby 版本
rvm install 2.3.0 # 选择指定 ruby 版本进行更新
rvm get stable # 更新 rvm
rvm use 2.2.2 # 切换到指定 ruby 版本
rvm use 2.2.2 --default # 设置指定 ruby 版本为默认版本
rvm list # 查询已安装的 ruby 版本
rvm remove 1.9.2 # 卸载移除 指定 ruby 版本
rvm uninstall 2.0.0
rvm system #使用系统ruby(就像没有rvm一样)

curl -L https://get.rvm.io | bash -s stable # 安装 rvm 环境
curl -sSL https://get.rvm.io | bash -s stable --ruby # 默认安装 rvm 最新版本
curl -sSL https://get.rvm.io | bash -s stable --ruby=2.3.0 # 安装 rvm 指定版本
source ~/.rvm/scripts/rvm # 载入 rvm

Gem常用指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
gem -v # 查看 gem 版本
gem source # 查看 gem 配置源
gem source -l # 查看 gem 配置源目录
gem sources -a url # 添加 gem 配置源(url 需换成网址)
gem sources --add url # 添加 gem 配置源(url 需换成网址)
gem sources -r url # 删除 gem 配置源(url 需换成网址)
gem sources --remove url # 删除 gem 配置源(url 需换成网址)
gem update # 更新 所有包
gem update --system # 更新 Ruby Gems 软件

gem install rake # 安装 rake,从本地或远程服务器
gem install rake --remote # 安装 rake,从远程服务器
gem install watir -v 1.6.2 # 安装 指定版本的 watir
gem install watir --version 1.6.2 # 安装 指定版本的 watir
gem uninstall rake # 卸载 rake 包
gem list d # 列出 本地以 d 打头的包
gem query -n ''[0-9]'' --local # 查找 本地含有数字的包
gem search log --both # 查找 从本地和远程服务器上查找含有 log 字符串的包
gem search log --remoter # 查找 只从远程服务器上查找含有 log 字符串的包
gem search -r log # 查找 只从远程服务器上查找含有log字符串的包

gem help # 提醒式的帮助
gem help install # 列出 install 命令 帮助
gem help examples # 列出 gem 命令使用一些例子
gem build rake.gemspec # 把 rake.gemspec 编译成 rake.gem
gem check -v pkg/rake-0.4.0.gem # 检测 rake 是否有效
gem cleanup # 清除 所有包旧版本,保留最新版本
gem contents rake # 显示 rake 包中所包含的文件
gem dependency rails -v 0.10.1 # 列出 与 rails 相互依赖的包
gem environment # 查看 gem 的环境

sudo gem -v # 查看 gem 版本(以管理员权限)
sudo gem install cocoa pods # 安装 CocoaPods(以管理员权限)
sudo gem install cocoapods # 安装 CocoaPods(以管理员权限)
sudo gem install cocoapods --pre # 安装 CocoaPods 至预览版(以管理员权限)
sudo gem install cocoapods -v 0.39.0 # 安装 CocoaPods 指定版本(以管理员权限)
sudo gem update cocoapods # 更新 CocoaPods 至最新版(以管理员权限)
sudo gem update cocoapods --pre # 更新 CocoaPods 至预览版(以管理员权限)
sudo gem uninstall cocoapods -v 0.39.0 # 移除 CocoaPods 指定版本(以管理员权限)

pod常用指令

1
2
3
4
pod setup # CocoaPods 将信息下载到~/.cocoapods/repos 目录下。如果安装 CocoaPods 时不执行此命令,在初次执行 pod intall 命令时,系统也会自动执行该指令
pod --version # 检查 CocoaPods 是否安装成功及其版本号
pod install # 安装 CocoaPods 的配置文件 Podfile

环境

| 名称 | 版本 |
| — | — | — |
| macOS | 10.14.4 (18E226) |
| Xcode | Version 10.2.1 |
| ruby | 2.6.3 |
| fastlane | 2.123.0 |

Fastlane安装

Fastlane是一套使用Ruby写的自动化工具集,用于iOS和Android的自动化打包、发布等工作,可以节省大量的时间。
安装过程如下

安装ruby(brew安装最新版)

1
2
3
4
5
6
7
8
9
10
11
12
13
#直接在终端执行,未安装brew,先安装brew

#安装最新的2.6.3
brew install ruby

#安装完根据提示执行
echo 'export PATH="/usr/local/opt/ruby/bin:$PATH"' >> ~/.zshrc

#退出终端查看ruby版本
ruby -v

#检查安装路径
which -a ruby

更改gem源

  • 更改
    1
    2
    gem sources --remove https://rubygems.org/
    gem sources --add https://gems.ruby-china.com/
  • 查看
    1
    gem sources -l
    如果是以下结果说明正确,如果有其他的请自行百度解决
    1
    2
    *** CURRENT SOURCES ***
    https://gems.ruby-china.com/
  • 安装fastlane
    1
    2
    3
    4
    5
    #安装
    gem install -n /usr/local/bin fastlane -NV

    #检查是否安装成
    fastlane --version

至此,我们已经可以使用fastlane自动化打包了

Fastlane配置

fastlane init

cd 到工程主目录执行fastlane init 我这里选择的是手动配置

1
2
cd /Users/vic/.jenkins/workspace/jike-iOS-Fastlan
fastlane init

会在你项目工程的目录下生成一个fastlane文件夹,里面有Fastlane的配置文件,一个是Appfile文件(保存了苹果开发者的相关信息),一个是Fastfile文件(运行脚本)

编辑Fastfile文件(运行脚本)

有时候一天需要打好几个包,为了区分,我们这里实现一个递增build号的功能。

  • 定义一个递增build号的函数,添加到Fastfile中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    def updateProjectBuildNumber

    currentTime = Time.new.strftime("%Y%m%d")
    build = get_build_number()
    if build.include?"#{currentTime}."
    # => 为当天版本 计算迭代版本号
    lastStr = build[build.length-2..build.length-1]
    lastNum = lastStr.to_i
    lastNum = lastNum + 1
    lastStr = lastNum.to_s
    if lastNum < 10
    lastStr = lastStr.insert(0,"0")
    end
    build = "#{currentTime}.#{lastStr}"
    else
    # => 非当天版本 build 号重置
    build = "#{currentTime}.01"
    end
    puts("*************| 更新build #{build} |*************")
    # => 更改项目 build
    increment_build_number(
    build_number: "#{build}"
    )
    end
  • 实现自动打包的完整Fastfile如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    # 定义fastlane版本号
    fastlane_version "2.123.0"

    # 定义打包平台
    default_platform :ios

    def updateProjectBuildNumber

    currentTime = Time.new.strftime("%Y%m%d")
    build = get_build_number()
    if build.include?"#{currentTime}."
    # => 为当天版本 计算迭代版本号
    lastStr = build[build.length-2..build.length-1]
    lastNum = lastStr.to_i
    lastNum = lastNum + 1
    lastStr = lastNum.to_s
    if lastNum < 10
    lastStr = lastStr.insert(0,"0")
    end
    build = "#{currentTime}.#{lastStr}"
    else
    # => 非当天版本 build 号重置
    build = "#{currentTime}.01"
    end
    puts("*************| 更新build #{build} |*************")
    # => 更改项目 build 号
    increment_build_number(
    build_number: "#{build}"
    )
    end

    #指定项目的scheme名称
    scheme="Ruguo"
    #蒲公英api_key和user_key
    api_key="8e860ee5ba4996f9b19e56c28e07846a"
    user_key="542e73b113b5908da8b028805bf7e837"

    # 任务脚本
    platform :ios do
    lane :development_build do|options|
    branch = options[:branch]

    puts "开始打development ipa"

    updateProjectBuildNumber #更改项目build号

    # 开始打包
    gym(
    #指定需要编译的scheme
    scheme:"#{scheme}",
    #输出的ipa名称
    output_name:"#{scheme}_#{get_build_number()}",
    # 是否清空以前的编译信息 true:是
    clean:true,
    # 指定打包方式,Release 或者 Debug
    configuration:"Release",
    # 指定打包所使用的输出方式,目前支持app-store, package, ad-hoc, enterprise, development
    export_method:"development",
    # 指定输出文件夹
    output_directory:"./fastlane/build",
    )

    #puts "开始上传蒲公英"
    # 开始上传蒲公英
    #pgyer(api_key: "#{api_key}", user_key: "#{user_key}", password: "alpha", install_type: "2")

    end
    end

    注意:蒲公英的 api_key 和 user_key,开发者在自己账号下的 账号设置-API信息 中可以找到。打其它类型的包的方法与development类似,可自定义一个新的lane实现。

自动打包并上传蒲公英,

在终端输入便会进行自动打包并上传蒲公英了。

1
fastlane development_build

Jenkins安装

下载安装Jenkins。具体安装过程可参考 Mac Jenkins搭建

Jenkins配置

  • 点击新建,输入名称,构建一个自由风格的软件项目

  • 添加Git仓库地址,可以是HTTP也可以是SSH。点击Add

  • 构建,点击“添加构建步骤”,选择Execute shell。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    export PATH=/usr/local/bin:$PATH
    fastlane development_build

    filepath=`pwd`fastlane/build/Ruguo*.api
    file=`ls $filepath`
    if [ $uploadPgy = true ]
    then
    curl -F "installType=2" -F "password=alpha" -F "file=@$file" -F "uKey=542e73b113b5908da8b028805bf7e837" -F "_api_key=8e860ee5ba4996f9b19e56c28e07846a" https://qiniu-storage.pgyer.com/apiv1/app/upload
    fi
  • 添加完成之后保存,点击立即构建

环境

| 名称 | 版本 |
| — | — | — |
| macOS | 10.14.4 (18E226) |
| Xcode | Version 10.2.1 |
| ruby | 2.6.3 |
| fir-cli | 1.7.1 |

下载安装Jenkins

  • Jenkins是基于Java环境的,所以电脑上必须先安装Java,Jenkins官网(https://jenkins.io
  • 下载安装Jenkins。具体安装过程可参考 Mac Jenkins搭建
  • 安装完成后会自动打开http://localhost:8080这个网址,如果没有自动打开可以手动打开。有些教程还有添加Xcode integrationKeychains and Provisioning Profiles Plugin之类插件的步骤,我们不需要。因为我们是用xcode脚本构建,所以不需要安装任何插件。

新建任务

构建一个自由风格的软件项目

点击左侧菜单中的新建任务,然后在新的界面输入项目名字,选择构建一个自由风格的软件项目选项完成创建

填写工程相关介绍

-w960

源码管理

  源码管理,使用github选择git
  -w949

构建触发器

  • 选择Build periodically,输入:
    1
    H 2 * * *
    周期性的执行编译任务,强制每天凌晨两点构建一次
  • 选择Poll SCM,输入

    1
    H/5 * * * *

    表示的就是每5分钟检查一次源码变化。

    第一个参数代表的是分钟 minute,取值 0~59;
    第二个参数代表的是小时 hour,取值 0~23;
    第三个参数代表的是天 day,取值 1~31;
    第四个参数代表的是月 month,取值 1~12;
    最后一个参数代表的是星期 week,取值 0~7,0 和 7 都是表示星期天。
    

构建环境

  通过xcodebuild脚本的方式构建,这里不用做任何设置。

构建

重点来了。
点击“添加构建步骤”,选择Execute shell。
-w951

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh +x
xcodebuild clean -workspace /Users/vic/.jenkins/workspace/jike-iOS/Ruguo.xcworkspace \
-scheme Ruguo \

xcodebuild archive -workspace /Users/vic/.jenkins/workspace/jike-iOS/Ruguo.xcworkspace \
-scheme Ruguo \
-archivePath "$WORKSPACE/Ruguo.xcarchive" \
-configuration Debug \

xcodebuild -exportArchive -archivePath "$WORKSPACE/Ruguo.xcarchive" \
-exportPath "$WORKSPACE/Export/Ruguo" \
-exportOptionsPlist "$WORKSPACE/ExportOptions.plist" \
-configuration Debug \



export PATH=/usr/local/bin:$PATH

fir publish /Users/vic/.jenkins/workspace/jike-iOS/Export/Ruguo/Ruguo.ipa -T 4554bab16fc50b5b8ee20d9663fa64c7 --password=jikebeta

说明:
-archivePath:.xcarchive文件的存放路径。
-exportPath:导出文件的路径。Export文件夹是自己建的。
-ExportOptions.plist:这个文件的内容其实是ipa的打包信息。手动archive并export时,这个文件就包含在最终输出的文件夹内,可以copy一份出来使用。
-configuration :默认Debug或Release。
这里有个坑要说明一下。archive和ipa文件的导出目录最好位于Jenkins用户下的那个工程中,我之前选择的是导出到当前用户的桌面,结果一直报读写权限的问题。

上传到fir

要实现自动上传到fir,要先安装fir-cli
fir-cli由ruby实现,所以先安装ruby

  • 安装ruby,ruby官网安装文档(我使用了brew安装)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #直接在终端执行,未安装brew,先安装brew

    #安装最新的2.6.3
    brew install ruby

    #安装完根据提示执行
    echo 'export PATH="/usr/local/opt/ruby/bin:$PATH"' >> ~/.zshrc

    #退出终端查看ruby版本
    ruby -v

    #检查安装路径
    which -a ruby
  • 安装fir-cli。要实现自动上传到fir,要先安装fir-cli。Github: fir-cli

    1
    gem install -n /usr/local/bin fir-cli 
  • fir token查看方法:
    -w459

  • 脚本

    1
    fir publish /Users/vic/.jenkins/workspace/jike-iOS/Export/Ruguo/Ruguo.ipa -T 4554bab16fc50b5b8ee20d9663fa64c7 --password=jikebeta

准备环境

配置Android ADB环境

关于Android整个开发环境的搭建,SDK的下载安装这里就不赘述了,配置adb环境可以参考、

这里查看adb是否配置成功可以在终端输入

1
adb version

显示以下信息则证明配置成功

1
2
3
Android Debug Bridge version 1.0.41
Version 28.0.3-5475833
Installed as /Users/vic/Library/Android/sdk/platform-tools/adb

配置Gradle环境

配置gradle环境可以看一下我之前写的这篇文章Mac 配置gradle环境变量.

这里查看gradle是否配置成功可以在终端输入

1
gradle -version

显示如下信息则表示配置成功
-w547

下载安装Jenkins

这里有两种方法可以用。参考Mac下Jenkins搭建

Jenkins环境变量的配置

左侧系统管理->系统设置,进入系统设置页面

全局属性下的环境变量,添加键值对
-w1284

左侧系统管理->全局工具设置,进入全局工具设置页面

1. JDK
    JDK installations一栏里面点击Add JDK,Name随便取,不过最好对应好版本号;JAVE_HOME就设置为当前Mac下Java的安装路径。当然也可以设置成自动安装

-w1268

2. Gradle
    gradle配置:一样名字最好跟着版本号,GRADLE_HOME就设置为gradle的安装目录

-w1294

项目配置

构建一个自由风格的软件项目

点击左侧菜单中的新建任务,然后在新的界面输入项目名字,选择构建一个自由风格的软件项目选项完成创建
-w1020

项目基本配置

点击左边的配置,从上到下一共6个tab,我们去一一配置自己想要的功能:

General

  • 描述里面我们可以写工程简介

  • “This project is parameterized”参数化构建
    -w931

  • “Discard old builds”丢弃旧的仓库
    -w920

源码管理

  • 源码管理,使用github选择git,使用svn选择subversion
    -w949

  • 点击add以后,进入选择身份验证的页面,没有ssh key的,选择用户名和密码,有ssh key的选择ssh验证(注意,这里填写的是private key,id_rsa文件中的key,不是id_rsa.pub中的key),填写完成后,点击add即可完成身份验证的添加。然后选择对应的身份验证
    -w1250

  • 项目的分支,填写你需要的分支

构建触发器

  • 选择Build periodically,输入:

    1
    H 2 * * *

    周期性的执行编译任务,强制每天凌晨两点构建一次
    -w945

  • 选择Poll SCM,输入

    1
    H/5 * * * *

    表示的就是每5分钟检查一次源码变化。
    -w934

    第一个参数代表的是分钟 minute,取值 059;
    第二个参数代表的是小时 hour,取值 0
    23;
    第三个参数代表的是天 day,取值 131;
    第四个参数代表的是月 month,取值 1
    12;
    最后一个参数代表的是星期 week,取值 0~7,0 和 7 都是表示星期天。

构建环境

这里可以设置一些项目信息,比如在构建之前删除工作目录,设置构建名称等等。。。(但是我没有设置)

构建

-w299

  • 增加构建步骤,选择Invoke Gradle script(编译)
    -w948

    • Gradle选择你配置的版本
    • Task中填写构建执行的语句(注意:不要选择Use Gradle Wrapper),一般要填写“clean assembleRelease”。先clean再build
      1
      2
      clean
      assembleReleaseChannels -PchannelFile=markets.txt
    • 在Root Build script中输入你想要包生成的位置,在Build File中输入项目的build.gradle的路径。即在Root Build script路径的后边加/build.gradle,可以不输入这两项,项目会有个默认构建地址,具体地址可以再jenkins的系统管理中查看。
  • 继续添加构建步骤,选择“Execute shell”,在command中填入(上传蒲公英)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ./gitswitch dev/ci
    cd Source

    filepath=`pwd`/app/build/outputs/channels/*pgy*.apk
    file=`ls $filepath`
    if [ $uploadPgy = true ]
    then
    curl -F "installType=2" -F "password=alpha" -F "file=@$file" -F "uKey=542e73b113b5908da8b028805bf7e837" -F "_api_key=8e860ee5ba4996f9b19e56c28e07846a" https://qiniu-storage.pgyer.com/apiv1/app/upload
    fi

构建后操作

Archive the artifacts(归档成品),输入内容

1
**/app/build/outputs/channels/*.apk, **/apk-checker-result.*

上传蒲公英

增加一个构建步骤,选择Execute shell,在command中填入

1
2
3
4
5
6
7
8
9
./gitswitch dev/ci
cd Source

filepath=`pwd`/app/build/outputs/channels/*pgy*.apk
file=`ls $filepath`
if [ $uploadPgy = true ]
then
curl -F "installType=2" -F "password=alpha" -F "file=@$file" -F "uKey=542e73b113b5908da8b028805bf7e837" -F "_api_key=8e860ee5ba4996f9b19e56c28e07846a" https://qiniu-storage.pgyer.com/apiv1/app/upload
fi

蒲公英官方文档地址https://www.pgyer.com/doc/api#uploadApp

调试

构建历史->控制台输出,可以查看构建日志,根据报错来具体定位问题
-w370
-w1064

Mac Jenkins搭建

这里有两种方法可以用。第一种操作方便,可以直接下载安装包,但是安装包会有一些莫名的问题。第二种操作繁琐一点,需要操作命令行,直接命令行安装:

直接登录官网下载安装:(不推荐)

jenkins的官网下载地址:https://jenkins.io/download/
因为这里是Mac下面所以需要选择MacOS的版本:(左边是稳定版本,右边是测试版本,所以一般都是选择左边的)
Jenkins官网
下载完成以后直接点击安装就可以了。之后在浏览器输入链接:http://localhost:8080 就行

brew下载安装(推荐使用):

  • 安装安装brew(如果已安装跳过)

    1
    /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)”
  • 安装Jenkins

    1
    brew install jenkins
  • 查找jenkins war地址

    1
    2
    3
    4
    #在安装后有可能找不到war包地址,可以用mdfind命令本地查询下

    mdfind -name jenkins.war
    #/usr/local/Cellar/jenkins/2.174/libexec/jenkins.war
  • 启动

    1
    java -jar /usr/local/Cellar/jenkins/2.116/libexec/jenkins.war --httpPort=8080
  • 后台启动

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    后台启动的意思是放到后台运行,即使窗口被干掉,jenkins的进程也会存在.

    简单用shell脚本nohup命令放到后台

    启动脚本命令:sh startjenkins.sh

    #!/bin/bash
    echo start jenkins

    nohup java -jar /usr/local/Cellar/jenkins/2.116/libexec/jenkins.war --httpPort=8080 &

    echo start end

Jenkins的环境配置

启动进入

启动成功就可以在浏览器输入http://localhost:8080后
Jenkins官网
可以通过红色路径找到文件打开查看密码,Mac下面路径这个

1
/Users/你的Mac用户名/.jenkins/secrets/initialAdminPassword

密码:
Jenkins官网

插件安装

输入密码进入插件安装的页面,分别是“安装建议的插件”和“选择插件进行安装”,这里我选择左侧的选项(“安装建议的插件”),然后等待自动安装完成
如果提示install failure,右下角有个retry,重试一下就好了

创建第一个管理员账户

Jenkins官网
填写完表格,点击save and finish,jenkins的初始配置就完成啦
然后要重启一下,填入刚才输入的admin账号密码才能登录进去

jenkins常用操作

1
2
3
4
5
6
7
8
9
10
访问:http://localhost:8080/login?from=%2F

退出:http://localhost:8080/exit

重启:http://localhost:8080/restart

重新加载:http://localhost:8080/reload

#如果你不想/不需要后台服务,你可以运行:
brew services start jenkins

简介

uiautomator2是一个python库,用于Android的UI自动化测试,其底层基于Google uiautomator,Google提供的uiautomator库可以获取屏幕上任意一个APP的任意一个控件属性,并对其进行任意操作

支持平台及语言你在

python-uiautomator2封装了谷歌自带的uiautomator2测试框架,提供便利的python接口。他允许测试人员直接在PC上编写Python的测试代码,操作手机应用,完成自动化,大大提高了自动化代码编写的效率。

工作原理

工作原理
如图所示,python-uiautomator2主要分为两个部分,python客户端,移动设备

  • python端: 运行脚本,并向移动设备发送HTTP请求
  • 移动设备:移动设备上运行了封装了uiautomator2的HTTP服务,解析收到的请求,并转化成uiautomator2的代码。

整个过程

  1. 在移动设备上安装atx-agent(守护进程), 随后atx-agent启动uiautomator2服务(默认7912端口)进行监听
  2. 在PC上编写测试脚本并执行(相当于发送HTTP请求到移动设备的server端)
  3. 移动设备通过WIFI或USB接收到PC上发来的HTTP请求,执行制定的操作

安装

安装adb

如命令行可以执行adb devices,则跳过此步骤
从谷歌官网下载Android Platform Tools https://developer.android.com/studio/releases/platform-tools.html, 解压,并加包含adb.exe的目录加入到系统的PATH中。

安装uiautomator2

1
pip install --upgrade --pre uiautomator2

如果你需要用到截图,安装pillow

1
pip install pillow

设备安装atx-agent

首先设备连接到PC,并能够adb devices发现该设备。执行下面的命令会自动安装本库所需要的设备端程序:uiautomator-server,atx-agent,openstf / minicap,openstf / minitouch

1
2
3
4
5
6
7
8
# init就是所有USB连接电脑的手机上都安装uiautomator2
python -m uiautomator2 init

# 指定手机安装uiautomator2, 用 --mirror
python -m uiautomator2 init --mirror --serial $SERIAL

# 嫌弃慢的话,可以用国内的镜像
python -m uiautomator2 init --mirror

最后提示success,代表atx-agent初始化成功。

安装weditor

有了这个,方便我们快速的识别手机上的元素,方便写代码

1
pip install --pre -U weditor#pip install --pre weditor

Windows系统可以使用命令在桌面创建一个快捷方式 python -m weditor –shortcut

启动方法:

1
python -m weditor 

浏览器会自动打开一个网页 http://atx.open.netease.com,看到如下界面
weditor

重点说下这个部分
weditor
如何与手机连接起来呢?就是通过手机序列号或IP来实现的。打开cmd,输入adb devices,查看手机序列号,输入框中填写设备的IP或者设备的Serial(序列号)。之后点击Connect,如果一切正常就会出现一个绿色的叶子
weditor
点击蓝色的Reload,就可以在网页上看到手机的界面了。非常的强大。我们可以看到手机元素的各种定位方法,在UI自动化写代码的时候,真的是非常的方面,手机页面和weditor是实时同步的。

【推荐】AppetizerIO 所见即所得脚本编辑器

AppetizerIO 提供了对uiautomator2的深度集成,可以图形化管理ATX设备,还有所见即所得脚本编辑器
*到网站下载直接打开,首次使用需要注册账号
* 设备管理 界面里可以检查设备是否正常init,起停atx-agent,抓取atx-agent.log文件
* APP测试->脚本助手调出脚本助手,实时界面同步,点击界面直接插入各种代码,同时支持uiautomator和Appium
* 视频教程 请戳这里 其他文档在此

应用及操作

调用uiautomator2的过程

  1. 配置手机设备参数,设置具体操作的是哪一台手机
  2. 抓取手机上应用的控件,制定对应的控件来进行操作
  3. 抓取手机上应用的控件,制定对应的控件来进行操作抓取手机上应用的控件,制定对应的控件来进行操作
    对抓取到的控件进行操作,比如点击、填写参数等。

设备连接方法,有两种

python-uiautomator2连接手机的方式有两种,一种是通过WIFI,另外一种是通过USB。两种方法各有优缺点。
WIFI最便利的地方要数可以不用连接数据线,USB则可以用在PC和手机网络不在一个网段用不了的情况。

  1. 通过WiFi,假设设备IP 192.168.5.4和您的PC在同一网络中
    1
    2
    import uiautomator2 as u2
    d = u2.connect('192.168.5.4') # WIFI链接设备。或者u2.connect_wifi('10.0.0.1')
  2. 通过USB, 假设设备序列是123456789F(见adb devices)
    1
    2
    3
    import uiautomator2 as u2
    d = u2.connect('123456789F') # USB链接设备。或者u2.connect_usb('123456f')
    #d = u2.connect_usb()#当前只有一个设备时可以用这个
    在没有参数的情况下调用u2.connect(), uiautomator2将从环境变量ANDROID_DEVICE_IP获取设备IP。如果这个环境变量是空的,uiautomator将返回connect_usb,您需要确保只有一个设备连接到计算机。

    检查并维持设备端守护进程处于运行状态

    1
    d.healthcheck()

    停用UiAutomator的守护程序?

    1
    d.service('uiautomator').stop()

    打开调试开关

    1
    2
    3
    4
    5
    6
    7
    8
    9
    d.debug = True
    d.info
    '''
    返回
    12:32:47.182 $ curl -X POST -d '{"jsonrpc": "2.0", "id": "b80d3a488580be1f3e9cb3e926175310", "method": "deviceInfo", "params": {}}' 'http://127.0.0.1:54179/jsonrpc/0'
    12:32:47.225 Response >>>
    {"jsonrpc":"2.0","id":"b80d3a488580be1f3e9cb3e926175310","result":{"currentPackageName":"com.android.mms","displayHeight":1920,"displayRotation":0,"displaySizeDpX":360,"displaySizeDpY":640,"displayWidth":1080,"productName"
    :"odin","screenOn":true,"sdkInt":25,"naturalOrientation":true}}
    '''

    安装应用,只能从URL安装

    1
    d.app_install('http://some-domain.com/some.apk')#引号内为下载apk地址

    启动应用

    1
    d.app_start('com.ruguoapp.jike')#引号内为包名称

    停止应用

    1
    2
    3
    4
    #相当于'am force-stop'强制停止应用
    d.app_stop('com.example.hello_world')
    #相当于'pm clear' 清空App数据
    d.app_clear('com.example.hello_world')

    停止所有正在运行的应用程序

    1
    2
    3
    4
    # 停止所有
    d.app_stop_all()
    # 停止所有应用程序,除了com.examples.demo
    d.app_stop_all(excludes=['com.examples.demo'])

跳过弹窗,禁止弹窗

1
2
d.disable_popups()#自动跳过弹出窗口 
d.disable_popups(假)#禁用自动跳过弹出窗

Session

Session表示应用程序的生命周期。可用于启动应用,检测应用崩溃

  • 启动和关闭应用程序

    1
    2
    sess = d.session("com.netease.cloudmusic") # start 网易云音乐
    sess.close() # 停止网易云音乐
  • 使用python with启动和关闭应用程序

    1
    2
    with d.session("com.netease.cloudmusic") as sess:
    sess(text="Play").click()
  • 链接正在运行的应用

    1
    sess = d.session(“ com.netease.cloudmusic ”,attach = True
  • 检测应用崩溃

    1
    2
    3
    4
    5
    6
    # App正在运行时
    sess(text="Music").click() # 操作是正常的

    # App崩溃时
    sess(text="Music").click() # 引发会话中断错误SessionBrokenError
    # session下的其他函数调用也会引发SessionBrokenError错误
    1
    2
    3
    # 检查会话是否正确。
    # 警告:函数名将来可能会更改
    sess.running() # True or False

    获取设备信息

  • 获取基本信息

    1
    d.info

    以下是可能的输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    { 
    u'displayRotation': 0,
    u'displaySizeDpY': 640,
    u'displaySizeDpX': 360,
    u'currentPackageName': u'com.android.launcher',
    u'productName': u'takju',
    u'displayWidth': 720,
    u'sdkInt': 18,
    u'displayHeight': 1184,
    u'naturalOrientation': True
    }
  • 获取窗口大小

    1
    2
    3
    d.window_size()
    # 设备垂直输出示例: (1080, 1920)
    # 设备水平输出示例: (1920, 1080)
  • 获取当前应用程序信息。对于某些android设备,输出可以为空(参见输出示例3)

    1
    2
    3
    4
    d.current_app()
    # 输出示例 1: {'package': 'com.netease.example', 'activity': '.Client', 'pid': 23710}
    # 输出示例 2: {'package': 'com.ruguoapp.jike', 'activity': 'com.ruguoapp.jike.business.video.ui.activity.videolist.VideoListActivity'}
    # 输出示例 3: {'package': None, 'activity': None}
  • 获取设备序列号

    1
    2
    d.serial
    # 输出示例: 74aAEDR428Z9
  • 获取WIFI IP

    1
    2
    print(d.wlan_ip)
    #输出示例:10.0.0.1
  • 获取详细的设备信息

    1
    print(d.device_info)

    下面是一个可能的输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    {'udid': '3578298f-b4:0b:44:e6:1f:90-OD103',
    'version': '7.1.1',
    'serial': '3578298f',
    'brand': 'SMARTISAN',
    'model': 'OD103',
    'hwaddr': 'b4:0b:44:e6:1f:90',
    'port': 7912,
    'sdk': 25,
    'agentVersion': 'dev',
    'display': {'width': 1080, 'height': 1920},
    'battery': {'acPowered': False,
    'usbPowered': False,
    'wirelessPowered': False,
    'status': 3,
    'health': 0,
    'present': True,
    'level': 99,
    'scale': 100,
    'voltage': 4316,
    'temperature': 272,
    'technology': 'Li-ion'},
    'memory': {'total': 3690280, 'around': '4 GB'},
    'cpu': {'cores': 8, 'hardware': 'Qualcomm Technologies, Inc MSM8953Pro'},
    'presenceChangedAt': '0001-01-01T00:00:00Z',
    'usingBeganAt': '0001-01-01T00:00:00Z'}

    获取应用信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    d.app_info("com.examples.demo")
    # 会输出
    #{
    # "mainActivity": "com.github.uiautomator.MainActivity",
    # "label": "ATX",
    # "versionName": "1.1.7",
    # "versionCode": 1001007,
    # "size":1760809
    #}
    # 保存应用程序图标
    img = d.app_icon("com.examples.demo")
    img.save("icon.png")

推拉文件

  • 将文件推送到设备

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # push文件夹
    d.push("foo.txt", "/sdcard/")
    # push和重命名
    d.push("foo.txt", "/sdcard/bar.txt")
    # push fileobj
    with open("foo.txt", 'rb') as f:
    d.push(f, "/sdcard/")
    # 推动和更改文件访问模式
    d.push("foo.sh", "/data/local/tmp/", mode=0o755)
  • 从设备中拉出一个文件

    1
    2
    3
    4
    d.pull("/sdcard/tmp.txt", "tmp.txt")

    # 如果在设备上找不到文件,FileNotFoundError将引发
    d.pull("/sdcard/some-file-not-exists.txt", "tmp.txt")

关键事件

  • 打开/关闭屏幕
    1
    2
    d.screen_on()#打开屏幕 
    d.screen_off()#关闭屏幕
  • 获取当前屏幕状态
    1
    d.info.get(' screenOn '#需要 Android> = 4.4
  • 硬键盘和软键盘操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    d.press("home") # 点击home键
    d.press("back") # 点击back键
    d.press("left") # 点击左键
    d.press("right") # 点击右键
    d.press("up") # 点击上键
    d.press("down") # 点击下键
    d.press("center") # 点击选中
    d.press("menu") # 点击menu按键
    d.press("search") # 点击搜索按键
    d.press("enter") # 点击enter键
    d.press("delete") # 点击删除按键
    d.press("recent") # 点击近期活动按键
    d.press("volume_up") # 音量+
    d.press("volume_down") # 音量-
    d.press("volume_mute") # 静音
    d.press("camera") # 相机
    d.press("power") #电源键
    你可以在 Android KeyEvnet上找到所有的关键代码定义
  • 解锁屏幕
    1
    2
    3
    4
    d.unlock()
    # 相当于
    # 1. 发射活动:com.github.uiautomator.ACTION_IDENTIFY
    # 2. 按home键

    手势与设备的交互

  • 单击屏幕
    1
    d.click(x,y)
  • 双击屏幕
    1
    2
    d.double_click(x,y)
    d.double_click(X,Y,0.1)#默认两个单击之间间隔时间为0.1
  • 长按
    1
    2
    d.long_click(x,y)
    d.long_click(X,Y,0.5#长按0.5秒(默认)
  • 滑动
    1
    2
    d.swipe(sx, sy, ex, ey)
    d.swipe(sx, sy, ex, ey, 0.5) #滑动0.5s(default)
  • 拖动
    1
    2
    d.drag(sx, sy, ex, ey)
    d.drag(sx, sy, ex, ey, 0.5)#拖动0.5s(default)
  • 滑动点 多用于九宫格解锁,提前获取到每个点的相对坐标(这里支持百分比),
    更详细的使用参考这个帖子 使用u2实现九宫图案解锁
    1
    2
    3
    #从点(x0, y0)滑到点(x1, y1)再滑到点(x2, y2)
    #两点之间的滑动速度是0.2秒
    d.swipe((x0, y0), (x1, y1), (x2, y2), 0.2)
    注意:单击,滑动,拖动操作支持百分比位置值。例:
    1
    d.long_click(0.5, 0.5) 表示长按屏幕中心

    屏幕相关的

  • 检索方向
    1
    2
    d.orientation
    # 检索方向。输出可以是 "natural" or "left" or "right" or "upsidedown"
  • 设置方向
    1
    2
    3
    4
    d.set_orientation('l') # or "left"
    d.set_orientation("l") # or "left"
    d.set_orientation("r") # or "right"
    d.set_orientation("n") # or "natural"
  • 冻结/ 开启旋转
    1
    2
    d.freeze_rotation()# 冻结旋转
    d.freeze_rotation(False)# 开启旋转
  • 截图
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 截图并保存到电脑上的一个文件中,需要Android>=4.2。
    d.screenshot("home.jpg")

    # 得到PIL.Image格式的图像. 但你必须先安装pillow
    image = d.screenshot() # default format="pillow"
    image.save("home.jpg") # 或'home.png',目前只支持png 和 jpg格式的图像

    # 得到OpenCV的格式图像。当然,你需要numpy和cv2安装第一个
    import cv2
    image = d.screenshot(format='opencv')
    cv2.imwrite('home.jpg', image)

    # 获取原始JPEG数据
    imagebin = d.screenshot(format='raw')
    open("some.jpg", "wb").write(imagebin)
  • 转储UI层次结构
    1
    2
    # get the UI hierarchy dump content (unicoded).(获取UI层次结构转储内容)
    d.dump_hierarchy()
  • 打开通知或快速设置
    1
    2
    d.open_notification()#下拉打开通知栏
    d.open_quick_settings()#下拉打开快速设置栏

    选择Ui对象

    选择器是一种方便的机制,用于在当前窗口中标识特定的UI对象。
    1
    2
    #选择带有文本'Clock'的对象,它的类名是'android.widget.TextView'
    d(text='Clock', className='android.widget.TextView')

选择器支持以下参数。有关详细信息,请参阅 UiSelector Java doc for detailed information.

  • text, textContains, textMatches, textStartsWith
  • className, classNameMatches
  • description, descriptionContains, descriptionMatches, descriptionStartsWith
  • checkable, checked, clickable, longClickable
  • scrollable, enabled,focusable, focused, selected
  • packageName, packageNameMatches
  • resourceId, resourceIdMatches
  • index, instance

##获取所选ui对象状态及其信息

  • 检查特定的UI对象是否存在
    1
    2
    3
    4
    d(text="Settings").exists # 返回布尔值,如果存在则为True,否则为False
    d.exists(text="Settings") # 另一种写法
    #高级用法
    d(text="Settings").exists(timeout=3) # 等待'Settings'在3秒钟出现
  • 获取特定UI对象的信息
    1
    d(text="Settings").info
    下面是一个可能的输出
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    {
    'bounds': {
    'bottom': 2744,
    'left': 386,
    'right': 478,
    'top': 2679},
    'childCount': 0,
    'className': 'android.widget.TextView',
    'contentDescription': None,
    'packageName': 'com.ruguoapp.jike',
    'resourceName': 'com.ruguoapp.jike:id/tv_main_tab_title',
    'text': '动态',
    'visibleBounds': {
    'bottom': 2744,
    'left': 386,
    'right': 478,
    'top': 2679},
    'checkable': False,
    'checked': False,
    'clickable': False,
    'enabled': True,
    'focusable': False,
    'focused': False,
    'longClickable': False,
    'scrollable': False,
    'selected': False
    }
  • 获取/设置/清除可编辑字段的文本(例如EditText小部件)
    1
    2
    3
    d(text="Settings").get_text()  #得到文本小部件
    d(text="Settings").set_text("My text...") #设置文本
    d(text="Settings").clear_text() #清除文本
  • 获取Widget中心点
    1
    2
    d(text="Settings").center()
    #d(text="Settings").center(offset=(0, 0)) # 基准位置左前

对选定UI对象的操作

  • UI对象有五种定位方式
    text、resourceId、description、className、xpath、坐标

  • 执行单击UI对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #text定位单击
    d(text="Settings").click()
    d(text="Settings", className="android.widget.TextView").click()
    #resourceId定位单击
    d(resourceId="com.ruguoapp.jike:id/tv_title", className="android.widget.TextView").click()
    #description定位单击
    d(description="设置").click()
    d(description="设置", className="android.widget.TextView").click()
    #className定位单击
    d(className="android.widget.TextView").click()
    #xpath定位单击
    d.xpath("//android.widget.FrameLayout[@index='0']/android.widget.LinearLayout[@index='0']").click()
    #坐标单击
    d.click(182, 1264)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 等待元素出现(最多10秒),出现后单击 
    d(text="Settings").click(timeout=10)
    # 在10秒时点击,默认的超时0
    d(text='Skip').click_exists(timeout=10.0)
    # 单击直到元素消失,返回布尔
    d(text="Skip").click_gone(maxretry=10, interval=1.0) # maxretry默认值10,interval默认值1.0
    # 点击基准位置偏移
    d(text="Settings").click(offset=(0.5, 0.5)) # 点击中心位置,同d(text="Settings").click()
    d(text="Settings").click(offset=(0, 0)) # 点击左前位置
    d(text="Settings").click(offset=(1, 1)) # 点击右下
  • 执行双击UI对象

    1
    2
    d(text="设置").double_click() #双击特定ui对象的中心
    d.double_click(x, y, 0.1)#两次单击之间的默认持续时间为0.1秒
  • 执行长按UI对象

    1
    2
    3
    # 长按特定UI对象的中心
    d(text="Settings").long_click()
    d.long_click(x, y, 0.5) # 长按坐标位置0.5s默认

    ##特定UI对象的手势操作

  • 将UI对象拖向另一个点或另一个UI对象

    1
    2
    3
    4
    5
    # Android<4.3不能使用drag.
    # 在0.5秒内将UI对象拖到屏幕点(x, y)
    d(text="Settings").drag_to(x, y, duration=0.5)
    # 将UI对象拖到另一个UI对象的中心位置,时间为0.25秒
    d(text="Settings").drag_to(text="Clock", duration=0.25)
  • ==未懂==从UI对象的中心向其边缘滑动
    滑动支持4个方向:左、右、上、下

    1
    2
    3
    4
    5
    6
    7
    8
       d(text="Settings").swipe("right")
    d(text="Settings").swipe("left", steps=10)
    d(text="Settings").swipe("up", steps=20) # 1步约为5ms, 20步约为0.1s
    d(text="Settings").swipe("down", steps=20)
    ```
    - ==未懂==两点手势操作,从一个点到另一个点
    ```python
    d(text="Settings").gesture((sx1, sy1), (sx2, sy2), (ex1, ey1), (ex2, ey2))
  • 特定UI对象的手势操作
    支持两种手势:从边缘到中心、从中心到边缘

    1
    2
    3
    4
    5
    #注意:缩放要到安卓4.3才能设置。
    #从边缘到中心
    d(text="Settings").pinch_in(percent=100, steps=10)
    # 从中心到边缘
    d(text="Settings").pinch_out()
  • 等待,直到特定的UI出现或消失

    1
    2
    3
    4
    # 等待ui对象出现
    d(text="Settings").wait(timeout=3.0) # 返回布尔值
    # 等待ui对象的消失
    d(text="Settings").wait_gone(timeout=1.0)

    默认超时为20秒。有关详细信息,请参阅全局设置

  • ==未懂==对特定的ui对象执行投掷(可滚动)
    可能的属性

    1. horiz(水平) 或 vert(垂直)
    2. forward(向前)或backward(向后)或toBeginning(开始位置)或toEnd(结束位置)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #向前投掷(默认)垂直(默认)
      d(scrollable=True).fling()
      #垂直向后滚动
      d(scrollable=True).fling.vert.forward()
      #没搞懂
      d(scrollable=True).fling.vert.backward()
      #没搞懂
      d(scrollable=True).fling.horiz.toBeginning(max_swipes=1000)
      #滚动到结束
      d(scrollable=True).fling.toEnd()
  • ==未懂==在特定的ui对象上执行滚动(可滚动)
    可能的属性

    1. horiz(水平) 或 vert(垂直)
    2. forward(向前)或backward(向后)或toBeginning(开始位置)或toEnd(结束位置)或 to
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      # 向前滚动(默认)垂直(默认)
      d(scrollable=True).scroll(steps=10)
      # 水平向前滚动
      d(scrollable=True).scroll.horiz.forward(steps=100)
      #垂直向后滚动
      d(scrollable=True).scroll.vert.backward()
      #滚动到开始水平
      d(scrollable=True).scroll.horiz.toBeginning(steps=100, max_swipes=1000)
      # 滚动到垂直结束
      d(scrollable=True).scroll.toEnd()
      #垂直向前滚动,直到出现特定的ui对象
      d(scrollable=True).scroll.to(text="Security")

Watcher(==未懂==)

当选择器没有找到匹配项时,可以注册watchers 来执行一些操作。

  • 注册 Watcher

    当选择器找不到匹配项时,uiautomator2将运行所有注册的watchers.
    当条件匹配时单击目标

    1
    2
    3
    4
    5
    d.watcher("AUTO_FC_WHEN_ANR").when(text="ANR").when(text="Wait") \
    .click(text="Force Close")
    # d.watcher(name) ## 创建一个新的名为watcher的程序。
    # .when(condition) ## 监视程序的用户选择条件。
    # .click(target) ## 对目标UiSelector执行单击操作
  • 还有一个关于点击的技巧。您可以不带参数地使用click。

    1
    2
    3
    d.watcher("ALERT").when(text="OK").click()
    # 一样
    d.watcher("ALERT").when(text="OK").click(text="OK")
  • 当条件为真时按下键

    1
    2
    3
    4
    5
    d.watcher("AUTO_FC_WHEN_ANR").when(text="ANR").when(text="Wait") \
    .press("back", "home")
    # d.watcher(name) ## 创建一个新的名为watcher的程序
    # .when(condition) ## 监视程序的用户选择条件
    # .press(<keyname>, ..., <keyname>.() ## 按顺序按下一个键
  • 检查指定的监视程序是否触发
    一个监视程序被触发,这意味着运行了监视程序并匹配了它的所有条件。

    1
    2
    d.watcher("watcher_name").triggered
    # 如果指定的监视程序被触发,则为true,否则为false
  • 删除指定的监视程序

    1
    2
    # 删除观察者
    d.watcher("watcher_name").remove()
  • 列出所有观察者

    1
    2
    d.watchers
    #列出所有注册观察员的名单
  • 检查任何触发的监视程序

    1
    2
    d.watchers.triggered
    # 在任何监视程序触发时为真
  • 重置所有触发观察者

    1
    2
    # 重置所有触发的观察者,然后是d.观察者。触发将为false。
    d.watchers.reset()
  • 删除观察者

    1
    2
    3
    4
    # 删除所有注册观察者
    d.watchers.remove()
    # 删除指定的监视程序,与d.watcher(“watcher_name”)相同。
    d.watchers.remove("watcher_name")
  • 强制运行所有的观察者

    1
    2
    # 强制运行所有注册的观察者
    d.watchers.run()
  • 当页面更新时运行所有观察者。
    通常可以用来自动点击权限确认框,或者自动安装
    1
    2
    3
    4
    5
    6
    7
    8
    9
    d.watcher("OK").when(text="OK").click(text="OK")
    # 启用自动触发监视程序
    d.watchers.watched = True

    # 禁用自动触发监视程序
    d.watchers.watched = False

    # 获取当前触发器监视者状态
    assert d.watchers.watched == False
    另外文档还是有很多没有写,推荐直接去看源码init.py

全局设置

1
2
3
4
5
# 设置每次UI点击后1.5秒的延迟
d.click_post_delay = 1.5 # 默认没有延迟

# 设置默认元素等待超时(秒)
d.wait_timeout = 30.0 # 默认的20.0

UiAutomator中的超时设置(隐藏方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
>> d.jsonrpc.getConfigurator() 
{'actionAcknowledgmentTimeout': 500,
'keyInjectionDelay': 0,
'scrollAcknowledgmentTimeout': 200,
'waitForIdleTimeout': 0,
'waitForSelectorTimeout': 0}

>> d.jsonrpc.setConfigurator({"waitForIdleTimeout": 100})
{'actionAcknowledgmentTimeout': 500,
'keyInjectionDelay': 0,
'scrollAcknowledgmentTimeout': 200,
'waitForIdleTimeout': 100,
'waitForSelectorTimeout': 0}

为了防止客户端程序响应超时,waitForIdleTimeoutwaitForSelectorTimeout目前已改为0

Refs: Google uiautomator Configurator

Input method

这种方法通常用于不知道控件的情况下的输入。第一步需要切换输入法,然后发送adb广播命令,具体使用方法如下

1
2
3
4
5
d.set_fastinput_ime(True) # 切换成FastInputIME输入法
d.send_keys("你好123abcEFG") # adb广播输入
d.clear_text() # 清除输入框所有内容(Require android-uiautomator.apk version >= 1.0.7)
d.set_fastinput_ime(False) # 切换成正常的输入法
d.send_action("search") # 模拟输入法的搜索

send_action 说明

该函数可以使用的参数有 go search send next done previous

什么时候该使用这个函数呢?

有些时候在EditText中输入完内容之后,调用press("search") or press("enter")发现并没有什么反应。
这个时候就需要send_action函数了,这里用到了只有输入法才能用的IME_ACTION_CODE
send_action先broadcast命令发送给输入法操作IME_ACTION_CODE,由输入法完成后续跟EditText的通信。(原理我不太清楚,有了解的,提issue告诉我)

Toast

  • 展示Toast

    1
    2
    d.toast.show("Hello world")
    d.toast.show("Hello world", 1.0) # 显示为1.0,默认为1.0
  • 获取 Toast

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # [参数]
    # 5.0: 最大等待超时。默认的10.0
    # 缓存时间10.0s。如果最近10s已经出现toast,则返回缓存toast。默认10.0(将来可能会有变化)
    # 如果最终没有toast,返回"default message"。默认没有
    d.toast.get_message(5.0, 10.0, "default message")

    # 常见的使用
    assert "Short message" in d.toast.get_message(5.0, default="")

    #清楚缓存toast
    d.toast.reset()
    # Now d.toast.get_message(0) is None

    XPath

  • 例如: 其中一个节点的内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <android.widget.TextView
    index="2"
    text="05:19"
    resource-id="com.netease.cloudmusic:id/qf"
    package="com.netease.cloudmusic"
    content-desc=""
    checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false"
    scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true"
    bounds="[957,1602][1020,1636]" />
  • xpath定位和使用方法
    有些属性的名字有修改需要注意

    1
    2
    description -> content-desc
    resourceId -> resource-id
  • 常见用法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 等待10s
    d.xpath("//android.widget.TextView").wait(10.0)
    # 找到并单击
    d.xpath("//*[@content-desc='分享']").click()
    # 检查是否存在
    if d.xpath("//android.widget.TextView[contains(@text, 'Se')]").exists:
    print("exists")
    # 获取所有文本视图文本、属性和中心点
    for elem in d.xpath("//android.widget.TextView").all():
    print("Text:", elem.text)
    #获取视图文本
    for elem in d.xpath("//android.widget.TextView").all():
    print("Attrib:", elem.attrib)
    #获取属性和中心点
    #返回: (100, 200)
    for elem in d.xpath("//android.widget.TextView").all():
    print("Position:", elem.center())
  • 其他XPath常见用法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 所有元素
    //*

    # resource-id包含login字符
    //*[contains(@resource-id, 'login')]

    # 按钮包含账号或帐号
    //android.widget.Button[contains(@text, '账号') or contains(@text, '帐号')]

    # 所有ImageView中的第二个
    (//android.widget.ImageView)[2]

    # 所有ImageView中的最后一个
    (//android.widget.ImageView)[last()]

    # className包含ImageView
    //*[contains(name(), "ImageView")]

前言:

SQL 是用于访问和处理数据库的标准的计算机语言。

什么是 SQL?

SQL 指结构化查询语言
SQL 使我们有能力访问数据库
SQL 是一种 ANSI 的标准计算机语言
编者注:ANSI,美国国家标准化组织

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#显示数据库
show databases;

#判断是否存在数据库test_mysql,有的话先删除
drop database if exists test_mysql;

#创建数据库
create database test_mysql;

#删除数据库
drop database test_mysql;

#使用该数据库
use test_mysql;

#显示数据库中的表
show tables;

#先判断表是否存在,存在先删除
drop table if exists student;

#创建表
create table student(
id int auto_increment primary key,
name varchar(50),
sex varchar(20),
date varchar(50),
)default charset=utf8;

#删除表
drop table student;

#查看表的结构
describe student; #可以简写为desc student;

#插入数据
insert into student values(null,'test','2018-10-2');

#查询表中的数据
select * from student;
select id,name from student;

#修改某一条数据
update student set name='jack' where id=4;

#删除数据
delete from student where id=8;

# and
select * from student where date>'2018-1-2' and date<'2018-12-1';

# or
select * from student where date<'2018-11-2' or date>'2018-12-1';

#between
select * from student where date between '2018-1-2' and '2018-12-1';

#in 查询制定集合内的数据
select * from student where id in (1,3,5);

#排序 asc 升序 desc 降序
select * from student order by id asc;

#分组查询 #聚合函数
select max(id),name,sex from student group by sex;

select min(date) from student;

select avg(id) as 'Avg' from student;

select count(*) from student; #统计表中总数

select count(sex) from student; #统计表中性别总数 若有一条数据中sex为空的话,就不予以统计~

select sum(id) from student;

#查询第i条以后到第j条的数据(不包括第i条)
select * from student limit 2,5; #显示3-5条数据

#修改数据
update student set name='test' where id=2;
update student set name='花花',sex='女' where id=2
delete from student where id=2;

#修改表的名字
#格式:alter table tbl_name rename to new_name
alter table student rename to test_1;

#向表中增加一个字段(列)
#格式:alter table tablename add columnname type;/alter table tablename add(columnname type);
alter table student add age varchar(20) set default '1'; #set default 设置默认值

#修改表中某个字段的名字
alter table tablename change columnname newcolumnname type; #修改一个表的字段名
alter table student change name test_name varchar(50);

#去掉表中字段age的默认值
alter table student alter age drop default;

#去掉表中字段age
alter table student drop column age;

#删除表中主键
alter table student drop primary key;

#表中增加主键
#alter table add primary key (column1,column2,....,column)
alter table student add primary key (student_id);

#用文本方式将数据装入数据库表中(例如D:/mysql.txt)
load data local infile "D:/mysql.txt" into table MYTABLE;

#导入.sql文件命令(例如D:/mysql.sql
source d:/mysql.sql; #或者 /. d:/mysql.sql;