GitHub-Page搭建个人博客

安装,建立和启动Hexo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 全局安装hexo-cli
npm install -g hexo-cli
# 安装插件
npm install hexo-renderer-ejs --save
npm install hexo-renderer-stylus --save
npm install hexo-renderer-marked --save
# 建立Hexo工程,并cd到博客目录里,安装相应的包
hexo init blog
cd blog
npm install
# 生成静态文件,public文件夹
hexo generate
# 启动Hexo
hexo server

修改主题(Fan)

https://hexo.io/themes/

https://github.com/fan-lv/Fan

1
2
3
4
5
6
7
8
9
10
11
12
13
# 拉取选择的主题
git clone https://github.com/fan-lv/Fan.git themes/Fan
# _config.yml中修改主题
# Extensions
## Plugins: https://hexo.io/plugins/
## Themes: https://hexo.io/themes/
theme: Fan
# 更新主题
cd themes\Fan
git pull
# 安装依赖
npm install --save hexo-renderer-pug hexo-renderer-stylus
npm install --save hexo-generator-feed hexo-generator-sitemap hexo-generator-archive hexo-browsersync

文章(Markdown 文件)设置


1
2
3
4
5
6
7
8
9
10
11
12
13
# 标题
title: xxx
# 作者
author: xxx
# 标签
tags:
- xxx
# 分类
categories:
- xxx
date: xxx
# 支持置顶
top: 1

部署到GitHub

打开blog目录下的_config.yml配置文件,改如下配置

1
2
3
4
5
6
7
# Deployment
## Docs: https://hexo.io/docs/one-command-deployment
deploy:
type: git
repo:
github: https://github.com/guobq/guobq.github.io
branch: master
1
2
3
4
# 安装插件
npm install --save hexo-deployer-git
# 部署到github
hexo deploy

Comments

_config.yml配置


1
2
3
4
5
6
7
8
9
10
11
12
13
# comments
comments:
gitment:
enable: true
owner: 'guobq'
repo: 'https://github.com/guobq/k8s'
client_id: 'a91e9ec426714f54a397'
client_secret: '614ed1b11be02220c9a08cd1e8c3fda3195d60a4'

# 可以自由配置 valine 的其他配置项(参考链接 https://valine.js.org/configuration.html)
valine:
appId: '8xUuEcUoXahLwm69LVK1eumF-gzGzoHsz'
appKey: 'NDF5dcJsxuEHHBOLlda7Y07b'

Gitment

获取client_id,client_secret

1.打开https://github.com/settings/developers

2.New OAuth APP

1
2
3
4
5
6
Homepage URL:
https://guobq.github.io/
#开发就是本地的地址加端口,生产就不用说了
Authorization callback URL:
https://guobq.github.io/
#开发就是本地的回调,这个概念不理解的话去学一下Oauth

3.生成Client ID,CLient Secret

Valine

LeanCloud的应用中得到的appId.

https://leancloud.cn/

登录或注册 LeanCloud, 进入控制台后点击左下角创建应用

进入刚刚创建的应用,选择左下角的设置>应用Key,然后就能看到你的APP ID和APP Key了

注册 Algolia账户,创建新的 Index,记下 index name

安装扩展

1
npm install hexo-algolia --save

修改 Algolia 搜索 ACL(访问控制列表) 默认的 Search-Only API Key 不能修改,需要在 All API Keys -> New API Key

勾选 ACLs: search addObject deleteObject listIndexes deletelndex

执行安装

1
2
3
export HEXO_ALGOLIA_INDEXING_KEY=New API Key
# 刷新索引
hexo algolia

获取 Key,更新站点根目录配置

1
2
3
4
algolia:
applicationID: 'Application ID'
apiKey: 'New API Key'
indexName: 'index name'
1
2
3
4
5
# algolia
algolia:
applicationID: '8ICB0LL54F'
apiKey: 'b6e01221a0e1bdb34d69e088b9bd39d8'
indexName: 'blog'

主题配置下开启 Algolia_search

1
2
3
4
algolia_search:
enable: true
hits:
per_page: 10

打开分类,标签,关于

切换到主目录

1
2
3
hexo new page categories
hexo new page tags
hexo new page about

./source/categories,tags,about目录下,会生成index.md

categories/index.md

1
2
3
4
5
layout: layout_single
title: 分类
date: 2020-09-09
type: categories
comments: false

tags/index.md

1
2
3
4
title: tags
date: 2020-09-09
type: "tags"
comments: false

about/index.md

自定义

生成永久文章链接

1.安装

插件源码地址: hexo-abbrlink

1
npm install hexo-abbrlink --save

可能会出现依赖,依据提示安装即可。

2.配置

修改博客根目录配置文件 _config.ymlpermalink

1
2
3
4
5
# permalink: :year/:month/:day/:title/
permalink: p/:abbrlink.html # p 是自定义的前缀
abbrlink:
alg: crc32 #算法: crc16(default) and crc32
rep: hex #进制: dec(default) and hex

不同算法和进制生成不同的格式:

1
2
3
4
5
6
7
8
crc16 & hex
66c8
crc16 & dec
65535
crc32 & hex
8ddf18fb
crc32 & dec
123456789

3.验证

先清理下本地的文件 hexo clean,然后重新生成 hexo g,启动博客 hexo s。该插件会在每篇文章的开头增加内容:

1
abbrlink: df27ccfb

这个字符串就是这篇文章的唯一标识,无论修改标题还是发布文章都不会改变。

参考

https://hexo.io/

https://valine.js.org/

https://blog.csdn.net/u011116672/article/details/51160742

https://blog.csdn.net/weixin_43870742/article/details/102004099

https://blog.csdn.net/qq_42595443/article/details/82466442?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.edu_weight&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.edu_weight

https://www.dazhuanlan.com/2019/11/03/5dbeaac238b49/?__cf_chl_jschl_tk__=53c7e61c6cb87976027ff79b485e7bc157d29a40-1602087870-0-AUbOpkBUnGVqTiIPnn1W8j-mk4RiIIk1-VQEcAnnN1JciituXSOs_o7b-405C3oJBLMXE-meOUymlyBwQ0f4jh7Cy3lAeQEblSmezSNOjfACC7iyIoYjP_ZSr0g2rpL-u8U6LeS93EJquJ0jbM7CYuks8Uk2vE6dnkMjTmaEA4peQcMPbE75lqujsB1Ax6lBPIa4dVYUD4iMoLE2BN6E-jVmJqJOb3X4SbtKRwSXMZP0-cPVpi46NOcvpSLgIr5vDKxDJHUd6PPgbsxy6Q-ywtneDqnTqZofb4wjdp-_FJaw_Z-cOWTnRWeFLEngd33Olg

阅读全文
效能与质量

效能与质量

研发阶段和效率价值金字塔

需求调研和评审>技术方案设计和评审>研发>测试

度量

考核千行代码Bug率的问题

代码行数<>价值

考核标准:Bug率数值越小就说明越好->尽量增大代码行数

考核阶段:Bug率的数据主要产出在研发阶段的后期,及提交测试后产出bug数->从项目的研发阶段和效率价值金字塔来看,其对项目的整体质量方面更多的聚焦在微观层面问题,整体的质量的影响范围会较小。

更合理的度量质量

  1. 需求的评审

  2. 架构设计方案评审

  3. 代码模块设计,包的依赖的规划,接口的设计的review

  4. 代码的review的机制

  5. 测试用例评审

  6. 使用代码检测工具,自动发现问题

过程评审是最有效也是成本最低的质量和效率保证和提升的手段。另外,过程评审还是迅速提高新人能力及其成果物的规范性的一个有效手段。

系统质量是要靠上游工程做出来的

上游的工作质量会更为重要,上游的问题的影响范围将更广,对效率和价值的影响更大,应该是我们重点关注的地方。仅仅依赖下游工程(种种测试)来把质量关,是十分低效,而且代价是非常昂贵的。

需求

需求管理

1.需求要被管理,被管理的意思又有两层:一是需求要被确认,二是要控制需求变更

2.需求要用来指导下游的工作产品,如:计划、设计、测试等

常见问题

1.因为项目进度赶等原因,在很多需求还没有明确情况下,便开始开发的工作。
2.开始客户只能提出模糊的需求,客户喜欢先让你做个东西给他看,然后他才可能逐渐提出真正的需求,而需求调研人员,对此没有什么好的处理办法。
3.客户以种种原因不签需求,项目组在不签需求的情况下,便开始开发工作。
4.客户不承认之前提出来的需求,项目组又不能得失客户,项目成员苦不堪言。
5.需求经常变化,无法控制。
6.设计、代码与需求不对应,特别是需求变更时,不知道应该修改哪部分,也不知道会有哪些影响

优秀的需求管理要素:

开发者应该理解需求

通过客户确认(记录)

需求变更管理(记录确认,影响清单确认)

需求双向跟踪维护(纵向:上下游工作产品之间的跟踪关系。横向:需求与需求之间的关系、设计与设计之前的关系、代码与代码之间的关系等)

需求与下游工作产品差异识别(需求变更时:双向跟踪表影响内容同步修改;编写写或者修改计划、设计、代码、测试计划、测试用例等需求下游工作产品时:要与需求保持一致)

实现

工具赋能:需求管理平台<->aido

需求开发

用系统的方法获取真正的全面的能实现的需求

常见问题

没有把握好客户需求,直接进入软件的细节,去讨论要做什么功能,界面要怎样设计去了,而忘记了软件的根本目的是为了解决什么问题。

明确客户需求后,就应该把客户需求转变成产品需求和产品组件需求,客户需求一般都是比较高层次的,而且描述也会比较简单,不能作为日后验收的标准,我们需要对软件的规格进行说明。一般来说,我们写的软件规格说明书都会包含产品需求和产品组件需求的。我们导出产品需求和产品组件需求的时候,要注意产品需求和产品组件需求,必须和客户需求对应起来,通常是多对多的关系。为什么要对应起来?我们要保证,软件的每一个界面,每一个功能都是有用的,都是“源自”客户需求的,这样才能保证我们做的事情都是正确的事情,防止被不相干的事情干扰。

客户需求

可以理解成客户为什么要做本系统,要解决什么问题,客户对系统有怎样的期望,希望能具备一些怎样的特点,简单的说,就是客户的需要是什么。

产品需求

是能满足客户需求,并对软件产品规格进行了详细描述的需求,软件设计师可以根据产品需求进行设计、编码等工作。

产品组件需求

是对产品需求的进一步细化,产品可能会分割成几个子系统、几个部分,每个子系统每部分要具备怎样的功能、要具备怎样的性能、接口要求等,这些可以认为是产品组件需求。

另外一个角度,需求可以分为功能性需求和非功能性需求两类,功能性需求就是系统具备怎样的功能,能做什么事情,而非功能性需求就是指系统要具备怎样的性能、安全级别等方面的要求。客户需求、产品需求和产品组件需求,都会包含功能需求和非功能需求。

优秀的需求开发要素:

干系人的需要、期望、约束和接口要求被收集并转化为客户需求

让客户能完整无遗漏准确地表达出他的想法:(原型、图示、类比、问卷)系统的目标、范围、解决什么问题、希望系统具备怎样的一些特性,满足一些什么接口要求和约束条件等

把客户原始的需求信息整理成正式的客户需求:通常会包括对系统目标、范围、解决问题、软件特性、接口要求等有详细的描述。

客户需求是精确和详细的,以用来开发产品需求和产品组件需求

产品和产品组件需求:对软件规格的描述,详细描述软件与用户是怎样交互的,用户需要输入什么,系统输出等都会比较详细描述出来。可做验收标准
分配需求给每一个产品组件:所有的需求应该与设计的产品组件对应,保证需求驱动后续的设计工作,同时也保证设计都是为需求服务

定义接口需求:包括系统与第三方的系统的接口要求,也包括系统本身各组件、各子系统、各部分之间的接口要求

需求被分析和确认,并定义出具体的功能性需求

操作场景与功能定义:描述出具体需求的操作场景、上下文,具体的操作步骤,对功能的详细描述等。通常我们可以通过序列图等来表达这些内容

分析需求:准确性、全面性、可实现性,平衡约束条件,确保需求符合最终的使用场景要求。通常是通过需求评审

开发

技术方案

常见问题

1)无设计文档
2)有设计文档,但形同虚设
3)设计时没有考虑可以重用以前项目或者第三方的代码或组件
4)没有用需求来驱动设计
5)设计没有考虑多过一个的方案
6)没有考虑清楚设计的原则和标准
7)设计的弹性不够、架构落后?
8)代码与设计脱节?
9)到处都是面条式代码

优秀的技术方案要素:

从候选方案中选择产品或者产品组件的解决方案

先考虑好我们设计方案的选择标准,并找出可能的候选方案

对产品的规格进行详细的表述,操作概念、场景、环境、操作模式和操作状态等

根据选择标准选出最佳方案

开发产品或者产品组件设计

概要设计:建立产品功能和框架,包含产品组成区块、产品组件界定、系统状态与模式、主要的内外部接口和界面设计(功能模块设计、数据库设计包括逻辑设计和物理设计及安全性能设计、模块接口和界面设计)

详细设计:完整定义产品组件的结构和功能,详细描述实现方法、算法

建立和维护技术数据包:(需求、设计资料等)为开发者提供开发产品或组件的综合性描述,以及产品架构描述、分配需求、产品组件的描述、产品相关生命周期过程描述、关键产品特性、必需的物理特征和约束、接口需求、用于确保实现需求的验证准则等。

设计合适的产品组件接口:考虑外部接口和内部接口;与原来系统的关系;

产品组件开发、购买或者重用评估:技术状况

实施产品组件的设计

编码:适当的标准与准则,走查,单元测试

用户文档:用户手册、安装手册、管理员手册、在线帮助等

产品集成

常见问题

时序

优秀的产品集成要素:

完成产品集成的准备工作

建立并维护产品组件的产品集成流程
建立并维护产品组件集成与评估标准
建立并维护产品组件的确认与交付标准

确保产品内部与外部的接口是兼容的

检查接口描述,保证覆盖性和完整性:通常通过评审接口说明。包括产品组件接口,产品集成所有环境的接口(定期)

管理产品和产品组件的内部和外部接口的定义、设计及变更:管理各组件之间的关系,保证组件间保持一致(配置库)

组合已集成的产品组件,并交付已集成、已验证、已确认的产品

在产品集成前,确定要集成的产品的产品组件已被确认、并依据其说明执行,并且确定产品集成接口符合接口说明

根据产品集成顺序和相关过程集成产品组件

评估产品组件的接口兼容性

打包组装已集成的产品或组件,并交付给适当的客户

实现

工具赋能:统一环境管理,持续集成,持续交付

阅读全文
WSL2使用

配置安装

环境要求

windows10版本大于20H1

升级工具链接:https://www.microsoft.com/zh-cn/software-download/windows10

启用适用于 Linux 的 Windows 子系统

1
2
#管理员
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

启用虚拟机功能

1
2
# 管理员
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

下载 Linux 内核更新包

https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi

将 WSL 2 设置为默认版本

1
wsl --set-default-version 2

安装linux子系统

Microsoft Store:https://aka.ms/wslstore

实现局域网访问

设置支持root远程登录

安装ssh服务

1
2
sudo apt-get remove openssh-server
sudo apt-get install openssh-server

Ubuntu设置root密码

1
sudo passwd root

修改/root/.profile文件

1
2
# mesg n || true
tty -s && mesg n || true

编辑配置文件/etc/ssh/sshd_config

1
2
3
4
Port 22 #默认即可,如果有端口占用可以自己修改
PasswordAuthentication yes # 允许用户名密码方式登录
# PermitRootLogin without-password
PermitRootLogin yes # 允许root用户登录

重启ssh

1
sudo service ssh restart

给 WSL2 设置静态 IP 地址

WSL2 会在启动时做网络适配器的配置,我们需要在启动后重新为其配置网络

1
2
3
4
5
6
# 将 WSL2 的网关设置为 192.168.50.1,并为 WSL2 分配 192.168.50.2 的 IP 地址
# 删除掉 WSL2 已经配置的 IP 地址,然后为其分配 192.168.50.2/24,最后配置路由并指定 DNS Server
sudo ip addr del $(ip addr show eth0 | grep 'inet\b' | awk '{print $2}' | head -n 1) dev eth0
sudo ip addr add 192.168.50.2/24 broadcast 192.168.50.255 dev eth0
sudo ip route add 0.0.0.0/0 via 192.168.50.1 dev eth0
sudo echo nameserver 192.168.50.1 > /etc/resolv.conf

对 WSL2 使用的 Internal Virtual Switch 进行配置,PowerShell

1
2
3
4
5
# 找到 vEthernet (WSL) 这个网络适配器,然后将其所有已有的 IP 地址删除,为其添加 192.168.50.1/24 的 IP 地址,最后设置 NAT:首先删除名字叫做 WSLNat 的 NAT(因为我们后续创建的 NAT 名字叫做 WSLNat,这些命令每次启动系统后都需要执行,因此可能系统中已经存在名为 WSLNat 的 NAT 了,为了防止冲突,如果存在的话就先删掉) ,然后创建一个名字叫做 WSLNat 的 NAT,设置内部地址为 192.168.50.0/24
Get-NetAdapter 'vEthernet (WSL)' | Get-NetIPAddress | Remove-NetIPAddress -Confirm:$False
New-NetIPAddress -IPAddress 192.168.50.1 -PrefixLength 24 -InterfaceAlias 'vEthernet (WSL)'
Get-NetNat | ? Name -Eq WSLNat | Remove-NetNat -Confirm:$False
New-NetNat -Name WSLNat -InternalIPInterfaceAddressPrefix 192.168.50.0/24;

一键配置

管理员运行.bat或.cmd

1
2
3
4
5
6
wsl -d Ubuntu-20.04 -u root ip addr del $(ip addr show eth0 ^| grep 'inet\b' ^| awk '{print $2}' ^| head -n 1) dev eth0
wsl -d Ubuntu-20.04 -u root ip addr add 192.168.50.2/24 broadcast 192.168.50.255 dev eth0
wsl -d Ubuntu-20.04 -u root ip route add 0.0.0.0/0 via 192.168.50.1 dev eth0
wsl -d Ubuntu-20.04 -u root echo nameserver 192.168.50.1 ^> /etc/resolv.conf
powershell -c "Get-NetAdapter 'vEthernet (WSL)' | Get-NetIPAddress | Remove-NetIPAddress -Confirm:$False; New-NetIPAddress -IPAddress 192.168.50.1 -PrefixLength 24 -InterfaceAlias 'vEthernet (WSL)'; Get-NetNat | ? Name -Eq WSLNat | Remove-NetNat -Confirm:$False; New-NetNat -Name WSLNat -InternalIPInterfaceAddressPrefix 192.168.50.0/24;"
wsl sudo service ssh restart

问题

开启SSHD服务失败

现象
sshd: no hostkeys available — exiting
解决

1
2
3
ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
# 如果上述两个文件存在,仍然出现这个错误,那么chmod 600 上述两个文件
阅读全文
vscode

romote ssh配置

在json配置文件中配置远程平台系统类型

1
2
3
4
{
"remote.SSH.remotePlatform": {
"AWS": "linux"
},

将公钥文件id_rsa.pub传到远程服务器的authorized_keys文件中,在.ssh/config里配置密钥信息

1
2
3
4
Host AWS
HostName 52.221.237.133
User root
IdentityFile C:\Users\22782\.ssh\id_rsa

通过跳板机连接服务器

有时候我们需要跳板机来连接服务器,也即先连接一台跳板机服务器,然后通过这台跳板机所在的内网再次跳转到目标服务器。最简单的做法就是按上述方法连接到跳板机,然后在跳板机的终端用ssh指令跳转到目标服务器,但这样跳转后,我们无法在VScode中打开服务器的文件目录,操作起来很不方便。我们可以把config的设置改成如下,就可以通过c00跳板机跳转到c01了:

1
2
3
4
5
6
7
8
9
Host c00
HostName xxx.xxx.xxx.xxx(跳板机IP)
User lyfeng

Host c01
HostName 192.168.0.10(内网地址)
User lyfeng
ProxyCommand "openssh的安装路径"\ssh.exe -W %h:%p -q c00
# 连接c00, 再通过c00的局域网ssh到c01

openssh的安装路径因人而异(C:\Windows\System32\OpenSSH\ssh.exe)

SFTP配置

sftp.json

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
{
"name": "xxx",
"host": "xxx",
"protocol": "sftp",
"port": xxx,
"username": "xxx",
"password": "xxx",
"remotePath": "/home/jenkins/sftp",
"context": "E:\\Downloads\\sftp",
"uploadOnSave": true,
"passive": false ,
"interactiveAuth": true,
"syncMode": "update",
"ignore":[
"**/.vscode/**",
"**/.git/**",
"**/.DS.Store",
"**/dataset/**",
"*.jpg",
"*.weights",
"*.pt"
],
"watcher":{
"files": "false",
"autoupload": true,
"autoDelete": false
}

}

快捷键

函数定义查看

1
2
3
4
5
6
跳转:
Ctrl + 鼠标左击
返回:
Windows: Alt + ← ;或者 鼠标侧键
Linux: Ctrl + Alt + - ;
Mac: Ctrl + -

左右括号之间跳转

ctrl + shift + \

questions

无法在这个大型工作区中监视文件更改

原因:工作区很大并且文件很多,导致VS Code文件观察程序的句柄达到上限。

解决:

1
2
3
4
5
6
7
8
9
#1、使用以下命令查看当前限制:
cat /proc/sys/fs/inotify/max_user_watches
#2、编辑/etc/sysctl.conf
sudo vim /etc/sysctl.conf
#3、将以下一行添加到文件末尾,可以将限制增加到最大值
fs.inotify.max_user_watches=524288
#4、保存即可
sudo sysctl -p
#524,288是可以观看的最大文件数,每个文件监视占用540字节(32位)或1kB(64位),假设所有524,288个句柄都被消耗,上限约为256MB(32位)或512MB(64位)
wsl,Ubuntu,E: Could not read response to hello message from hook [ ! -f /usr/bin/snap ] || /usr/

解决:

1
sudo rm -rf /etc/apt/apt.conf.d/20snapd.conf
阅读全文
实用工具

Jetbrains系列产品reset

https://zhile.io/2020/11/18/jetbrains-eval-reset.html

postman使用

https://huajiakeji.com/utilities/2018-06/1461.html

Typora 使用

https://sspai.com/post/54912

winSCP密钥登录问题

Winscp使用的是putty作为SSH登录工具,而puttygen所生成的是以.ppk结尾的密钥文件,所以你使用xshell生成的密钥会提示添加失败,此时你有两种选择,

1、使用putty重新生成putty格式的密钥,并添加到服务器上。

2、将openssh格式的私钥转换成winscp支持的.ppk格式。

阅读全文
csv与json

CSV的换行符号要使用CRLF既” 回车符+换行符”的形式.
文字可以使用双引号围起来, 逗号可以围在双引号里面
每个单引号要换成””(两个单引号)且字段要用一对单引号围住

csv里的json

1
2
3
4
5
6
7
8
"{
""Content-Type"": ""application/json"",
""apiCode"": ""INTF_BC_outsvc_write_refreshAuthorize"",
""apiVersion"": ""1.0.0"",
""appCode"": ""app_jscn_wf_7310"",
""charset"": ""utf-8"",
""format"": ""http+json""
}"
阅读全文
python selenium

python selenium

弹出框处理的实现

弹出框有两种:页面弹出框(可定位元素能操作)、Windows弹出框(不能直接定位)

一、页面弹出框

等待弹出框出现之后,定位弹出框,操作其中元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
driver = webdriver.Chrome()
driver.get("https://www.baidu.com")
driver.maximize_window()
#点击百度登录按钮
driver.find_element_by_xpath('//*[@id="u1"]//a[@name="tj_login"]').click()

#等待百度登录弹出框中 要出现的元素可见
ele_id = "TANGRAM__PSP_10__footerULoginBtn"
param = (By.ID,ele_id)
#元素可见时,再进行后续操作
WebDriverWait(driver,10).until(EC.visibility_of_element_located(param))

driver.find_element_by_id(ele_id).click()
time.sleep(5)
driver.quit()

二、Windows弹出框

使用 driver.switch_to.alert 切换到Windows弹出框

Alert类提供了一系列操作方法:

  • accept() 确定
  • dismiss() 取消
  • text() 获取弹出框里面的内容
  • send_keys(keysToSend) 输入字符串
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
#1:定位alert弹出框
#点击页面元素,触发alert弹出框
driver.find_element_by_xpath('//*[@id="alert"]').click()
time.sleep(3)
#等待alert弹出框可见
WebDriverWait(driver,20).until(EC.alert_is_present())

#从html页面切换到alert弹框
alert = driver.switch_to.alert
#获取alert的文本内容
print(alert.text)
#接受--选择“确定”
alert.accept()

#2:定位confirm弹出框
driver.find_element_by_xpath('//*[@id="confirm"]').click()
time.sleep(3)
WebDriverWait(driver,20).until(EC.alert_is_present())
alert =driver.switch_to.alert
print(alert.text)
# 接受--选择“取消”
alert.dismiss()


#3:定位prompt弹出框
driver.find_element_by_id("prompt").click()
time.sleep(3)
WebDriverWait(driver,20).until(EC.alert_is_present())
alert =driver.switch_to.alert
alert.send_keys("jaja")
time.sleep(5)
print(alert.text)
# alert.dismiss()
alert.accept()

文件上传

input标签(type=file)

1
driver.find_element('name','file').send_keys('./bq.png')

非input型上传

autoit

AutoIt Windows Info 用于识别Windows控件信息
Compile Script to.exe 用于将AutoIt生成 exe 执行文件
Run Script 用于执行AutoIt脚本
SciTE Script Editor 用于编写AutoIt脚本

1.识别元素,主要是文件名输入框和打开按钮,使用AutoIt Windows Info完成,记录结果如下:
文件名输入框的class 为“Edit”,Instance为“1”
打开按钮的class 为“Button”,Instance为“1”
2.编写脚本,使用SciTE Script Editor,内容如下:

1
2
3
4
5
6
ControlFocus("文件上传", "","Edit1")
WinWait("[CLASS:#32770]","",10)
ControlSetText("文件上传", "", "Edit1","D:bq.jpg")
Sleep(2000)
ControlClick("文件上传", "","Button1");
#注意“文件上传”字样是你点击上传按钮之后弹出的对话框的title

3、验证脚本
保证页面的上传对话框打开,然后运行脚本tools>go
4、打开Compile Script to.exe工具,将其生成为exe可执行文件
5、python脚本中调用

1
2
3
4
up=self.driver.find_element('class name','avatar-uploader-trigger')
up.find_element('class name','ant-btn').click()
os.system('D:\python_workspace\QiangSEAuto\upload.exe')
time.sleep(20)
win32gui

示例网址:http://www.sahitest.com/demo/php/fileUpload.htm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# -*- coding: utf-8 -*-
from selenium import webdriver
import win32gui
import win32con
import time

dr = webdriver.Firefox()
dr.get('http://sahitest.com/demo/php/fileUpload.htm')
upload = dr.find_element_by_id('file')
upload.click()
time.sleep(1)

# win32gui
dialog = win32gui.FindWindow('#32770', u'文件上传') # 对话框
ComboBoxEx32 = win32gui.FindWindowEx(dialog, 0, 'ComboBoxEx32', None)
ComboBox = win32gui.FindWindowEx(ComboBoxEx32, 0, 'ComboBox', None)
Edit = win32gui.FindWindowEx(ComboBox, 0, 'Edit', None) # 上面三句依次寻找对象,直到找到输入框Edit对象的句柄
button = win32gui.FindWindowEx(dialog, 0, 'Button', None) # 确定按钮Button

win32gui.SendMessage(Edit, win32con.WM_SETTEXT, None, 'd:\\baidu.py') # 往输入框输入绝对地址
win32gui.SendMessage(dialog, win32con.WM_COMMAND, 1, button) # 按button

print upload.get_attribute('value')
dr.quit()

需要windows窗口、消息查看分析器:小工具:Spy++,也可以用autoIT自带的工具

相关API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
win32gui.FindWindow(lpClassName=None, lpWindowName=None):
#自顶层窗口开始寻找匹配条件的窗口,并返回这个窗口的句柄。
#lpClassName:类名,在Spy++里能够看到
#lpWindowName:窗口名,标题栏上能看到的名字
#代码示例里我们用来寻找上传窗口,你可以只用其中的一个,用classname定位容易被其他东西干扰,用windowname定位不稳定,不同的上传对话框可能window_name不同,怎么定位取决于你的情况。
win32gui.FindWindowEx(hwndParent=0, hwndChildAfter=0, lpszClass=None, lpszWindow=None)
#搜索类名和窗体名匹配的窗体,并返回这个窗体的句柄。找不到就返回0。
#hwndParent:若不为0,则搜索句柄为hwndParent窗体的子窗体。
#hwndChildAfter:若不为0,则按照z-index的顺序从hwndChildAfter向后开始搜索子窗体,否则从第一个子窗体开始搜索。
#lpClassName:字符型,是窗体的类名,这个可以在Spy++里找到。
#lpWindowName:字符型,是窗口名,也就是标题栏上你能看见的那个标题。
#代码示例里我们用来层层寻找输入框和寻找确定按钮
win32gui.SendMessage(hWnd, Msg, wParam, lParam)
#hWnd:整型,接收消息的窗体句柄
#Msg:整型,要发送的消息,这些消息都是windows预先定义好的,可以参见系统定义消息(System-Defined Messages)
#wParam:整型,消息的wParam参数
#lParam:整型,消息的lParam参数
#代码示例里我们用来向输入框输入文件地址以及点击确定按钮
SendKeys
1
pip install SendKeys 

通过SendKeys库可以直接向焦点里输入信息,不过要注意在打开窗口是略微加一点等待时间,否则容易第一个字母send不进去(或者你可以在地址之前加一个无用字符,不推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# -*- coding: utf-8 -*-
from selenium import webdriver
import win32gui
import win32con
import time

dr = webdriver.Firefox()
dr.get('http://sahitest.com/demo/php/fileUpload.htm')
upload = dr.find_element_by_id('file')
upload.click()
time.sleep(1)

# SendKeys
SendKeys.SendKeys('D:\\baidu.py') # 发送文件地址
SendKeys.SendKeys("{ENTER}") # 发送回车键

print upload.get_attribute('value')
dr.quit()
keybd_event

win32api提供了一个keybd_event()方法模拟按键

多文件

多文件上传就是在文件路径框里用引号括起单个路径,然后用逗号隔开多个路径,例如:“D:\a.txt” “D:\b.txt”
注意:只有多个文件在同一路径下,才能这样用,否则是会失败的

切换iframe

1.有id,并且唯一,直接写id

1
driver.switch_to_frame("x-URS-iframe")

2.有name,并且唯一,直接写name

1
driver.switch_to_frame("xxxx")

3.无id,无name,先定位iframe元素

1
2
3
iframe = driver.find_elements_by_tag_name("iframe")[0]
driver.switch_to_frame(iframe)
driver.switch_to.frame(driver.find_element_by_xpath("//iframe[contains(@src,'myframe')]"))

5.从frame中切回主文档(switch_to.default_content())

1
driver.switch_to.default_content()

6.嵌套frame的操作(switch_to.parent_frame())

1
2
3
4
5
<html>
<iframe id="frame1">
<iframe id="frame2" / >
</iframe>
</html>
1
2
3
4
5
#从主文档切到frame2,一层层切进去
driver.switch_to.frame("frame1")
driver.switch_to.frame("frame2")
#从frame2再切回frame1,这里selenium给我们提供了一个方法能够从子frame切回到父frame,而不用切回主文档再切进来。
driver.switch_to.parent_frame() # 如果当前已是主文档,则无效果
阅读全文
Selenium&appium grid

部署selenium hub与浏览器节点

1
2
3
4
# 部署selenium hub (java -jar selenium-server-standalone-3.141.59.jar -role hub)
docker run -d -p 4444:4444 --name hub -e GRID_TIMEOUT=0 -e GRID_THROW_ON_CAPABILITY_NOT_PRESENT-true -e GRID_NEW_SESSION_WAIT_TIMEOUT=-1 -e GRID_BROWSER_TIMEOUT=15000 -e GRID_TIMEOUT=30000 -e GRID_CLEAN_UP_CYCLE=30000 selenium/hub:3.141
# 部署node-chrome浏览器节点,debug模式可以通过VNC查看,密码secret(--link hub关联到hub,需同网段)
docker run -d -p 5900:5900 --name node -e NODE_MAX_INSTANCES=6 -e NODE_MAX_SESSION=6 -e NODE_REGISTER_CYCLE=5000 -e DBUS_SESSION_BUS_ADDRESS=/dev/null -v /dev/shm:/dev/shm --link hub selenium/node-chrome-debug:3.141

移动端接入

https://blog.csdn.net/chen072086/article/details/106503548?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param

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
# 设备注册到hub
cd '/c/Program Files/Appium/resources/app/node_modules/appium/build/lib'
node main.js -a 127.0.0.1 -p 4723 -bp 4725 -U 8676-A01-0x3da2c286 --nodeconfig /d/bq/config/dason.json

#配置说明
{
"capabilities":
[
{
"deviceName": "127.0.0.1:62001", #此为adb devices检测到的设备名
"version":"4.4.2", #模拟器/真机的系统版本
"maxInstances": 1, #最多的实例个数
"platform":"ANDROID", #测试平台:Android
"browserName": "" #测试普通App可置为空,如果是测试浏览器如chrome就可以写上
}
],
"configuration":
{
"cleanUpCycle":2000,
"timeout":30000,
"proxy": "org.openqa.grid.selenium.proxy.DefaultRemoteProxy",
"hub":"127.0.0.1:4444/grid/register", #hub的注册接口地址
"url":"http://127.0.0.1:4723/wd/hub", #Appium Server的默认访问路径
"host": "127.0.0.1", #Appium Server的IP地址
"port": 4723, #Appium Server端口号
"maxSession": 1,
"register": true,
"registerCycle": 5000,
"hubPort": 4444, #hub端口号
"hubHost": "127.0.0.1", #hub IP地址,这里是在本机上启动的,如果是在另外的机器上需要用其外部的IP地址,比如:192.168.1.111这种
"hubProtocol": "http" #协议,默认为http
}
}

#配置参考
{
"capabilities": [
{
"deviceName": "8676-A01-0x3da2c286",
"version": "5.1",
"maxInstances": 1,
"platform": "ANDROID",
"browserName": "chrome"
}
],
"configuration": {
"cleanUpCycle": 2000,
"timeout": 30000,
"proxy": "org.openqa.grid.selenium.proxy.DefaultRemoteProxy",
"hub": "http://192.168.0.108:4444/grid/register",
"url": "http://127.0.0.1:4723/wd/hub",
"host": "192.168.0.103",
"port": 4723,
"maxSession": 1,
"register": true,
"registerCycle": 5000,
"hubPort": 4444,
"hubHost": "192.168.0.108",
"hubProtocol": "http"
}
}
阅读全文
httprunner3.x

httprunner

相关:python,pytest,allure,locust,requests,git

ENV prepare

1
2
3
4
5
6
7
8
pip install httprunner -i https://pypi.douban.com/simple
hrun -V #查看版本
httprunner --help #查看帮助
#positional arguments:{run,startproject,har2case,make}
#run Make HttpRunner testcases and run with pytest.运行httprunner文件
#startproject Create a new project with template structure.创建httprunner项目结构
#har2case Convert HAR(HTTP Archive) to YAML/JSON testcases for HttpRunner.转换har文件为yml文件或者json文件或者pytest文件
#make Convert YAML/JSON testcases to pytest cases. 转换yml或json文件为pytest文件

create project

1
2
#cmd到项目文件下执行:
httprunner startproject interfacedemo#interfacedemo为接口项目名称

各个目录代表的含义:

1
2
3
4
5
6
- debugtalk.py 放置在项目根目录下(类似pytest的conftest文件)
- .env 放置在项目根目录下,可以用于存放一些环境变量
- reports 文件夹:存储 HTML 测试报告
- testcases 用于存放测试用例
- har 可以存放录制导出的.har文件
- .gitignore 设置上传到git时需要忽略那些文件信息

执行方式

1
2
3
4
5
6
1、hrun interfaceDemo
#命令等价于httprunner run interfaceDemo,其中先进行httprunner make json/yml,会将json/yml文件先转换为pytest文件,之后再执行hrun(httprunner run),如果pytest文件是已经存在的(你直接编写的pytest文件,而不是yml或者json),httprunner会直接运行你的pytest脚本,不需要进行转换,官方推荐:直接使用pytest脚本编写
#在tacecases目录下生成py文件,生成的py文件会加上_test后缀,如果yml或者json文件有修改,需要再次http make scriptPath一下,或者直接修改py文件
#生成了logs日志文件,每一个yml都会对应生成一个日志文件如下,每一个testcase脚本都会又要给唯一的id,对应了日志文件的文件名:
2、pytest interfaceDemo
#前提,已经使用hrun interfaceDemo生成了yml或json对应的pytest文件,否则不生效

har文件录制及转换

httprunner脚本转换

工具:Fiddler,Charles,Chrome等

1
2
3
4
5
har2case har_demo.har -2j#生成json文件命令
har2case har_demo.har -2y#生成yml文件命令
har2case har_demo.json/har_demo.yml#转换为py文件
#可以使用:hrun har_demo_test.py/har_demo.json/har_demo.yml运行脚本
#使用pytest har_demo_test.py只能运行py文件,不能运行yml或者json

测试用例结构

每个测试用例都是的子类HttpRunner,并且必须具有两个类属性:config和teststeps。
配置:配置测试用例级别设置,其中包括base_url,verify,variables,export。
teststeps:teststep(List[Step])的列表,每个步骤都对应一个API请求或另一个测试用例引用调用。此外,variables/ extract/ validate/ hooks机制支持,可制作十分复杂的测试方案,可以参数传递、参数提取、断言、以及其他hook机制,可以自定义钩子函数,也支持pytest和、locust的相关操作,例如:我们可以增加一个pytest.ini文件,在其中进行测试用例目录检索的操作等

chain call

就是IDEA的api代码自动补全

config

每个测试用例都应该有一个config部分,您可以在其中配置测试用例级别的设置。

name(必填)

指定测试用例名称。这将显示在执行日志和测试报告中

base_url(可选)

指定SUT的通用架构和主机部分,例如https://postman-echo.com。如果base_url指定,则teststep中的url只能设置相对路径部分。如果要在不同的SUT环境之间切换,这将特别有用。

variables(可选)

指定测试用例的公共变量。每个测试步骤都可以引用未在步骤变量中设置的配置变量。换句话说,步骤变量比配置变量具有更高的优先级。

verify (可选)

指定是否验证服务器的TLS证书。如果我们想记录测试用例执行的HTTP流量,这将特别有用,因为如果没有设置verify或将其设置为True,则会发生SSLError。
SSLError(SSLCertVerificationError(1,’[SSL:CERTIFICATE_VERIFY_FAILED]证书验证失败:证书链中的自签名证书(_ssl.c:1076)’)

export (可选)

指定导出的测试用例会话变量。将每个测试用例视为一个黑盒,config variables是输入部分,而config export是输出部分。特别是,当一个测试用例在另一个测试用例的步骤中被引用,并且将被提取一些会话变量以在后续测试步骤中使用时,则提取的会话变量应在配置export部分中进行配置。将测试用例的某些变量指定为全局变量。(PS:不配置export在另一个引用类中进行该类的变量调用时,直接export也是可以的,最好还是配置一下)

一个测试套件testsuite,套件中会有很多的测试用例testcase,testcase之间可以相互引用teststep,通过with_jmespath进行参数的提取,通过call引用其他测试用例类,然后通过export引用其他测试用例的变量。teststeps中会有很多step,也就是常说的测试步骤,一个step中只有RunRequest或者RunTestCase,step的先后顺序,有step的前后控制,由step所处的位置由上到下执行

RunRequest(名称)

RunRequest 在一个步骤中用于向API发出请求,并对响应进行一些提取或验证。
.nameRunRequest 的参数用于指定测试步骤名称,该名称将显示在执行日志和测试报告中。
.with_variables
指定测试步骤变量。每个步骤的变量都是独立的,因此,如果要在多个步骤中共享变量,则应在配置变量中定义变量。此外,步骤变量将覆盖配置变量中具有相同名称的变量。(PS:注意参数传递的格式使用**{},使用关键字参数解包的方式进行参数传递给with_variables),参数引用使用”$变量名”,如果是函数引用使用”${函数名()}”

method(url)

指定HTTP方法和SUT的URL。这些对应于method和url参数requests.request。
如果base_url在config中设置,则url只能设置相对路径部分。如果在Config中设置了baseurl,method中只能设置相对路径,可选参数为get/post/put/delete/等。
.with_params
指定请求网址的查询字符串。这对应于的params参数requests.request。
.with_headers
为请求指定HTTP标头。这对应于的headers参数requests.request。
.with_cookies
指定HTTP请求cookie。这对应于的cookies参数requests.request。
.with_data
指定HTTP请求正文。这对应于的data参数requests.request。
.with_json
在json中指定HTTP请求正文。这对应于的json参数requests.request。

extract(数据提取)

.WITH_JMESPATH
使用jmespath提取JSON响应主体。
with_jmespath(jmes_path:文字,var_name:文字)
jmes_path:jmespath表达式,有关更多详细信息,请参考JMESPath教程
var_name:存储提取值的变量名,可以在后续测试步骤中引用它
validate
.ASSERT_XXX
使用jmespath提取JSON响应主体并使用期望值进行验证。
assert_XXX(jmes_path:文本,期望值:任何,消息:文本=“”)
jmes_path:jmespath表达式,有关更多详细信息,请参考JMESPath教程
Expected_value:指定的期望值,变量或函数引用也可以在此处使用
消息(可选):用于指示断言错误的原因

RunTestCase(名称)

RunTestCase 在一个步骤中用于引用另一个测试用例调用。
.name
RunTestCase 的参数用于指定测试步骤名称,该名称将显示在执行日志和测试报告中。
.with_variables
指定测试步骤变量。每个步骤的变量都是独立的,因此,如果要在多个步骤中共享变量,则应在配置变量中定义变量。此外,步骤变量将覆盖配置变量中具有相同名称的变量。
.call
指定引用的测试用例类。你在引用另一个测试用例的step中的参数时,需要先指定引用的测试用例类
.export
指定会话变量名称以从引用的测试用例中导出。导出的变量可以通过后续测试步骤step进行引用。导出的是step中的jmespath提取的变量,export之后,这个变量是全局变量,但是不能再confg中进行设置,因为测试类的引用是在step中进行的,而类的初始化是先初始化config,然后初始化teststeps,所以参数的传递在step之间

.env文件

1
2
1、httprunner脚手架会默认创建创建.env文件,将需要设置为环境变量或全局变量的值,存储在env中
2、使用${ENV(变量名)}调用环境变量

针对环境变量为列表信息的可以先设置为字符串,再进行字符串转列表操作

例如服务器连接(ip|port|user|pwd):192.168.xxx.xxx|22|admin|admin

代码实现时,将字符串按“|”来进行切割,将结果保存在列表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
def load_file_to_service(serviceInfo, local_path, remote_path):
"""
:param serviceInfo: 服务器信息格式:ip|port|user|pwd
:param local_path: 本地路径
:param remote_path: 远程路径
:return:
"""
info = serviceInfo.split("|")
t = paramiko.Transport(info[0], int(info[1]))
t.connect(username=info[2], password=info[3])
sftp = paramiko.SFTPClient.from_transport(t)
sftp.put(local_path, remote_path) # 上传文件到远程机
sftp.close()

debugtalk.py

debugtalk.py和pytest的conftest.py文件区别:

1
conftest.py文件中的定义对当前同级目录下及同级目录下的子目录下的脚本生效,而debugtalk.py按照脚手架的默认生成,只对同级目录testcases目录下的脚本生效,如果需要在testcase下面进行脚本的细分,创建新的模块目录,然后在里面添加测试脚本,是不生效的。

debugtalk主要进行一些公共函数方法的编写例如常用的获取token、获取cookie,或者数据清理,环境初始化等操作,都可以放在debugtalk中。

将脚本中的方法引用到其他处,格式”${方法名}”,即能实现参数化

setup和teardown及hook

httprunner的setup和teardown可以在yml或者json文件中定义(建议py文件中)

httprunner有两种setup和teardown的定义方式,一个是测试类级别,一个是测试步骤级别的定义。

测试类级别的setup和teardown

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
#!/user/bin/env python  
# -*- coding: utf-8 -*-
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase


class TestBaiduRequestTestCase(HttpRunner):
def setup(self):
print("运行于测试用例之前")

def teardown(self):
print("运行于测试用例之后")

config = (
Config("get user list")
.base_url("https://www.baidu.com")
.verify(False)
)

teststeps = [
Step(
RunRequest("get info")
.get("/")
.validate()
.assert_equal("status_code", 200)
)
]


if __name__ == "__main__":
TestBaiduRequestTestCase().test_start()
#结果为:
#Process finished with exit code 0
#运行于测试用例之前
#PASSED [100%]...
#运行于测试用例之后

#!/user/bin/env python
# -*- coding: utf-8 -*-
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase


class TestBaiduRequestTestCase(HttpRunner):
@classmethod
def setup_class(cls):
print("运行于测试用例之前")

@classmethod
def teardown_class(cls):
print("运行于测试用例之后")

config = (
Config("get user list")
.base_url("https://www.baidu.com")
.verify(False)
)

teststeps = [
Step(
RunRequest("get info")
.get("/")
.validate()
.assert_equal("status_code", 200)
)
]


if __name__ == "__main__":
TestBaiduRequestTestCase().test_start()
#上面两种写法在unittest和pytest中是不一样的,setup_class是运行于测试类的前面,setup是运行与每个测试方法的前面,在httprunner貌似不区分这两个方法。

测试步骤前后的setup和teardown

在debugtalk.py中写hook_up和hook_teardown

1
2
3
4
5
6
7
8
9
def hook_up():  
print("前置操作:setup!")


def hook_down(response=None):
print("后置操作:teardown!")
if response:
print(response)
response.status_code = 300

在demo_baidu_request_test.py中调用debugtalk的两个hook方法,使用setup_hook()和teardown_hook()来加载我们自定义的hook:

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
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase  



class TestBaiduRequestTestCase(HttpRunner):
@classmethod
def setup_class(cls):
print("运行于测试用例之前")

@classmethod
def teardown_class(cls):
print("运行于测试用例之后")

config = (
Config("get user list")
.base_url("https://www.baidu.com")
.verify(False)
)

teststeps = [
Step(
RunRequest("get info")
.setup_hook("${hook_up()}")
.get("/")
.teardown_hook("${hook_down()}")
.validate()
.assert_equal("status_code", 200)
)
]


if __name__ == "__main__":
TestBaiduRequestTestCase().test_start()
#运行结果:
#Process finished with exit code 0
#运行于测试用例之前
#PASSED [100%]前置操作:setup!
#后置操作:teardown!
#...
#运行于测试用例之后

既然是hook方法,那么肯定是会集成一些内置的钩子,满足特殊的要求所使用的。
setup_hooks:在测试步骤前执行,先调用setup_hooks()内的函数。可以传入 $request 参数,可以对请求进行预处理或者修改,修改请求参数
teardown_hooks:在测试步骤执行后,先调用teardown()内的函数,可以传入$response参数,可以对返回值进行处理
先在debugtalk.py中定义两个方法,输出一下后面获取的request和response.

1
2
3
4
5
6
7
8
def hook_up(request=None):  
print("输出request:{}".format(request))
print("前置操作:setup!")


def hook_down(response=None):
print("输出response:{}".format('\n'.join(['%s:%s' % item for item in response.__dict__.items()])))
print("后置操作:teardown!")

在demo_baidu_request_test.py文件中调用这两个hook,然后传递参数$request和$response。

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
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase  


class TestBaiduRequestTestCase(HttpRunner):
@classmethod
def setup_class(cls):
print("运行于测试用例之前")

@classmethod
def teardown_class(cls):
print("运行于测试用例之后")

config = (
Config("get user list")
.base_url("https://www.baidu.com")
.verify(False)
)

teststeps = [
Step(
RunRequest("get info")
.setup_hook("${hook_up($request)}")
.get("/")
.teardown_hook("${hook_down($response)}")
.validate()
.assert_equal("status_code", 200)
)
]


if __name__ == "__main__":
TestBaiduRequestTestCase().test_start()
#结果如下:
#Process finished with exit code 0
#运行于测试用例之前
#...
#前置操作:setup!
#resp_obj:<Response [200]>
#validation_results:{}
#后置操作:teardown!

传入的是一个request和response对象,可以对传入的request和response对象进行操作,修改resquest和response传入和返回的值,完成复杂的业务要求。
改变一下debugtalk.py:

1
2
3
4
5
6
7
8
9
10
11
12
def hook_up(request=None):  
print("输出request:{}".format(request))
print("前置操作:setup!")
if request:
request["params"]["username"] = "888888"


def hook_down(response=None):
print("输出response:{}".format('\n'.join(['%s:%s' % item for item in response.__dict__.items()])))
print("后置操作:teardown!")
if response:
response.status_code = 404

修改了传入的setp的密码为“888888”,修改了step返回的状态码为404,看一下调用情况:

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
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase  


class TestBaiduRequestTestCase(HttpRunner):
@classmethod
def setup_class(cls):
print("运行于测试用例之前")

@classmethod
def teardown_class(cls):
print("运行于测试用例之后")

config = (
Config("get user list")
.variables(
**{
"username": "123456"
}
)
.base_url("https://www.baidu.com")
.verify(False)
)

teststeps = [
Step(
RunRequest("get info")
.setup_hook("${hook_up($request)}")
.get("/")
.with_params(**{"username": "${username}"})
.teardown_hook("${hook_down($response)}")
.validate()
.assert_equal("status_code", 200)
)
]


if __name__ == "__main__":
TestBaiduRequestTestCase().test_start()
#执行结果:
#test_start
#运行于测试用例之前
#...
#前置操作:setup!
#输出response:resp_obj:<Response [200]>
#validation_results:{}
#后置操作:teardown
#method : GET
#url : https://www.baidu.com/?username=888888
#httprunner.exceptions.ValidationFailure: assert status_code equal 200(int) ==> fail
#check_item: status_code
#check_value: 404(int)
#assert_method: equal
#expect_value: 200(int)
#可以看到断言是失败的,设置的成功断言状态码是200,传入的request中的username开始是123456,截获请求参数后更改为了888888。在实际应用中,可以对于传入账号密码等进行加密,或者对于返回值的格式等进行解码操作

参数化数据驱动

1
2
3
4
5
支持三种入参,返回一个列表:
1、列表:["iOS/10.1", "iOS/10.2", "iOS/10.3"]
2、Parameterize类的回调,例如csv:${parameterize(account.csv)}
3、debugtalk.py的回调,${gen_app_version()}
在使用参数化之前,首先要导入pytest包,和httprunner的Parameters这个类
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
import pytest  
from httprunner import Parameters
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase


class TestBaiduRequestTestCase(HttpRunner):
@pytest.mark.parametrize("param", Parameters(
{"username": [111, 222, 333]}
))
def test_start(self, param):
super().test_start(param)


config = (
Config("get user list")
.variables(
**{
"username": "${get_username()}"
}
)
.base_url("https://www.baidu.com")
.verify(False)
)

teststeps = [
Step(
RunRequest("get info")
.get("/")
.with_params(**{"username": "$username"})
.validate()
.assert_equal("status_code", 200)
)
]


if __name__ == "__main__":
TestBaiduRequestTestCase().test_start()

pytest.mark.parametrize()会先将param作为一个动态参数,传递给param,然后由httprunner在进行参数化,httprunner在pytest的parametrize上封装了一层,增加了csv及debugtalk.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
import pytest  
from httprunner import Parameters
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase


class TestBaiduRequestTestCase(HttpRunner):
@pytest.mark.parametrize("param", Parameters(
{"username": [111, 222, 333]}
))
def test_start(self, param):
super().test_start(param)


config = (
Config("get user list")
.variables(
**{
"username": "${get_username()}"
}
)
.base_url("https://www.baidu.com")
.verify(False)
)

teststeps = [
Step(
RunRequest("get info")
.get("/")
.with_params(**{"username": "$username"})
.validate()
.assert_equal("status_code", 200)
)
]


if __name__ == "__main__":
TestBaiduRequestTestCase().test_start()

debugtalk.py的回调函数

在debugtalk.py中定义一个函数,返回列表:

1
2
3
4
5
6
7
8
9
def get_username():  
return [
{"username": "111111"},
{"username": "222222"},
{"username": "333333"},
{"username": "444444"},
{"username": "555555"},
{"username": "666666"},
]

在xx_test.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
import pytest  
from httprunner import Parameters
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase


class TestBaiduRequestTestCase(HttpRunner):
@pytest.mark.parametrize("param", Parameters(
{"username": "${get_username()}"}
))
def test_start(self, param):
super().test_start(param)

config = (
Config("get user list")
.base_url("https://www.baidu.com")
.verify(False)
)

teststeps = [
Step(
RunRequest("get info")
.get("/")
.with_params(**{"username": "$username"})
.validate()
.assert_equal("status_code", 200)
)
]


if __name__ == "__main__":
TestBaiduRequestTestCase().test_start()

使用csv文件

1.csv文件中的title要为变量名

1
2
3
name,pwd
name1,123
name2,123

2.csv映射的时候,参数名要以“-”分割,name和pwd使用的-进行分割

3.csv的路径要使用相对路径,不支持绝对路径不支持\符号的路径

1
2
3
@pytest.mark.parametrize("param", Parameters(  
{"name-pwd": "${parametrize(testdata/namepwd.csv)}"}
))

测试报告

pytest-html

1
2
pip install pytest-html#安装pytest-html插件
hrun testcasesPath --html=path/report.html

allure

1
2
3
4
5
6
#生成测试结果
hrun testcasePath --alluredir=resultPath#测试结果存放路径
#第一种方式,会生成静态资源文件,通过静态资源index.html地址访问
allure generate resultPath -o reportPath
#第二种方式,不会生成静态资源文件,启动一个web服务,提供ip和port在线访问
allure serve resultPath

调试、XML消息支持、XML格式断言

添加调试信息

在调试代码时,引入loguru打印日志(httprunner源码中作者使用loguru.logger进行日志打印)

可在debugtalk.py中沿用此模块来进行日志打印

1
from loguru import logger

使用print未必会打印,但是使用logger.info会将信息打印到命令行中,格式为: logger.info()

XML消息支持

将报文内容写在文档中,用例中调用

1
2
3
4
5
6
7
8
9
10
11
12
#将报文内容保存在.xml文件中,使用get_file_std()方法读取文件内容返回
最后将变量m_encoder付给data关键字
def get_file_std(filename):
# 获取当前脚本所在文件夹路径
curPath = os.path.dirname(os.path.realpath(__file__))
print('curPath', curPath)
# 获取文件路径
fileurl = os.path.join(curPath, "messages/%s" % filename)
print('fileurl', fileurl)

with open(fileurl, "r+", encoding='utf8') as f:
return f.read()

xml报文断言实现

方案一:将xml报文内容作为整体进行断言操作

( 定义变量:xmlinfo 0)

1
2
3
4
5
6
7
8
9
10
11
config:
name: "batch user order sync interface"
base_url: ${ENV(49_HOST)}
verify: False
variables:
localpath: "D:\\work\\wy_only\\CtIntfDemo\\data\\update.txt"
remotepath: "/iot/Filesync/User_20201027150556_7169060048412345.txt"
49serv: ${ENV(49_SERV)}
oracle1: ${ENV(oracle11)}
xmlinfo: '<BatchNotifyRsp><ResultCode>0</ResultCode><ResultDec></ResultDec></BatchNotifyRsp>'
12345678910

断言:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    variables:
file_path: "data\\batchOrderSync.xml"
m_encoder: ${get_file_std($file_path)}
retbody: ${str_bytes($xmlinfo)} # 将str格式转化为bytes格式
request:
method: POST
url: /Sync/BatchOperatorNotify/User
headers:
Content-Type: application/xml
data: $m_encoder
validate:
- eq: ["status_code", 200]
# - eq: [body.BatchNotifyRsp.ResultCode, "0"]
- eq: [body, $retbody] #直接断言返回xml整体
1234567891011121314

方法:

1
2
3
4
5
6
def str_bytes(str):
"""
:param str:字符串
:return:bytes类型
"""
return bytes(str, encoding="utf8")
方案二:将xml报文内容转为json格式,再将返回结果赋值为转化后的json,最后进行json格式断言。

teardown_hooks机制,会在断言之前执行

1
2
teardown_hooks:
- ${teardown_hook_xml_json($response)}

断言:

1
2
3
4
validate:
- eq: ["status_code", 200]
- eq: [body.BatchNotifyRsp.ResultCode, "0"]
#- eq: [body, $retbody] #直接断言返回xml整体

方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def xml_to_json(xml_str):
# parse是的xml解析器
xml_parse = xmltodict.parse(xml_str)
# json库dumps()是将dict转化成json格式,loads()是将json转化成dict格式。
# dumps()方法的ident=1,格式化json
json_str = json.dumps(xml_parse, indent=1)
return json_str


def teardown_hook_xml_json(response):
"""
将xml报文内容转化为json格式内容,并将返回内容替换成json格式
:param response: 返回报文对象
"""
jsoninfo = xml_to_json(response.body)
response.body = json.loads(jsoninfo)

questions

1.查看log时,或者报错信息是中文时,显示unicode编码。

1
2
3
4
5
6
#可以在httprunner/client.py 文件中添加 ensure_ascii=False修改以下代码如下:
def log_print(req_or_resp, r_type):
msg = f"\n================== {r_type} details ==================\n"
for key, value in req_or_resp.dict().items():
if isinstance(value, dict):
value = json.dumps(value, indent=4, ensure_ascii=False)

2.cookies管理

httprunner继承requests,可自动管理

403 Forbidden,一般是网站处于安全考虑,缺少cookies导致(有些接口需要用到cookies,cookie没关联起来)

隐藏参数csrfmiddlewaretoken

csrfmiddlewaretoken参数是html页面上的隐藏参数,是为了防止跨域伪造请求。每次刷新页面都会自动变,需要先把此参数提取出来,动态关联到请求参数的body里面

阅读全文
docker使用

常用命令

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
# 1.停止所有的container,这样才能够删除其中的images:
docker stop $(docker ps -a -q)
# 如果想要删除所有container:
docker rm $(docker ps -a -q)
# 2.查看当前有些什么images
docker images
# 3.删除images,通过image的id来指定删除谁
docker rmi <image id>
# 想要删除untagged images,也就是那些id为<None>的image的话可以用
docker rmi $(docker images | grep "^<none>" | awk "{print $3}")
# 删除全部image
docker rmi $(docker images -q)
# 退出容器
# 1.退出并停止容器:(而后执行docker ps不能看到该容器)
Ctrl+D
# 2.退出但不停止容器:而后执行docker ps能看到该容器)"
Ctrl+P+Q
# 进入容器(只能进入已经启动了的容器,若容器已经停止,则先执行docker start <容器名称或ID> 来启动容器)
docker attach <容器名称或ID>
# 启动/停止容器
docker start/stop <容器名称或ID>
# 查看镜像
docker images
# 提交镜像,(docker commit <容器名称或id> <自定镜像名称>:<自定版本号TAG> )
docker commit 2736 myproject:1.0.0
# 查看镜像的修改历史
docker history 镜像ID
# 连接到容器上运行bash
docker exec -ti CONTAINER /bin/bash
# 删除镜像
docker rmi IMAGE ID
docker rmi REPOSITORY+TAG
# 查看版本
docker version
# 搜索可用docker镜像
docker search centos
# 下载镜像
docker pull centos
# docker导入本地镜像
cat centos.tar | docker import - centos6
# docker导出镜像
docker export id > cenos6.tar
# 在docker容器中运行hello world!
docker run centos echo "hello word"
# 在容器中安装ntpdate的程序
docker run centos yum install ntpdate
# 关闭容器
docker stop id
# 启动某个容器
docker start id
# 删除容器
docker rm id
#容器主机间文件复制
#从主机复制到容器
sudo docker cp host_path containerID:container_path
#从容器复制到主机
sudo docker cp containerID:container_path host_path

启动,端口映射及目录挂载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 从镜像启动容器
docker run -d -p 9090:8080 --name testproject myproject:1.0.0 /root/run.sh
-d:
# 表示以“守护模式”执行/root/run.sh脚本,此时 Tomcat 控制台不会出现在输出终端上。此时需要查看日志使用命令:docker logs <容器名称或ID>
-p:
# 表示宿主机与容器的端口映射,此时将容器内部的 8080 端口映射为宿主机的 9090 端口,这样就向外界暴露了 9090 端口,可通过 Docker 网桥来访问容器内部的 8080 端口了。
--name:
# 表示容器名称,用一个有意义的名称命名即可"

# 把所需要的环境(如jdk,tomcat等)拉取到docker容器中
# 方法一:启动一个容器,同时指定将宿主机中的某个文件夹挂载到docker容器中,-v参数中,冒号":"前面的目录是宿主机目录,后面的目录是容器内目录
docker run -i -t -v /myjava/:/mnt/software/ ubuntu /bin/bash
-i:
# 以交互模式运行容器,通常与 -t 同时使用;
-t:
# 为容器重新分配一个伪输入终端,通常与 -i 同时使用(容器启动后会进入其命令行);
-v:
#指定宿主机的哪个目录挂载到容器中,格式:-v <宿主机目录>:<容器目录>

# 方法二:使用docker cp 命令拷贝文件,(docker cp <宿主机目录或文件> <容器名称或id>:<容器目录>)
# 若已经在容器里面,则先退出容器,退出容器后执行命令:
docker cp myjava/ 190d:mnt/software/

便捷搭建服务搭建

jernkins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
docker run -d --name jenkins -p 8080:8080 -p 50000:50000 -v ~/docker/jenkins:/var/jenkins_home jenkins/jenkins
chown -R 1000:1000 ~/docker/jenkins

#更换插件源地址
#进入jenkins的工作目录
#hudson.model.UpdateCenter.xml
#http://updates.jenkins-io/update-center.json
#改成
#https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json
### 发现http://updates.jenkins-io/update-center.json https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json 内容一样,需修改json文件里的实际地址
#进入Jenkins工作目录:
cd /var/lib/jenkins/updates/
#使用sed命令修改default.json文件:
sed -i 's#https://updates.jenkins.io/download#https://mirrors.tuna.tsinghua.edu.cn/jenkins#g' default.json && sed -i 's#http://www.google.com#https://www.baidu.com#g' default.json
jenkins节点失联

如果您不想重新启动,则只需在从站上终止这些进程,然后再次启动从属代理

1
2
3
$ ps -ef |grep java
1006 11948 11930 0 Jul17?00:00:00 bash -c cd“/ mnt / jenkins”&& java -jar slave.jar
1006 11949 11948 0 Jul17?00:02:55 java -jar slave.jar
Timestamper

job配置,构建环境中勾选:Add timestamps to the Console Output。执行时控制台输出就会在每一行显示时间

对于脚本化管道,只需将您的脚本包装在timestamps { } Eg中即可。

1
2
3
timestamps {
// do your job
}

向声明管道添加选项

1
2
3
4
5
pipeline {
agent any
options { timestamps () }
// stages and rest of pipeline.
}

全局配置:在Jenkins配置的“时间戳”部分中选中“为所有管道版本启用”

QQ&微信通知

https://my.oschina.net/u/4320414/blog/3930777

钉钉通知

https://blog.csdn.net/liujingqiu/article/details/87977396

重置Jenkins的build序号

1
2
3
4
5
6
7
# 系统管理->脚本命令行
item = Jenkins.instance.getItemByFullName("your-job-name-here")
//THIS WILL REMOVE ALL BUILD HISTORY
item.builds.each() { build ->
build.delete()
}
item.updateNextBuildNumber(1)

sonar

构建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
docker pull postgres
docker pull sonarqube
docker run -p 5432:5432 --name db -e POSTGRES_USER=sonar -e POSTGRES_PASSWORD=sonar -d postgres
docker run --name sq -e SONARQUBE_JDBC_URL=jdbc:postgresql://172.31.19.221:5432/sonar -e SONARQUBE_JDBC_USERNAME=sonar -e SONARQUBE_JDBC_PASSWORD=sonar -p 9000:9000 -d sonarqube
#加插件:https://github.com/rhinoceros/sonar-p3c-pmd/releases/tag/pmd-3.2.0-beta-with-p3c1.3.6-pmd6.10.0
#https://gitee.com/chenlingtao/sonar-pmd.git
docker stop d0c95e1e9270
docker cp /home/docker/sonarqube/extensions/plugins/sonar-pmd-plugin-3.2.0-SNAPSHOT.jar
d0c95e1e9270:/opt/sonarqube/extensions/plugins
docker start d0c95e1e9270
#宿主机目录挂载,方便加插件
docker run -v /home/bq/docker/sonarqube/extensions/plugins:/opt/sonarqube/extensions/plugins --name sq --link db -e SONARQUBE_JDBC_URL=jdbc:postgresql://db:5432/sonar -e SONARQUBE_JDBC_USERNAME=sonar -e SONARQUBE_JDBC_PASSWORD=sonar -p 9000:9000 -d sonarqube
docker run -v ~/docker/sonarqube/extensions/plugins:/opt/sonarqube/extensions/plugins --name sq -e SONARQUBE_JDBC_URL=jdbc:postgresql://172.17.225.160:5432/sonar -e SONARQUBE_JDBC_USERNAME=sonar -e SONARQUBE_JDBC_PASSWORD=sonar -p 9000:9000 -d sonarqube

#内存要够,不够es抛137;sudo sysctl -w vm.max_map_count=524288 太小es抛78
扫描
1
sonar-scanner.bat -D"sonar.projectKey=homepage" -D"sonar.sources=./src" -D"sonar.java.binaries=./out" -D"sonar.host.url=http://ec2-52-221-237-133.ap-southeast-1.compute.amazonaws.com:9000" -D"sonar.login=31d831eb5f55827eb84201821a921ed3131ae455"

禅道

1
2
3
4
5
6
# 创建docker网络驱动
#sudo docker network create --subnet=[ip范围] [网络驱动名]
#ip范围:例如172.172.172.0/24的意思ip可以指定范围为172.172.172.1到172.172.172.254;网络驱动名:创建的网络驱动名,可随意指定;
docker network create --subnet=172.172.172.0/24 zentaonet
#启动
docker run --name zentao -p 8090:80 --network=zentaonet --ip 172.172.172.172 --mac-address 02:42:ac:11:00:00 -v ~/docker/zentao/zentaopms:/www/zentaopms -v ~/docker/zentao/mysqldata:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -d easysoft/zentao:latest
阅读全文
Algolia