大家好~我是米洛!我正在从0到1打造一个开源的接口测试平台, 也在编写一套与之对应的完整教程,希望大家多多支持。回顾上一节我们牛刀小试,编写了redisManager,并且成功执行了redis命令。那这一节,就让我们来折腾下在线执行Redis命令。由于某些特定的原因,在使用aredis的时候有了一些不好的体验,主要是以下几个方面, 所以我打算弃坑了。get和set操作没有支持中文,非常难受对于报错信息不是那么友好,因为我这边出现了一个连接出错的问题,发现对方的error是ConnectionError,里面毫无内容之前也说了,迁移成本巨低,所以我们可以完美切回用户最多的同步库(如果踩坑也有人一起想办法)安装redis和redis-py-clusterpip3 install redis redis-py-cluster改写Manager类其实要改变的并不多,看下git的改动就知道了:修改引入路径image修复之前的bug之前的获取单实例从集群中拿数据了,大错特错了改写cluster换个名字而已,从StrictCluster改为Cluster完善在线执行命令的方法做一个判断,如果是cluster则获取集群客户端,否则获取实例客户端这样我们的后端接口就编写完毕了,因为我们想做的是类似redis-cli的功能。寻找前端组件我们需要一个web版本的终端,所以我在github寻找了很久,找到了这样一款质量还不错的:到时候我们的页面上,就让用户输入这个了最终效果最终显示效果还是比较丑,左侧是现有的redis连接配置,右侧是类似redis-cli的客户端页面。在右侧执行语句以后,就会调用我们刚才编写的redis在线执行接口。凑合能用就行。给大家看看使用gif:image目前只支持基础的操作,包括常见的:get set hget hset hgetall等等自动更新Redis每当配置有变更的时候,我们都需要变更RedisManager中的数据,所以我们之前为之编写了Refresh方法。我们修改update/delete方法:删除client也编写了一个方法这里的background_tasks是来自fastapi的后台任务,如果大家了解go的话,这里就等于:func test() { // 业务逻辑 result := xxx go RedisManager.delete() return result }其实就是一个后台任务,咱们的主体方法可以直接return了。删除客户端的方法,如果是集群则pop集群字典今天的内容就到这了,下期我们解决APScheduler重复执行的问题。
记得之前我们讲过生成excel文件的事情,那么如何把服务器生成的excel文件正确发送给用户呢?今天我们就来说说在FastApi中如何正确让用户下载到想要的文件。基本流程其实文件下载的场景还是挺多的,比如我想要拿到我这个用户最近10天创建的测试用例数据,那么我们服务端应该怎么做呢?根据条件筛选出正确的数据处理数据,生成对应的目标格式文件,比如csv,xlsx等等返回http响应,其中指定response的内容和类型温馨提示现在假设我们已经完成了之前的步骤,并且生成了一个临时文件。需要注意的是,临时文件的名字为了确保唯一性,最好是用时间戳+随机字符串,或者懒一点可以用uuid这里为了方便,我就编写一个简便的方法:import time import random random_str = list("abcdefgh") random.shuffle(random_str) filename = f"{time.time_ns()}_{''.join(random_str)}"取当前时间戳(精确到纳秒),这时候还是有可能会有同一请求发生,所以我们再用random.shuffle对我们想要加的字符串进行随机排序(打乱顺序)。这样一来,文件重名的概率就小了非常多,如果要严谨的话,可以把字符串放长点,但是文件名也会拖很长。记得一定要保存这个随机的文件名,并且加上文件后缀哈~!FastApi怎么做呢其实文件也是HTTP的响应之一,只不过它相对特殊。在FastApi中,响应有Response和FileResponse等多种,我们暂时看Response和FileResponse即可。imageResponse是我们常见的类型,当然fastapi比较友好,你如果return 一个字典,会默认将之转换为JSON Response。但当你要设置返回的http状态码,那就需要去操作这个Response对象了。我们常见的比如403 forbidden,401未认证,都可以用Response来实现。那么对于FileResponse,我们怎么用呢?其实用法比较简单,我们来看实战:from fastapi import FastAPI from starlette.responses import FileResponse app = FastAPI(name="monitor") @app.get("/download") async def download(): # 处理完毕文件以后,生成了文件路径 filename = "你要下载的文件路径.xls" return FileResponse( filename, # 这里的文件名是你要发送的文件名 filename="lol.exe", # 这里的文件名是你要给用户展示的下载的文件名,比如我这里叫lol.exe )这样,前端页面提供一个a标签,href地址填对应的接口地址就好了。<!DOCTYPE html> <html> <head> <title>测试</title> </head> <body> <!-- 这个地址用你的host:port加上接口地址--> <a href="http://localhost:7777/download">下载文件</a> </body> </html>等等 好像少了点啥我们这个生成的文件虽然说都是随机的,没啥影响。但是如果一直有人生成,那不删除真的大丈夫吗?所以我们得考虑下怎么删除文件~理所当然认为try finally答案是行不通的,因为finally的内容会在return之前进行。如果这时候你删除了文件,那么Response就返回不了文件了,会报错。还好我们FastApi原生提供了background功能,幕后工作人员会在执行完毕之后进行一些暗箱操作。所以我们可以这么改动:from starlette.background import BackgroundTask return FileResponse( filename, filename="application.xls", background=BackgroundTask(lambda: os.remove(filename)), )使用background接受一个参数BackgroundTask,里面参数是一个无参方法:lambda: os.remove(filename)也就是删除这个文件的方法。最后flask的相关文件下载可以看博主几年前写的文章,原理都通用。
前言大家在用Python写一些小程序的时候,经常都会用到文件下载,对于一些较小的文件,大家可能不太在乎文件的下载进度,因为一会就下载完毕了。但是当文件较大,比如下载chromedriver的时候,我们如果能够看到下载的进度条,那该多么友好。毕竟在npm,pip安装包的时候都有类似的进度条。那笔者今天就给大家分享一个展示文件下载进度条的方法。requestsrequests库相信大家都用过,做接口测试少不了它。其实我们平时下载文件,也可以用requests做到的,比如有这样一个下载地址:Termius.exe我们要下载它,应该怎么做呢?这里来写一下伪代码,非常好懂。# 第一步: 访问这个链接 import requests r = requests.get("https://autoupdate.termius.com/windows/Termius.exe") # 第二步: 获取返回的文件内容,并写到本地 with open(r"./termius.exe", "wb") as f: f.write(r.content)由于我这个文件很大,所以过程很漫长,我这边就不展示具体细节了。大家可以找个几M的小文件试试。放上最终结果:可以看到,执行完毕,文件也获取到了思考有没有觉得,这样下载文件很单调,我也不知道进度,比如下载了多少了,特别是针对大一点的文件,一直这样等着我以为他断开连接了呢。如果咱们能在下载文件的时候,显示出进度条该多好,比如迅雷/百度网盘那样子的。只需要安装一个库即可要做到这些,我们只需要安装tqdm库就行了。pip install tqdm下面是带注释的示例代码:import requests from tqdm import tqdm def download(url: str, fname: str): # 用流stream的方式获取url的数据 resp = requests.get(url, stream=True) # 拿到文件的长度,并把total初始化为0 total = int(resp.headers.get('content-length', 0)) # 打开当前目录的fname文件(名字你来传入) # 初始化tqdm,传入总数,文件名等数据,接着就是写入,更新等操作了 with open(fname, 'wb') as file, tqdm( desc=fname, total=total, unit='iB', unit_scale=True, unit_divisor=1024, ) as bar: for data in resp.iter_content(chunk_size=1024): size = file.write(data) bar.update(size) if __name__ == "__main__": # 下载文件,并传入文件名 url = "https://autoupdate.termius.com/windows/Termius.exe" download("https://autoupdate.termius.com/windows/Termius.exe", "haha.exe")代码比较简单,tqdm的内容基本都是固定写法,大家不用疑惑,注意好requests等自己熟悉的部分就好。我们来看看gif效果图,是不是狂炫酷霸吊炸天(别好奇我的cmd为何这么帅气,我是不会告诉你滴,除非你点赞):image好啦,今天的小知识就分享到这里。拜了个拜!
关于图片验证码的文章,我想大家都有一定的了解了。在我们做UI自动化的时候,经常会遇到图片验证码的问题。image当开发不给咱们提供万能验证码,或者测试第三方网站比如知乎的时候,我们就需要自己去识别验证码。OCROCR是一种图像文字识别的技术,例如图中的验证码,我们用肉眼识别就是c5s3,但机器可不比咱们肉眼。所以我们要利用ocr技术,让我们的Python脚本自动通过图片识别出对应的文字。常见的识别类库在Python中其实有许多识别类库,这里只介绍博主自己实践过的成功率还不错的: 百度ocr。简单的说,就是百度提供了一个SDK,让我们传入图片数据,从而拿到识别的结果。ocr的细节我们不需要关心。申请开通OCR首先我们得有一个百度账号,这个相信大家都有,没有的可以申请一个。登录百度控制台进入https://login.bce.baidu.com/并登录。选择文字识别左上角展开->产品服务->文字识别创建应用点击创建应用按钮填写相关应用信息简单描述下应用是干嘛的就行,因为我们只需要识别文字,所以其他也不用勾上image创建好了之后可以看到具体的应用信息,记住这3个关键信息。待会会用到。appidapikeysecret key熟悉OCR文档官方文档地址: https://cloud.baidu.com/doc/OCR/s/wkibizyjk文档会写的比较清楚,简单的说就是通过你的appid,api key和secret key获取一个client,接着你就可以调用client的api去获取图片中的文字了。官方的SDK还是比较贴心的。安装SDKpip install baidu-aip讲完了文字怎么识别,接着就来说说标题中的动态图片验证码。动态图片验证码这个概念是我自己命名的,一般来说,我们的一张图片都是对应唯一一个url的,比如:https://yuque.com?image=dshqadiau(这个地址是我编的)一般来说image字段的值不同,图片也就不同,都是一串随机的或者规律的不重复数据,确保图片不会重复。但是博主最近遇到了这样一种情况:输入一个url,每次输入,拿到的图片都不一样。这样就会带来一个很严重的问题,页面上你虽然读取了图片的信息。我们把图片的url传递给百度sdk的时候,url由于再次调用,导致图片发生了变化。比如网站上显示的是: c5s3,调用百度sdk的时候,百度会通过url读取图片,但再次读取,图片可能变成了lfew。不信大家可以看看这个图片地址:每次刷新,这个图片都会变,但是url不变怎么解决呢?好在百度sdk,他不仅仅支持url,还支持图片文件和base64的图片数据。我们看看官方文档:所以此时我们用图片的base64数据就行了再回到Selenium里面,我们怎么才能获取到验证码那张图片呢?思考一下:读取img标签的src,然后下载图片,保存图片文件再转为base64很显然这个方法行不通,为什么呢?因为img的src属性就是刚才这个url,你去获取一遍url,它同样会变化。截图,裁剪出验证码部分,扔给百度去识别可行是可行,但是会不会太复杂了??如果我只对验证码的img元素进行截图,生成base64的数据是不是更方便?其实呢,selenium作为一款老牌的自动化测试工具,很多方法供大于求了。所以它是有这样的功能的!Selenium对指定区域截图我们都知道,selenium有一些截图方法。driver.get_screenshot_as_file(filename)但其实,针对元素,也是有截图方法的。伪代码如下:# 通过id获取到图片 img = driver.find_element_by_id("image") # 调用WebElement的screenshot_as_png属性方法,获取到png的数据,因为百度需要png data = img.screenshot_as_png接着我们就可以用这个获取到的图片数据去找百度要答案了!完整版代码:from aip import AipOcr from selenium import webdriver client = AipOcr("你的appid", "你的app_key", "你的secret_key") driver = webdriver.Chrome() driver.get("https://iam.pt.ouchn.cn/am/UI/Login") img = driver.find_element_by_id("kaptchaImage") data = img.screenshot_as_png res = client.basicGeneral(data, {}) print(res)更多图片识别的配置可以查看度娘文档哦可以看到,只识别到了CFX,而且图片没有继续变化了。毕竟文字识别是从图片里面找文字,而且文字会有一些横线这样的干扰,所以如果一次不行,可以多试几次。思路就是写一个while循环,不断尝试去识别验证码并登录,接着判断是否登录成功,没成功则重复上一个步骤。以我个人的经验,一般1-10次就可以成功。好了,以上是博主简单替大家尝试一下UI自动化过程中对于验证码的识别。主要重点在于验证码的识别和对部分区域截图。有兴趣的同学可以联系博主探讨哦。上一篇给点工们的进阶教程好像漏发了一些人,这里补发一下,希望大家都能够取得进步!~
React之全屏化组件介绍本文基于React+antd,给大家演示一个完整的全屏demo。起因是开发今天给我提了一个sql编辑器输入框比较小,不支持放大,不太方便。希望能够全屏显示,联想到自己以后可能也会需要,便研究并记录之。其实我觉得也没有很小(orz)image全屏大家应该都在web页面里面见过全屏按钮,点击它以后页面就成了全屏,经常会在代码编辑器中出现。可以看到有对应的数据出现上图就是leetcode全屏后的效果了,省略了菜单等内容。看起来全屏展示分为很多种,我说说我的看法。leetcode这种 它只是页面全屏F11 我们可以按F11进入全屏模式,是chrome自带的,不需要修改代码改变dom,其实和第一种一样,只不过会隐藏浏览器部分内容image如上图一样,浏览器的躯壳已经不见了。全屏的用处全屏的话,似乎当你希望全身心投入阅读的时候比较需要,就好像大家看电影也喜欢全屏一样。主要还是放大组件,让大量输入/阅读操作能够更愉快♀地进行。安装react-full-screen// yarn add react-full-screen npm install react-full-screen --save使用yarn或者npm安装这个库。官网提供了一些demo,链接在此。编写一个最简单的组件这里就直接上代码了,代码不多,很好懂。import React, { useState } from "react"; import ReactDOM from "react-dom"; import "antd/dist/antd.css"; import "./index.css"; import { FullscreenOutlined, FullscreenExitOutlined } from "@ant-design/icons"; import { Tooltip, Card, Col, Row } from "antd"; import { FullScreen, useFullScreenHandle } from "react-full-screen"; const App = () => { // 定义full变量,为的是兼容全屏和非全屏的样式,比如full的时候高度为200,非full高度为100 const [full, setFull] = useState(false); // 创建一个fullScreen的handle const handle = useFullScreenHandle(); return ( <div style={{ background: "#ececec", height: 500 }}> <Row gutter={[8, 8]}> <Col span={8}> <Card style={{ height: 500 }}>左侧card</Card> </Col> <Col span={16}> <FullScreen handle={handle} onChange={setFull} style={{ background: "#ffffff" }} > <Card style={{ height: 500 }}> <div> <Tooltip title="全屏"> <FullscreenOutlined style={{ fontSize: 16 }} onClick={() => { // 点击设置full为true,接着调用handle的enter方法,进入全屏模式 setFull(true); handle.enter(); }} /> </Tooltip> <Tooltip title="退出全屏"> <FullscreenExitOutlined style={{ fontSize: 16, marginLeft: 16 }} // 退出全屏模式并把full设置为false onClick={() => { setFull(false); handle.exit(); }} /> </Tooltip> </div> <div>假设这是一个编辑器</div> </Card> </FullScreen> </Col> </Row> </div> ); }; ReactDOM.render(<App />, document.getElementById("container"));image展示出来是这个样子,代码里面加入了注释,大家对着看即可。由于codesandbox里面不太支持,所以我放到了一个antd pro的项目里面,给大家看看效果。image这样,我们做到了只放大编辑器的效果,隐藏掉了其他不重要的部分(左侧部分)。存在的问题这样还远远不够,里面还有一些细节要优化。默认背景为黑色,不友好,我们需要设置样式我们应该在全屏模式把编辑器高度变大还有暗坑,待会再说各个击破背景色我们使用的这个库,会默认包裹一个全局的div,当全屏的时候,class为.fullscreen.fullscreen-enabled,而非全屏的时候则为fullscreen。所以我们在全局/组件的样式里面写如下的css即可:.fullscreen.fullscreen-enabled { background: #fff; padding: 24px; }image可以看到这个样式已经生效了,而且我们加入了padding,这样看起来Card就不会被挤到边上。高度我们之前设置了full变量,所以我们修改一下代码,根据full来判断高度。imageimage可以看到盒子的高度已经发生了变化。扩展部分如果你以为这就结束了,那就大错特错了。接下来我们说一说暗坑。在antd组件里面,modal/drawer/message等等都是在body中生成的dom元素,所以我们会遇到什么问题呢?在全屏模式根本就看不到对话框/消息提示等。但好在antd提供了对应的参数,控制dom的挂载元素。Modalmodal可以这么解决,我们首先设置一个full_screen的id:image注意,这个id一定要在FullScreen组件里面。接着我们在Modal.info,Modal组件里面都加入如下参数:image注意: 这里的modal我的demo里面并没有写,这个属于扩展部分。写一个modal组件也不复杂,大家可以自己尝试下。Modal.info这样的apiModal.info({ title: 'cud请求参数', width: 800, // 注意加上这个 getContainer: document.getElementById('full_screen') })message通过message.config传入getContainer方法:image这里我没找到很好的办法,每次message.info的时候都需要config一下,还是比较麻烦的。如果作为全局配置则又可能出问题,大家有更好的办法可以留言哈。
大家好~我是米洛!我在从0到1打造一个开源平台, 也在编写一套完整的接口测试平台系列教程,希望大家能够多多支持回顾上一节我们完善了一整套测试用例执行的流程,这一节我们来讲讲async方法的装饰器。在此之前,我们先来看下用于同步方法的装饰器。日志装饰器还记得我们之前编写过的日志装饰器吗?不记得的话也没关系,咱们现卤一个。import functools def log(func): @functools.wraps(func) def wrapper(*args, **kwargs): print("正在执行方法: ", func.__name__) print("参数: ", *args, *[f"{k}={v} " for k, v in kwargs.items()]) result = func(*args, **kwargs) print("执行结果: ", result) return result return wrapper @log def print_user(name, age, height=180): return f"hello, {name}. Age: {age}, Height: {height}" if __name__ == "__main__": print_user("klose", 43, height=182)我们编写了一个print_user(打印用户信息)的方法,并给他加上了log装饰器,这样一旦这个方法执行了,我们就会输出这些数据到日志里面。因为这里我没有现成的日志包,所以我用了print代替。输入到日志需要记录什么内容呢?调用了什么方法方法的参数是什么方法的返回值是什么我们来看看输出:这个装饰器的目的是当我们发现这个方法报错了,能够回溯到当时的参数信息,以便于排查问题,当然这边不会有报错,因为场景比较简单就是这么个场景,普通的装饰器实现如上。接着我们来思考一个问题,如果我们的方法是异步的,在装饰器不变的情况下,会是什么输出呢?求豆麻袋。我们来进行下简单的改造。async方法的装饰器@log async def print_user(name, age, height=180): return f"hello, {name}. Age: {age}, Height: {height}"只需要把print_user方法改为async,然后用asyncio.run执行print_user方法即可。来看下输出:@log async def print_user(name, age, height=180): return f"hello, {name}. Age: {age}, Height: {height}" if __name__ == "__main__": asyncio.run(print_user("klose", 43, height=182))image可以看到,执行结果变成了coroutine了,也就是说拿不到返回值了。那么接下来就进入我们的改造阶段,让装饰器也支持异步方法。查询资料通过google,我们发现asyncio有这样一个方法: iscoroutinefunction看名字就知道,是判断一个function是不是coroutine,如果这招有用的话,那说明我们的问题能够解决了。def log(func): if asyncio.iscoroutinefunction(func): @functools.wraps(func) async def wrapper(*args, **kwargs): print("正在执行方法: ", func.__name__) print("参数: ", *args, *[f"{k}={v} " for k, v in kwargs.items()]) result = await func(*args, **kwargs) print("执行结果: ", result) return result else: @functools.wraps(func) def wrapper(*args, **kwargs): print("正在执行方法: ", func.__name__) print("参数: ", *args, *[f"{k}={v} " for k, v in kwargs.items()]) result = func(*args, **kwargs) print("执行结果: ", result) return result return wrapper我们把代码改成这个样子: 可以看到,装饰器又和上一次讲的一样,进入了一个分水岭,当iscoroutine成立的时候,形成了一个async的装饰器,反之则是普通的装饰器。试试效果先效果良好再试试把log放到普通方法上面:注意看方法名, 执行的确实是print_user2方法代码冗余细心的朋友可能发现了,3个print的内容都是很重复的,但是ide并没有提示你,但其实这几块内容是完全可以封装起来的。这大概就是顺手为之,慢慢就形成了屎山代码吧~所以核心方法是asyncio.iscoroutinefunction, 你学会了吗?今天的内容就介绍到这里了,大家没点关注的点个关注,点了关注的点个赞,点了赞的点个在看,点了在看的点个赞赏,点了赞赏的点个退出。
回顾我们上一节已经写好了左侧数据表目录,今天继续完成sql编辑器的部分。调研组件monaco因为我们的项目用的是React,市面上很多编辑器都是js编写,react提供了一层方便的封装。比如我们在HTTP调试页面用的JSON编辑器,是以monaco为原型封装成的React组件。这个就是monacomonaco呢,是微软开源的,大家熟悉的VsCode其实内部核心也是monaco。优点是美观,专业,缺点是使用比较复杂。AceEditor用过yapi的人都知道,里面填写JSON_SCHEMA的时候用到了JSON编辑器,会校验你的JSON格式。里头的编辑器就是AceEditor。我觉得它的优点就是功能比较强大,包括代码补全,UI响应都做的很棒,唯一的缺点可能就是主题很少,不太好看。CodeMirror这是我最开始调研过的一款插件,包括大名鼎鼎的leetcode都是用的它。如果用好了自然非常牛逼,但我确实玩不太转,觉得里面的API太生硬了。React对应的实现: react-codemirror2唯一缺点就是使用困难,遇到问题不好解决。最终选型由于AceEditor我在公司实现了一套,为了避免重复造轮子,花更多的时间去搞一套新的。我决定直接搬运过来。当然如果以后有时间,我会向leetcode学习,做一个更好用的编辑器。封装编辑器组件简单看看就行接受value, 语言,改变value的事件以及高度和theme。这样我们就可以对编辑器的主题,内容等进行完美控制。还是看看下最终页面成果吧~image页面分为3块,左侧是上一节编写好的部分,右侧上半部分是编辑器,下半部分是返回结果。选中db后的效果自动补全切换主题执行后的结果渐渐地成型了一点~后端bug修复先给大家道个歉,自己上一节写的代码没有经过很严谨的测试,导致出了一些问题:问题1: 多个fat环境这个问题我现在是修复了可以看到,出现了2个fat数据。仔细找一下原因,发现是这里出了岔子:image这里我们添加好第一个fat数据后,idx=0对不,所以env_index里面的数据是这样的:env_idx = {"fat": 0}那么当第二个fat来了,会取到idx=0,判断if not idx,这里not idx自动隐式转换为True了,导致又append了一次result。所以这时候我们需要换个判断方式if idx is None,这个判断代表字典里面没有这个key。这样就ok了问题2: 天真的以为MetaData可以复用get_tables这个方法还记得get_tables里面这个metadata变量吗?之前是通过方法传递进来的参数,这样只需要实例化MetaData()一次。但是坑就坑在,后续生成的数据,也会带上之前获得的表信息,所以我们还是不能复用这个对象。改造online_sql方法前面我们已经实现过在线执行sql的方法了,但是会发现有一些问题。datetime不是我们想要的所以我们需要自行处理,拿到我们标准的datetime。编写2个新的方法,方法json_serialize是针对datetime数据进行序列化拿到字段里面的keys(),也就是列名,用于前端展示。对于update这种没有返回结果的方法没有兼容如果sql没有返回值,那一定是delete/insert/update之类的语句我们返回更新的行数就好,不管是增删还是改。添加全局loading,使得体验更好。加载数据表的过程会比较缓慢后端未来优化由于数据表不会一直修改,我们可以把他们的数据放到redis之中。这样我们效率会提高很多,加载速度也不会像现在这么慢。最后,pg的支持还不是很完善,目前来说处于待测状态,有感兴趣的小伙伴可以提供下pg的连接或者自测一下。数据库的配置需要管理员,如果你不是可以联系我,我给你添加。今天的内容就港(肝)到这里了,数据库这块总算告一段落了大家`周末happy`
大家好~我是米洛!Unittestunittest大家应该都不陌生。它作为一款博主在5-6年前最常用的单元测试框架,现在正被pytest,nose慢慢蚕食。渐渐地,看到大家更多的讨论的内容从unittest+HTMLTestRunner变为pytest+allure2等后起之秀。不禁感慨,终究是自己落伍了,跟不上时代的大潮了。回到主题感慨完了,回到正文。虽然unittest正在慢慢被放弃,但是它仍然是一款很全面的测试框架。今天在群里看到番茄卷王(公众号: 测试开发番货)的一番言论,激起了我的一番回忆。自己以前是知道unittest的执行顺序并不是按照编写test方法的顺序执行,而是按照字典序执行的。但遗憾的是我都是投机取巧去解决的问题(后面会讲)。下面我们就来探讨下unittest类的test方法的执行顺序问题。源码初窥研究一下源码(unittest.TestLoader)可以发现,在加载一个class下面的test方法的时候,原生Loader进行了排序,并且根据functools.cmp_to_key方法对测试方法列表进行了排序。image我们知道,unittest是不需要我们指定对应的方法,说白了,它是从类里面自动获取到咱们的方法,并约定了以test开头的方法都会被视为测试方法。可以看到testMethodPrefix,即测试方法前缀,如果不是test开头则直接return False查询一下self.sortTestMethodsUsing(这个是一个排序的方式)。找到对应的排序方法可以看到这个比较方法写的很明确了,如果x < y那么返回-1,x = y则返回0,x > y返回1。其实大家可能不知道Python里面的字符串也是可以比较的,在此必须说明一下字典序。我们来看看这个例子:a = "abc" b = "abcd" c = "abce" print(a > b) print(b > c)猜猜看执行结果,很显然,字典序的比较,是按A-Z的顺序来比较的,如果前缀一样但长度不一样,那么长度长的那个,字典序靠后。所以这里面a < b < c了解了字典序以后,我们就不难知道,在unittest里面它寻找case的过程可以这样简化:找到对应类下面以test开头的测试方法对他们进行字典序排序依次执行这样就不难解释为什么我们有时候写的case不按照自己想的顺序来。回到问题的本质搞清楚为什么用例会乱,那就想到对应的解决方案。由于修改源码是不太合适的,那我们有2个策略去达成目的。比如我有多个test方法:class Testcase(unittest.TestCase): def setUp(self) -> None: pass def test_1(self): print("执行第一个") def test_2(self): print("第二个") def test_3(self): print("第三个") def test_10(self): print("第四个") def test_11(self): print("第五个") def tearDown(self) -> None: pass if __name__ == "__main__": unittest.main()执行起来,按照字典序,其实是1 10 11 2 3的顺序。可以看到现在还是不对的1. 以字典序的方式编写test方法我们可以手动修改test方法的名称,这也是我早前的处理方式。也就是说把想要先执行的case字典序排到前面:class Testcase(unittest.TestCase): def setUp(self) -> None: pass def test_0_1(self): print("执行第一个") def test_0_2(self): print("第二个") def test_0_3(self): print("第三个") def test_1_0(self): print("第四个") def test_1_1(self): print("第五个") def tearDown(self) -> None: pass我们可以把数字按位数拆开,个位数就把10位补0,这样就能达到效果,如果会写100个case,我们就需要补2个0,比如0_0_1,当然一个文件里面也不会有太多case。如果遇到test_login这种怎么办呢,不是数字结尾的方法。其实是一样的,可以写成test_数字_业务的模式。番货写了一个装饰器专门解决这样的问题,大家可以去参考下。2. 回归本质,从根本解决问题方案1用了番货的装饰器,好是好,但是改变了方法本身的名称,我们其实可以针对他的排序方式入手,按照我们编写case的顺序排序测试方法,就能达到想要的目的。说说思路:手写一个loader继承自TestLoader类,改写里面的排序方法在unittest运行的时候传入这个新的loader来看看完整代码,注释里面写的很完善了。import unittest class MyTestLoader(unittest.TestLoader): def getTestCaseNames(self, testcase_class): # 调用父类的获取“测试方法”函数 test_names = super().getTestCaseNames(testcase_class) # 拿到测试方法list testcase_methods = list(testcase_class.__dict__.keys()) # 根据list的索引对testcase_methods进行排序 test_names.sort(key=testcase_methods.index) # 返回测试方法名称 return test_names class Testcase(unittest.TestCase): def setUp(self) -> None: pass def test_1(self): print("执行第一个") def test_2(self): print("第二个") def test_3(self): print("第三个") def test_10(self): print("第四个") def test_11(self): print("第五个") def tearDown(self) -> None: pass if __name__ == "__main__": unittest.main(testLoader=MyTestLoader())执行一下还是不对执行了一下还是不对,是不是哪里出了什么问题呢?是因为pycharm有一种默认的unittest的调试方法,我们要改成普通的方法去执行。这种就是unittest的专属测试模式改成data(我的py文件名称),然后点击debug按钮别选Python tests,选正常的Python搞定试试用控制台执行:也没什么毛病今天的内容就讲到这里了,看懂的记得给个赞哦~
大家好,我是米洛,一位肝帝!硬货预警!!!对肝帝不感兴趣可直接跳过。回顾上篇其实我们还在弄数据构造器,而且还没结束。其实博主我早就已经更了2篇了,只不过还没发出来。这篇我们先不管数据构造器,谈点别的。我为什么是肝帝事情就源于我周五晚上闲着没事,在寻找异步相关的类库,发现了一个比较漂亮的官方文档网站:https://aioredis.readthedocs.io/en/latest/getting-started/大概长这样细心的朋友可能发现了,这个是不是和httprunner的官网长得很像啊。就在我发现这个新大陆之后,我就一发不可收拾。首先我们观察他的域名: readthedocs.org,其实很多其他项目也有类似的官方文档。想着httprunner做了这么帅气的官网,那我们虽然不能说要赶上他(毕竟人家还是有几把刷子的),但是也要像他看齐吧,把官网整一整。于是我也有了个这样的网站:https://pity.readthedocs.io/requirements/%E5%87%86%E5%A4%87%E6%9D%A1%E4%BB%B6/image你别说,是不是还像模像样的?昨晚弄页面+写文档+今天写文档,到现在才算完。我愿称自己为肝帝,因为我昨晚3点才调好(坑多资料少)。后面我会放出文章教大家怎么弄!回到今天的话题古语有云: 天下大事,分久必合合久必分!在我们前后端这里,其实也可以分分合合!且听我慢慢道来~熟悉这个平台的兄弟集美萌应该都知道,项目的前端是React开发,独立起的一个服务,后端里面根本没有templates,static这样的静态资源数据。注意这句话: 如果说前后端分离是剥离flask/django中的static资源,那么前后端合并(我自己随便取的名)就是把它塞回娘胎里!今天就动手,立刻!!我们先来看一下我自己官方给出的好坏处。image了解路由我们做了前后端分离的项目后,前端有url比如我们常用的:/user/login,还有一些静态资源如图片, js,css等。一旦合并后,想一下是不是可能会和我们在FastApi定义的路由串了,如果他们都是中华田园犬的话,我们可以叫他们串串。前端路由类型其实,前端路由是分2种的,第一种就是我们目前的browser模式,和正常的路由没区别,比如:image但其实还有一种路由类型是hash类型,你们可能在公司也看到过:比如大名鼎鼎的element-ui就是如此看到中间那个/#/没有,那就是hash路由的标志。其实对我们用户使用来说是没有什么区别的,就是感官上的。hash路由可以给我们带来一个好处,我们可以避免串串。因为hash路由不算很规则的路由。眼瞅着内容比较多,赶紧切换到下一节。
大家好,这里是米洛,一个想和大家一起分享测试开发相关的技术,面试经验和成长经历的博主!从Flask到FastApi上次我们已经拿到了FastApi体验卡,并且搭建了一个demo服务。说好的要开始学FastApi,那怎么能从入门到放弃呢?所以我稍稍看了一下文档,理了一下他里面的门路。所以这篇文章可以算是私货吧,由官方文档加上个人理解组成。我打算先完善比较重要的功能,剩下的到用到的时候再切换就行了。为了方便大家能从Flask无缝切换到FastApi,我也经过一定的实践,结合自己的项目特意编写了这篇文章,可能有些地方没有考虑到,希望大家见谅。文章有点点长,可以不用一口气看完~留着后面啃也可以!1. 配置项目改造我们之前会给Flask的app(pity)初始化一个配置:image其实配置还有一种用法,就是直接引入Config类,利用Config.字段去获取配置项,所以我们在原项目里面取配置的方法都要修改。2. Cors跨域修改imageFlask支持跨域很简单,引入CORS,将app套进去即可。其实FastApi也不难,其中官网就有对应的例子:image通过引入FastApi自己封装好的CORSMiddleware,即可达到一样的效果。3. 支持Debug因为按照我们上一篇的内容,我们通过uvicorn启动了FastApi服务,但是由于我们是在终端(Terminal)运行的,所以其实打的断点是无法起作用的,所以我们需要通过运行main.py来达到调试的目的,官网也有类似的教程。image首先导入uvicorn库,然后通过uvicorn.run来运行对应的app,我经常提到的app,其实是一个FastApi的实例的概念。虽然我给他取名叫pity,但是我有时候也会叫他app,希望不要给大家带来困扰。注意,我这边run方法接受了4个参数,host和port就不多说了,dddd。reload呢,就是热更新的意思。至于app='main:pity',main代表的是这个文件的名字: main.py,pity也就是app的名字。main:pity即代表当前要启动的是main里面的pity。至于为什么要这么复杂,归根结底还是这个reload参数,为了能热更新,它需要这些信息,不然会报错:imageimage所以,都是被逼的。4. Flask-Sqlalchemy变更其实这个不太属于这块内容,因为有的人甚至没有用到这个模块。用sqlalchemy的同学可以跳过哦!其实解决方法呢,就是换成sqlalchemy。所以我们需要按照sqlalchemy的格式去编写ORM。修改models/init.pyimage可以看到我这边读取链接URL,是通过Config来直接获取的。改造models/user.py随便以user.py为例子:image构造函数可不变,Use类继承的对象就是models/init.py里面的Base类,需要注意的是: sqlalchemy需要tablename这样一个字段,所以我们需要给它加上,它不会默认生成,不加就报错。其他地方基本上没有差异。改写增删改查部分image以注册用户为例,改写方法是去掉以前的User.query.filter_by(),改为session.query(User).filter_by(),其他的时候差距不大。注意为什么要用with,因为with执行完毕之后会自动调用exit(),也就是会自动关闭session。5. 参数校验部分FastApi呢,和Pydantic进行了强强结合,虽然这一块我还摸得不是很清楚,不过我暂时可以用起来了。先看下旧版本的,人肉校验器:image新版本的话,等于说是把参数校验和业务逻辑解耦了,参数校验放到另外的地方去编写,接口里面只负责处理业务逻辑即可。新版本接口:image一切的核心都在于这个UserDtoimage可以看到,我们为UserDto类指定了4个字段,因为都是必填项,所以未加上默认值,如果我们需要email是非必填的,则要改成:class UserDto(BaseModel): name: str password: str username: str email: str = None接着就是具体的校验方法了,由于我们的校验规则很简单,所以对所有字段都是采取的一个方法: field_not_empty意思是字段不能为空字符串,否则抛出ParamsError,注意这个ParamsError是我自定义的错误类型,它继承了ValueError。image进阶由于我们的字段校验不通过的返回格式是这样的:image但是这个字段呢,是pydantic帮忙校验好的,所以我们需要添加这么一个方法:image这个方法是针对请求参数校验失败的处理,类似于一个hook,只有请求参数校验失败了,才会走到这个步骤。虽然里面错误信息多,但是我们只取第一条错误信息,不然数据多了展示不方便。image接着我们定义了一个错误字典,目前支持missing,params(自己封装的), not_allowed(参数类型不一致)看看效果image这样就完成了参数的校验了!6. 蓝图在http请求里,接口分类是很关键的事情,所以蓝图这块我们不能跳过,我们粗略讲一下。其实flask里面我们也只是用来给url分组,那我们这里也完成一样的事情就好了。编写接口imageAPIRouter约等于Blueprint,创建一个APIRouter实例,prefix即url的前缀。编写接口的时候从@app.route改为@router.post/get即可,变化不大。注册routerrouter的注册也很简单,和之前蓝图注册类似,通过app.include_router方法即可实现:image由于我这里只改造了user下的router,所以其他的未include进来。
引入Ant Design Pro回顾还是继续回顾下之前的作业, 返回的中文变成了ascii字符,不要紧,我们光荣地百度一哈。image随便点进去看看,都可以找到正确答案:image可以看到,我们需要修改config中的JSON_AS_ASCII字段为False。但是我们本身是没有这个配置项的,所以直接给加上就好了。pity/config.pyimport os class Config(object): ROOT = os.path.dirname(os.path.abspath(__file__)) LOG_NAME = os.path.join(ROOT, 'logs', 'pity.log') # Flask jsonify编码问题 JSON_AS_ASCII = False再试一下,可以发现问题完美解决了,这里篇幅原因就不上图了。前言在开始之前,希望读者们已经能够掌握了一部分react的知识,这里假定我们已经了解了react,redux和dva/umi相关的知识。并有做过相关练习。如果还不了解以上相关的知识,建议去以下网站学习一遍,下面给出一个大概学习的路线吧。html/css/js这里推荐大家去w3cschool稍作了解,不过看本篇文章的同学肯定都是有一定基础的。那么就当做巩固好了。es6这里我没有系统看过教程,我是在一本叫做react学习手册的书上面看到的。这里还是放一下地址吧,可以去阮一峰老师的博客看,也可以去这里学习es6的一些新特性。比如箭头函数,promise,let等。react+reduxreact的话,一开始我几乎是没有入门。拿着react-element里的demo就开始实操了,遇到了各种各样的问题。在寻找solution的时候遇到了一本名曰react小书的教程,觉得实在是再合适不过了。同时里面还有redux的部分教程(第三部分)。dvadva是一个封装了redux和router等方法的框架,掌握了它的api,可以快速完成react项目的开发。具体教程还请看 官网dvajs。Ant Design Pro介绍Ant Design(简称antd),它是蚂蚁金服的前端设计团队出品的一款UI组件库,如果要类比的话,我会把他比作bootstrap,但是它又远远比bootstrap好看且交互性更强。而antd pro,就是它的专业版。为什么呢,因为antd pro已经是一个完整的中后台项目,我们如果需要快速开发的话,直接拿着里面的页面修改便是了。但是我个人总结了一下,缺点也很明显,就是antd pro做出来的系统几乎都长一个样,长久了会有审美疲劳,至少我对bootstrap也是这样的,当然蚂蚁出的东西品质是真的棒!话不多说,先看看它的预览页面。以下是随便截取的几个图片,感受一下它的美。imageimage条件准备确保你安装了Nodejs,这样你就拥有强大的包管理工具Npm使用npm安装cnpm,因为国内有很多资源是访问受限的,所以需要淘宝开源的cnpm。npm install -g cnpm --registry=https://registry.npm.taobao.org安装yarncnpm install -g yarn安装Umicnpm install -g umi创建antd pro项目先在pity同级建立pityWeb项目, 然后进入pityWeb输入如下命令, 开始等待:yarn create umiimageimage选择ant design pro并回车image选择pro v4并回车image选择JavaScript并回车image选择simple并回车image选择ant design 4并回车安装成功截图安装antd pro依赖包进入pityWeb目录执行命令cnpm installimage尝试在本地运行antd pro尝试在本地运行antd pro在pityWeb目录下输入npm start并回车image接着浏览器就自动打开了页面http://localhost:8000(如果没有的话就手动打开),那么我们的antd pro就成功部署了。image后端代码地址: https://github.com/wuranxu/pity前端代码地址: https://github.com/wuranxu/pityWeb
背景是这样的,最近在研究一个定时任务系统的改造,可能有点像jenkins做到的那种吧。 可以输入shell命令,也可以执行py脚本等等,相比之前来说,也要能够及时停止! 但是遇到了这么个问题,golang执行py脚本的时候获取不到脚本的输出。首先来看看go里面怎么运行shell脚本吧,我比较喜欢执行全部命令。普通用法(一次性获取所有输出)package main import ( "fmt" "os/exec" ) func main() { Command("ls") } // 这里为了简化,我省去了stderr和其他信息 func Command(cmd string) error { c := exec.Command("bash", "-c", cmd) // 此处是windows版本 // c := exec.Command("cmd", "/C", cmd) output, err := c.CombinedOutput() fmt.Println(string(output)) return err }可以看到,当前命令执行的是输出当前目录下的文件/文件夹image.png实时显示效果图:image.pngpackage main import ( "bufio" "fmt" "io" "os/exec" "sync" ) func main() { // 执行ping baidu的命令, 命令不会结束 Command("ping www.baidu.com") } func Command(cmd string) error { //c := exec.Command("cmd", "/C", cmd) // windows c := exec.Command("bash", "-c", cmd) // mac or linux stdout, err := c.StdoutPipe() if err != nil { return err } var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() reader := bufio.NewReader(stdout) for { readString, err := reader.ReadString('\n') if err != nil || err == io.EOF { return } fmt.Print(readString) } }() err = c.Start() wg.Wait() return err }可关闭+实时输出package main import ( "bufio" "context" "fmt" "io" "os/exec" "sync" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) go func(cancelFunc context.CancelFunc) { time.Sleep(3 * time.Second) cancelFunc() }(cancel) Command(ctx, "ping www.baidu.com") } func Command(ctx context.Context, cmd string) error { // c := exec.CommandContext(ctx, "cmd", "/C", cmd) c := exec.CommandContext(ctx, "bash", "-c", cmd) // mac linux stdout, err := c.StdoutPipe() if err != nil { return err } var wg sync.WaitGroup wg.Add(1) go func(wg *sync.WaitGroup) { defer wg.Done() reader := bufio.NewReader(stdout) for { // 其实这段去掉程序也会正常运行,只是我们就不知道到底什么时候Command被停止了,而且如果我们需要实时给web端展示输出的话,这里可以作为依据 取消展示 select { // 检测到ctx.Done()之后停止读取 case <-ctx.Done(): if ctx.Err() != nil { fmt.Printf("程序出现错误: %q", ctx.Err()) } else { fmt.Println("程序被终止") } return default: readString, err := reader.ReadString('\n') if err != nil || err == io.EOF { return } fmt.Print(readString) } } }(&wg) err = c.Start() wg.Wait() return err }效果图:image.png可以看到输出了3次(1秒1次)之后程序就被终止了,确切的说是读取输出流的循环结束了。执行Python脚本(阻塞)其实很简单,只要python -u xxx.py这样执行就可以了, -u参数简单的说就是python的输出是有缓存的,-u会强制往标准流输出,当Python脚本阻塞的时候 也不会拿不到输出!其他"bash" 和"-c",据我的观察,这2个参数代表在当前cmd窗口执行,而不加这2个参数,直接上shell的话,会启动一个新窗口,目前观察是stdout拿不到数据。仍有缺陷上面的命令可以解决大部分问题,但是获取不到stderr的信息,所以我们需要改造一下。 下面是输出和错误一并输出的实时读取,类似于jenkins那种。package main import ( "bufio" "context" "fmt" "io" "os/exec" "sync" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) go func(cancelFunc context.CancelFunc) { time.Sleep(3 * time.Second) cancelFunc() }(cancel) Command(ctx, "ping www.baidu.com") } func read(ctx context.Context, wg *sync.WaitGroup, std io.ReadCloser) { reader := bufio.NewReader(std) defer wg.Done() for { select { case <-ctx.Done(): return default: readString, err := reader.ReadString('\n') if err != nil || err == io.EOF { return } fmt.Print(readString) } } } func Command(ctx context.Context, cmd string) error { //c := exec.CommandContext(ctx, "cmd", "/C", cmd) // windows c := exec.CommandContext(ctx, "bash", "-c", cmd) // mac linux stdout, err := c.StdoutPipe() if err != nil { return err } stderr, err := c.StderrPipe() if err != nil { return err } var wg sync.WaitGroup // 因为有2个任务, 一个需要读取stderr 另一个需要读取stdout wg.Add(2) go read(ctx, &wg, stderr) go read(ctx, &wg, stdout) // 这里一定要用start,而不是run 详情请看下面的图 err = c.Start() // 等待任务结束 wg.Wait() return err }image.pngwindows输出乱码问题参考资料: https://blog.csdn.net/rznice/article/details/88122923最后给一个解决windows乱码的完整案例需要下载golang.org/x/text/encoding/simplifiedchinesepackage main import ( "bufio" "fmt" "io" "os/exec" "sync" "golang.org/x/text/encoding/simplifiedchinese" ) type Charset string const ( UTF8 = Charset("UTF-8") GB18030 = Charset("GB18030") ) func main() { // 执行ping baidu的命令, 命令不会结束 Command("ping www.baidu.com") } func Command(cmd string) error { //c := exec.Command("cmd", "/C", cmd) // windows c := exec.Command("bash", "-c", cmd) // mac or linux stdout, err := c.StdoutPipe() if err != nil { return err } var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() reader := bufio.NewReader(stdout) for { readString, err := reader.ReadString('\n') if err != nil || err == io.EOF { return } byte2String := ConvertByte2String([]byte(readString), "GB18030") fmt.Print(byte2String) } }() err = c.Start() wg.Wait() return err } func ConvertByte2String(byte []byte, charset Charset) string { var str string switch charset { case GB18030: var decodeBytes, _ = simplifiedchinese.GB18030.NewDecoder().Bytes(byte) str = string(decodeBytes) case UTF8: fallthrough default: str = string(byte) } return str }
如题采用了antd最为最新的ui组件(之前是bootstrap), 其他地方暂时还没有修改,新老效果对比如下:新报告:image.png下载地址: template.html旧报告:image.png下载地址: template_old.html对于使用webtest的同学们,下载新版本报告替换到对应的template目录即可。 也可以git pull一下,拉取最新版本代码。修改如果有你自己的字段需要调整,可以参考jinja2的语法进行调整。 实际上这是个模板文件,填入你具体的数据进来即可。
Apifox今天闲来无事,看到了这款工具。相当于出个小评测吧,说下自己的感受。先放地址:Apifox官网具体的介绍都可以在官网看到,官网会比我详细,下面来说说我自己的感受。感受首先呢,这个工具对于yapi、postman和jmeter的使用者来说肯定是很友好的,因为操作流程基本上一致。 大家可以看到他的首页,基本上长得很想postman的"汉化版":image.png但是, 他们也有区别postman是一个十分纯粹的http调用工具,只需要输入http的url、headers、body等信息即可,他十分轻量,虽然后面也迭代了不少的关于test的功能。 总的来看,这个工具是对接口文档/接口请求/接口测试的一次整合。 当然这种整合也带来了一些麻烦,也就是说你需要录入一个接口的相关信息才能完成对这个接口的请求。因为这毕竟是针对团队,针对自身接口的一个测试工具。所以情有可原,复杂点能够理解。 可是,当我觉得它复杂化了postman的时候,我发现它也有快捷调试的功能。因为有时候很多接口,可能是第三方的,比如我常常试用的[百度api](https://ai.baidu.com/) (这个是宝藏api),我可能只是需要简单调用下,并不想那么麻烦,可能只需要随手一个调用即可。原来这个便捷调试也是存在的:image.pngimage.png打开后长这样, 这才是最纯粹的postman呀!还是汉化版,爱了爱了! 那么有的同学可能会有疑问了,既然你就是个汉化版的postman,为什么我不用postman呢?我想这个工具吸引我的可能有以下几点:它解决了我平时不写接口文档的问题,因为平时我自己开发接口完了以后,用postman去调试一下,后续可能需要搜索才能找到对应的数据,而且别人根本不知道这个接口怎么调用,只能口口相传。它支持换肤功能,对比postman,虽然二者UI类似。它的测试功能强于postman,支持直接导入接口文档当做测试用例。它完全支持mock功能,这点基本上和yapi提供的一致,如果后端接口还没编写完毕的时候前端已经可以开始通过mock开始调试了。如果有现有的接口文档工具,它支持了大部分主流数据的导入,比如yapi、rap、swagger等,接入门槛很低。写在最后这个工具,挺适合小团队,比如2-3人的项目组且公司没有内部统一的接口文档管理平台比如yapi或者rap2或者自研的,例如我要和一个朋友开发一个项目,那我觉得这个就很合适,适合自测,也适合前端提前介入开发,还能输出文档。 但是如果只是个人开发自己的项目或者很简单的项目,在接口数量不多的时候,对测试要求不高,不需要花费时间写测试脚本的时候:postman还是最合适的工具看后续apifox还有新的迭代计划,比如完成性能测试相关,但那时候我觉得可能会收费了吧。 对于这种高级功能。其实本人比较欣赏他们的UI设计,毕竟是一个公司的产物,整个设计相对来说还是比较美观的。也期待他们的后续吧!
前言在开始之前,希望我们已经掌握了一部分react的知识,由于没有太多经验,其实我也是属于摸索阶段。这里假定我们已经了解了react,redux和dva/umi相关的知识。并有做过相关练习。如果还不了解以上相关的知识,建议去以下网站学习一遍,下面给出一个大概学习的路线吧。html/css/js这里推荐大家去w3cschool稍作了解,不过看本篇文章的同学肯定都是有一定基础的。那么就当做巩固好了。es6这里我没有系统看过教程,我是在一本叫做react学习手册的书上面看到的。这里还是放一下地址吧,可以去阮一峰老师的博客看,也可以去这里学习es6的一些新特性。比如箭头函数,promise,let等。react+reduxreact的话,一开始我几乎是没有入门。拿着react-element里的demo就开始实操了,遇到了各种各样的问题。在寻找solution的时候遇到了一本名曰react小书的教程,觉得实在是再合适不过了。同时里面还有redux的部分教程(第三部分)。dvadva是一个封装了redux和router等方法的框架,掌握了它的api,可以快速完成react项目的开发。具体教程还请看 官网dvajs。antd pro介绍antd是ant design的缩写,顾名思义,它是蚂蚁金服的前端设计团队出品的一款UI组件库,如果要类比的话,我会把他比作bootstrap,但是它又远远比bootstrap好看且交互性更强。而antd pro,就是它的专业版。为什么呢,因为antd pro已经是一个完整的中后台项目,我们如果需要快速开发的话,直接拿着里面的页面修改便是了。但是我个人总结了一下,缺点也很明显,就是antd pro做出来的系统几乎都长一个样,长久了会有审美疲劳,至少我对bootstrap也是这样的,当然蚂蚁出的东西品质是真的棒!话不多说,先看看它的预览页面。以下是随便截取的几个图片,感受一下它的美。image.pngimage.pngimage.png下载antd pro以上内容均来自antd pro官网。第一步: 克隆项目,我们将之克隆到Lamb的client目录下,由于我不想在Lamb中创建2个git项目,所以我选择去github下载代码。image.png如果你想随时升级antd pro或者给它们提pull requests,则执行以下命令。$ git clone --depth=1 https://github.com/ant-design/ant-design-pro.git my-project $ cd my-project下载后解压并放入Lamb/client目录。image.png安装依赖包确保你安装了nodejs,这样你就拥有强大的包管理工具npm使用npm安装cnpm,因为国内有很多资源是访问受限的,所以需要淘宝开源的cnpm。在cmd/shell窗口输入并回车。npm install -g cnpm --registry=https://registry.npm.taobao.org安装antd pro依赖包进入Lamb/client目录执行命令cnpm installimage.pngimage.png尝试在本地运行antd pro在Lamb/client目录下输入npm start并回车image.png接着浏览器就自动打开了页面http://localhost:8000,那么我们的antd pro就成功部署了。image.png
语言为什么还是Python?它是我真正用来谋生的一门语言。广度和深度的把握,一直以来我都是学得多但是深入的少。这门语言撩完撩下一个,需要深入学习。Python简单易学, 受众面广,现有技术框架更成熟,代码量更少。这虽然是我个人的一个学习项目,但是也可以给大家参考。web框架这里我仍然打算用Flask,源于17年的时候做过一段时间web开发,使用flask的话个人觉得比django稍微简单好上手一点。工具工欲善其事必先利其器,所以我这里采用Pycharm做开发工具,其实用什么不重要,sublime也好,记事本也好,只要开发效率高,顺手就行。大致目录结构(后续可能发生变化)image.pngserver这是Lamb的后端服务,由app、logs、config.py、run.py组成。config.py这是一个存放项目配置的文件,由于目前配置较少,只存放了当前项目的根路径和日志文件名。app目录controller控制器层,我个人的理解,这里使用blueprint编写路由信息,通过dao进行数据处理并返回http response。dao这里主要是负责和db层的交互。dbdb主要是mysql, mongo等连接类。其中会用到flask_sqlalchemy简化我们对db的读写操作。middleware主要处理mail, mq等。models根据sqlalchemy将db表映射至Python cls对象。utilsapp中需要使用的工具包如log和装饰器类等。logs日志文件存放。requirement.txt用例包含库,类似node的package.json。
接口文档生成流程 介绍 目前我们QA在测试过程中, 存在着接口文档不全或有出入(包括更新)的情况。 这时候我们一般会阅读开发编写的代码或者直截了当去问开发。 这2种方法的弊端都很明显, 即增加了沟通和时间成本。 自己看代码且不论QA对于开发语言的熟悉程度, 有的代码QA并不可见。独自研究费时费力, 去找开发询问的时候,得问到对应的人, 他们还需要花费时间精力去搜寻。 ==现在, 这些问题都将迎刃而解==。 原理介绍 通过swagger插件(如jar包)解析编写了接口注解的java代码, 而后通过生成的swagger.json文件解析出接口信息并导入接口文档管理工具(yapi)。 第一步: 编写注解 swagger是一个较为流行的接口文档管理工具, 但是这里我们不打算将他作为我们的大方向。其实接口文档的核心基本都已固定, 如path(route), 参数, 响应, 请求方式等。swagger在这点做得相当不错, 使用json-schema约束json字段的属性(required, example, type等)。 简而言之, 第一步就是通过注解对java中各个字段的参数做了约束, 通过插件生成json文档。 下面我们来看一个例子: example地址, 这是一个长得像外国人的中国老哥 我们来看下注解的具体实现 package com.github.kongchen.swagger.sample.wordnik.resource; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.annotations.*; import com.github.kongchen.swagger.sample.wordnik.model.LoginData; import javax.ws.rs.FormParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; @Path("/login") @Api(value = "login", description = "登录接口") @Produces({"application/json"}) public class woodyTest { @POST @ApiOperation(value = "用户登录") @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid ID supplied"), @ApiResponse(code = 404, message = "Order not found")}) public Response getSuite( @ApiParam(value = "登录请求json参数", required = true) LoginData data) { System.out.println(data); return Response.ok().entity("").build(); } } ==图中的@POST, @ApiResponses, @Path等@== 意味都比较显著了吧, 因为我的java只有一点点语法基础, 所以理解可能有点出入, 我这里简单理解为注释的意思。如有不对求指教=。= 接下来我们来看看LoginData怎么写。 package com.github.kongchen.swagger.sample.wordnik.model; import io.swagger.annotations.ApiModelProperty; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.util.Date; @XmlRootElement(name = "LoginData") // 这是xml的信息, 我这里都去掉了不用 public class LoginData { @ApiModelProperty(value="用户名", name="user", example = "wuranxu") private String user; @ApiModelProperty(value="用户密码", name="pwd", example = "wodemimajiushimeiyoumima", required = true) private String pwd; @XmlElement(name = "user") public String getUser() { return user; } public void setUser(String user) { this.user = user; } @XmlElement(name = "pwd") public String getPwd() { return pwd; } public void setPwd(String pwd) { this.pwd = pwd; } } 这个类里面, 有user和login属性, 分别给属性加了类似这样的注解 @ApiModelProperty(value="用户名", name="user", example = "wuranxu") 这里就是字段的约束。 第二步: 通过注解生成swagger.json 下载第一步那个小哥给出的demo, 解决好pom文件的依赖后。 在demo目录执行: mvn clean compile 可以看到图中目录生成了swagger.json 来看看生成的json 第三步: 导入yapi 先来介绍下yapi吧~ yapi是去哪儿的大前端团队开发,基于react+antd的一套接口文档管理工具。给个掌声, 真的很良心。 具体地址 大家可以试用了感受一下。 至于不需要yapi, 钟爱原生swagger的童鞋, 也可以直接将swagger.json放入你的本地swaggerUI中查看接口文档啦。 那么附上一张swagger的截图吧... 后话 其实缺点就是开发需要在每个model的类加上注解, 写每一个接口也需要注解, 开发不好惹千万千万不要推:) 当然还有第四步啦, 因为···这些都是手动干的啊, 没人有那么多精力去手动维护这些破json。预知后事如何, 请看下集预告。(写文档码字太久要去干活儿了)
goBoss 基佬github地址 这是基于go语言编写的一款boss直聘机器人软件(牛人版)。附上Python版本, 无需配置Go环境, 我会提供windows和macos的可执行程序。不喜勿喷O(∩_∩)O~ 闪光点 自动回复boss消息 回复消息有3种类型。可自行修改, 传入关键字即可(忽略大小写如b站)。消息同一个人只会回复一次。 大厂 普通 黑名单 自动发送简历 当自动回复以后, 大厂的回复中包含"简历"的子字符串, 则会自动发送您的附件简历。 自动刷新消息 随时已读, 给人随时随地无时无刻不在的感觉。 效果图 自动回复(这里我特意注册了招聘者的号) map中key为boss名字, 可能会有重名情况。但是目前我只遍历前5条数据, 暂时还能用。value为发送消息/简历的状态, 如果key未找到说明没有回复过这个人, value为false代表简历未发送但是消息已发送, true代表消息和简历都已经发送。 快速开始 下载 git clone https://github.com/wuranxu/goBoss.git 下载zip文件并解压 修改json配置文件 百度API文字识别(每日500次免费),进入官网申请并配置。 app_id api_key secret_key 用户密码配置 user(boss直聘手机号) password(boss直聘登录密码) 其他配置 下面是我本人的配置, 注意, star_reply字段里的第一个%s代表对方姓名, 第二个%s代表对方公司名。如果去掉的话会报错(设计如此, 后续可修改), 黑名单我就不放出来了哈。O(∩_∩)O~ { "star_company": [ "百度", "阿里", "口碑", "天猫", "盒马", "UC", "淘宝", "蚂蚁", "支付宝", "今日头条", "字节跳动", "腾讯", "滴滴", "bili", "美团", "点评", "饿了么", "京东", "喜马拉雅", "盛大", "拼多多", "链家", "58", "沪江", "bili", "哔哩", "二三四五", "2345", "猫眼", "陆金所", "小红书", "七牛", "musical", "虎扑", "小度", "唯品会", "苏宁", "平安", "携程", "有赞", "哈罗", "运满满", "蔚来", "巨人", "游族", "易果", "爱奇艺", "美味不用等", "号店", "360", "拍拍贷", "b站", "网易" ], "star_reply": "%s您好, 十分荣幸能受到大厂: %s的亲睐, 这是程序自动下发的消息, 如果您需要我的简历, 请在回复中带上\"简历\"字样。项目地址:https://github.com/wuranxu/goBoss", "black_reply": "您好, 暂时没有兴趣, 抱歉~", "common_reply": "您好, 这是一条由直聘机器人自动发送的消息, 请等待我本人查看..." } 下载chromedriver chrome浏览器是使用本软件的前提, 我们需要chromedriver驱动 前往淘宝镜像 选择与你浏览器版本对应的驱动 如我的版本是66, 确实很6哈哈哈哈... 找到适配版本和操作系统的浏览器驱动 可以看到, 2.38和2.39都满足我的需求, 现在我选2.39. mac/windows选择自己的驱动 将下载好的驱动放入driver目录里 运行 之后就可以双击main.exe(windows)或者main挂起你的聊天机器人了。 注意: windows下要用管理员身份开启main.exe, 而且最好杀毒软件信任。 todolist 还有很多不完善, 没做好的。之后填坑, 首当其冲就是解决用户需要手动安装浏览器驱动的问题。 发简历后邮件通知 低薪过滤 工作地点筛选 chromedriver自动下载 对方连续发送表情时会接收不到新消息的bug(因为表情不是文本, 在web页面属于icon) 去除time.Sleep这种丑陋的等待元素方式
前言 本文是我在公司总结的一点点个人建议, 可能有非常多的遗漏, 先记录下来这时候我的理解。公司是做共享单车业务的, 所以场景基本上也可以复用, 毕竟大家都骑过单车。注明: code是我司接口返回的标志。 编写之前 接口相关(这块总结不全) 了解接口的功能及其使用场景(正常/异常)及接口具体做的事情。 接口实现了什么功能 接口是否有操作了数据库对应字段 接口是否有操作了redis对应key 接口的入参 包括必填项和选填项丢失/多余带来的影响, 入参字段的长度是否有限制, 如身份证姓名等 接口的出参 包括正常/异常场景下code, msg等字段的校验, 如有返回数据, 对返回数据的校验如何去做 接口的设计是否符合功能的预期 如数据不允许重复时, 连续调用接口2次是否会插入2条数据 场景准备 掌握每个场景所需要的前置条件 如关锁接口在 正常使用时,他的前置条件为该车辆的锁已经打开。 考虑如何设计场景 可选择数据库/redis添加测试数据或调用接口新增数据的办法(==接口之间会存在依赖, 一旦添加数据的接口出错, 此场景也无法验证==) 用例数据准备 尽可能的动态准备测试数据 如车辆编号, 可选择从数据库捞取。如有身份证号+姓名这种较为复杂的数据, 可写在变量里。但需要多挑选几组数据, 随机读取 数据依赖 优先采取新增数据的方式, 保证之前数据完好, 新增数据如有name等字段, 可带上特定标识+时间戳的方式。在用例执行完成之后将其清除, 如果出现垃圾数据, 也便于使用定时任务进行清理。 尽量不要把数据写死 断言 对比较重要的字段作断言, 如需要展示给用户的字段。 http状态码校验 code/msg校验 db校验(业务相关, 如无类似情况可忽略) 存在接口名返回与数据库不一致的情形, 应以接口为主。db目前多使用下划线式, 接口出参常使用驼峰式。编写sql查询语句的时候, 使用select ride_type as rideType此类。 异常场景db校验 为了防止: 接口出参返回code不为0, 但db却被修改。 redis校验 如有涉及到redis, 需要对redis字段做断言。 最近比较火的异步接口 异步接口如何做断言, 本人没有太多接触。由于http协议是无状态的, 异步接口一般是调用后将任务放入消息队列, 接口就成功返回了。我的理解是去检查消息队列是否存在消息, 如有如果被消费了, 可起一个收尾类似tearDown的用例专门针对异步接口, 当他们消费完毕之后, 再对数据库/redis进行相关校验。 开始编码 编码 gat是公司内部封装的基于golang的自动化测试框架, 其实只封装了http请求和做了一部分单元测试框架的工作。 用例描述 用例编写之前, 脑海里应该有以下几点。如何设计场景, 覆盖哪些场景, 如何做断言。可以在文件顶部, 写入自己的思路, 这样在编码过程中会游刃有余, 不至于乱了方寸。之后维护的时候也不至于被业务逻辑绕晕。 setUp和tearDown 目前gat框架是由TestFuncName为入口, 我们可以在函数开始执行后, 调用setUp()函数, 将自己想处理, 想得到的数据都处理完成。再后面就是逻辑的代码, 到最后使用tearDown进行清理。 用例名称与Action对应, 文件名尽量与结构体名一致 大体结构 注释要多写, 常用方法可以封装 /* 测试功能点: 检查用户行程 覆盖到的场景: 1. 用户正在骑行中 2. 用户未骑行 数据准备: 这里填写, 你将怎样制作数据 数据清理: 这里填写, 你将如何清理脏数据 用例执行流程: 这里写你的执行思路, 首先检测什么测试点, 然后.... 断言: 写出断言的标准, 理由, 如何做(这也是评审的一部分) */ package UserCenter import ( "fmt" "testing" ) type struct UserRideCheck { Data []map[string]interface{} // 测试数据 Action string //调用接口名 } func (u *UserRideCheck) setUp() { fmt.Println("用例正准备执行!") } func (u *UserRideCheck) tearDown() { fmt.Println("用例执行完毕, 正在清理!") } func (u *UserRideCheck) TestUserRideCheck() { setUp() // 初始化 //主逻辑, 可再封装函数 defer tearDown() // 清理(后续可添加Recovery防止用例失败阻塞) } func init() { // 自己框架添加用例的逻辑 data := initStruct() testcase.Cases["UserCenter"] = append(testcase.Cases["UserCenter"], data) } func initStruct() *UserRideCheck{ return &UserRideCheck { Action: "user.ride.check" } } // unittest func UnitTestUserRideCheck(t *testing.T) { u := initStruct() u.TestUserRideCheck() } 附加: 如果可能的话, 对开发做代码走查, 尽可能覆盖其if else分支 我们自身的代码也会出错, 我们需要用日志记录测试过程中接口出现的问题以及自己的问题 如果可以, 与CI结合
茫茫题海好像旷野,突发奇想,想要记录一下曾经面试被虐的自己,看看自己当时是怎么被花式吊打的... Python常见的几个面试题 值传递和引用传递 下面代码会输出什么: def f(x,l=[]): for i in range(x): l.append(i*i) print l f(2) f(3,[3,2,1]) f(3) 我的错误答案: 都是值传递(来自16年8月左右的菜菜的自己) 类的成员变量和继承 class Parent(object): x = 1 class Child1(Parent): pass class Child2(Parent): pass print Parent.x, Child1.x, Child2.x Child1.x = 2 print Parent.x, Child1.x, Child2.x Parent.x = 3 print Parent.x, Child1.x, Child2.x 我的答案, 哈哈哈,有点羞耻···child1的值我一直以为是沿用Parent的,来自16年8月笨笨的自己 lambda惰性相关 def multipliers(): return [lambda x : i * x for i in range(4)] print [m(2) for m in multipliers()] 我的答案: [0, 2, 4, 6] 面壁面壁 实际答案: [6, 6, 6, 6] Git 之前用的啥版本控制工具 答svn, git有了解一点,但是没有实际投入使用。 git 拉取代码用啥命令 答: clone... 哈哈哈,无力吐槽,一时想不起pull答了个clone, 面试官还能坚持问我我还是挺佩服的。 requests requests里怎么传递post参数的 答: 我通过params传递的 params = json.dumps(dict) 那用json参数可以吗 答: 没有试过, 应该不可以吧。(orz, 我现在都是用json参数来传了) 这家就到这里了,很显然没有然后了。。 未完。。。
技术栈 后台: gin(golang) 前端: react+antd+dva 问题 前端这边使用fetch发送http请求的时候,后端解析formData报错: multipart: NextPart: EOF 分析问题 原因是上传文件太小了Content-Length数量太小了,尝试将headers里这字段的value变大,发现实际的请求依然是较小值。 解决方法 检查fetch参数的headers有没有自动添加Content-Type, 有的话去掉。参考此篇文章,默认设置了Content-Type还有其他的字段时会引起fetch无法控制你的Content-Type 起因 查看Antd里的上传文件的demo, 发现里边使用的是reqwst上传,而我本地用的是Antd pro封装的request(fetch),按照demo,formData放在data字段,而fetch应该接受的是body字段。但是服务端报错说的是Content-Type未识别,所以才手贱去加Content-Type引发了一系列报错··· 参考链接 点此查看
安装zlib相关依赖 解决zipimport.ZipImportError: can’t decompress data和pip3 ssl证书问题 sudo yum -y install zlib* sudo yum install openssl-devel 找到Python3下载链接 进入官网 选择对应版本 选一个你喜欢的Python3版本, 建议3.4以后(这里我选择3.6) 选择压缩包 右键赋值链接地址 比如这里是https://www.python.org/ftp/python/3.6.5/Python-3.6.5.tar.xz 下载 wget https://www.python.org/ftp/python/3.6.5/Python-3.6.5.tar.xz 解压 tar xvf Python-3.6.5.tar.xz 进入目录编译 configure sudo ./configure make sudo make install sudo make install 安装完成
Boss 基于Python3的找工作利器--Boss直聘来消息邮件通知, 自动发送简历脚本,O(∩_∩)O~ 无聊写的,因为有时候觉得找工作心急如焚,想自动回复自动发简历啊有木有~~~ github地址 效果图 程序运行日志图 邮件展示图 快速开始 下载 git clone https://github.com/wuranxu/Boss.git 下载zip文件并解压 修改json配置文件 百度API文字识别(每日500次免费),进入官网申请并配置。 app_id api_key secret_key 用户密码配置 user(boss直聘手机号) password(boss直聘登录密码) 邮箱配置 sender(发件人邮箱账号,需要选择126邮件, 否则需要更改host='smtp.126.com') sender_pwd(发件人邮箱密码) receiver(收件人) 使用 进入boss目录, 执行命令 pip install -r requirements.txt python boss.py (如果出现安装失败, 请及时升级pip) 其他配置说明: retry(百度ocr识别出错时重试等待时间) delay(获取boss消息等待时间, 单位: 分钟) auto_resume(是否自动发送简历) black_list(黑名单配置) 其他url(抓取职位及消息所用) 原理 requests生成session, 访问boss直聘网页版 利用beautifulsoup解析网站, 获取到图片验证码 调用百度ocr的图片识别api, 识别网站验证码 模拟用户登录(为什么不用selenium或者phantomJs, 因为比较笨重) 持续监听历史消息, 有新消息且不是自己发送时,发邮件通知收件人(包括职位, 薪资等信息) 当配置里的自动发简历为true时会在手动boss消息时自动调用发简历的接口 问题 百度识别率不是很高哦 由于boss直聘防止骚扰,所以在只有双方都有回复时才会发送消息 有多条消息同时到来时只会读取一条消息 由于邮件限制, 当消息火爆时邮件可能被视为垃圾邮件而导致发不出去 由于boss的聊天协议采取的是websocket并加密,所以不太好揣测它的规则,导致无法自动回复消息(简历也会受影响) TODO 薪资配置(低于多少K咱直接不看他) 心动公司(大厂)设置 多个百度Key轮流使用
简介 虽然Python有很多连接mysql的库,比如mysqldb, pymysql~这些都很方便,现在就教大家使用mysql的官方库来操作mysql. 安装 windows: 下载链接 选择自己的windows版本和Python版本,下载msi文件后双击安装即可。 (如果在网站没找到msi版本的话,试下pip install mysql-connector-python) mac os: 在终端运行:pip3 install mysql-connector-python 使用 在写文章的时候,特意去看了一下官方给的参数配置,看到passwd和password这种参数都能识别,因为官方为别的mysql库做了兼容,赞一个~~ 接下来就展示demo了,不过没有做封装处理哦~ import mysql.connector as mysql # 连接数据库, 此处可指定dbname, 但是因为需要关联到其他库的表,所以未填 conn = mysql.connect(host="127.0.0.1", port=3306, user="root", passwd="your_pwd") # 获取游标 cursor = conn.cursor() # 查询, 例如查询logistics.users表的所有信息 sql = "select * from users;" cursor.execute(sql) # 取出该查询语句返回的所有结果, 也有fetchone和fetchmany方法 cursor.fetchall() # 删除 sql = "delete from logistics.users where username=%s" cursor.execute(sql, params=("woody", )) # params参数为一个元祖, %s用于接收此参数 cursor.commit() # 增删改此种操作之后需要commit # 关闭连接 cursor.close() conn.close() 最近的小发现 因为需要比对redis里存储的json数据是否与sql数据一致,但是由于sql取出的数据是元祖类型,而且没有对应的字段名,所以很是头疼。 解决办法: 获取游标的时候添加一个参数!!! cursor = conn.cursor(dictionary=True) 效果图
介绍 这个模板改编自这位外国老哥 效果图 错误截图 录像 失败的case可以点击"view"查看报错信息, 也可以点击screenshot查看截图信息,更可以点击replay查看该条用例的一个完整运作过程! 加入了测试环境 加入了截图和录像功能 加入了echarts也就是这个大饼 觉得好看的可以联系我,哈哈哈!!!
脑洞 最近脑洞有点儿大,最开始是想给自己的测试平台添加手动执行用例的功能,又觉得没有一个很好的展示,所以想着要实时展示手机上的内容,输出到web页面,但是觉得有点难啊。 想了一下,还是换个方式吧,每个case执行完毕,都可以看到实时的录像。这样也许可行,哈哈哈! 准备条件 一台安卓机 appium环境 基础知识 adb命令 adb大家应该很熟悉,简单的说就是个连接手机和电脑的工具,哈哈哈,简单粗暴。 我们常用的adb命令有: adb devices 查看已经连接上的安卓设备 adb logcat 查看安卓日志 具体的大家可以自己去百度下。 不过这里要讲的是adb录屏的命令~ 我们连上安卓手机后, 在cmd窗口输入命令:adb shell screenrecord /sdcard/test.mp4 这时候其实手机上的内容已经开始录制了~ 我为了偷懒,就照搬这位兄弟的博客了~~!传送门在此 注意 这里的adb命令开始录制以后,不管是否正常结束,都会保存文件,所以我们可以把录制时间放长一点,到时候关闭就好了。 思路 因为用例是以一个class为单位的,就算这个class有很多个test开头的测试用例函数,所以我这里是以一个class类为单位存储视频的。 第一步 用例在setUp的时候,开启录制~ 第二步 用例在tearDown的时候, 结束录制~ 第三步 用例结束录制以后,使用adb命令将mp4文件拉取到本地硬盘上,供web页面展示,或者将本地文件放入测试报告里作为超链接访问。(但是此种方法不被Chrome浏览器支持) 开始劳作 import unittest from time import sleep class TestCase(unittest.TestCase) @classmethod def setUpClass(cls): print("[{}]--正在执行登陆初始化操作: {}".format(datetime.now().strftime( "%Y-%m-%d %H:%M:%S"), cls.__name__)) # 录屏 cls.replay = subprocess.Popen(r"adb shell screenrecord " r"/sdcard/{}.mp4 --time-limit 600".format(cls.__name__), creationflags=subprocess.CREATE_NEW_CONSOLE) def test_01(self): pass # 这里其实最好多一些操作,不然视频一下就结束了,看不到效果 @classmethod def tearDownClass(cls): # 终止录像 cls.replay.terminate() # 这里我的cls.conf是个配置文件, 获取到录像保存的路径然后mp4的名字是以用例class命名的 replay_path = os.path.join(cls.conf.get_value("replay_path"), "{}.mp4".format(cls.__name__)) sleep(2) # 这里的pull是指从sdcard获取文件到本地硬盘, subprocess.Popen(r"adb pull /sdcard/{}.mp4 {}".format(cls.__name__, replay_path), creationflags=subprocess.CREATE_NEW_CONSOLE) # 等待视频拉取完毕 sleep(6) 注意: 第一个sleep 为了等用例停止录制后有个缓冲时间~ 第二个sleep 为了pull的时候等文件完全pull完毕,不然下一个case开始的时候又会调用adb,这个pull还没结束,第二个adb又开始了,导致拉取的文件异常,无法播放。 subprocess 这个是Python调用控制台命令的方法,后面的CREATE_NEW_CONSOLE是新起一个命令窗口。 优化: sleep 应该有更好的办法解决, 暂时还没有考虑到更好的办法~ 手机垃圾文件清理 暂时还没有做
Web/app端自动化测试 做了一段时间的Android自动化测试,对比个人之前做的web端自动化测试,有一些感想。(由于个人接触的时间也不是太久,很多东西理解也并不深刻,先写下菜鸟时期的感想。) 区别 1. 启动差别 app端:在执行用例的时候,1部安卓手机同一时刻打开一个apk包,可以理解,因为比如你在做王者荣耀的测试,那么你的apk肯定只有1个在主屏幕显示,其他apk在后台继续运行,但是你无法对他们做操作。 web端:web端就不太一样了,在web端,我们可以通过Python多线程(或多进程)同时开启几个浏览器,让selenium对多个浏览器进行操作,同样100个测试用例,如果均匀分布在3个浏览器进行,那么测试效率会有所提高,而且只需要一台电脑就可以测试多种浏览器。 2. 安装检查 app端:这点感觉很像c/s架构软件,因为app是需要安装了才能使用的,所以软件是否安装异常,也是需要检查的一个点。 web端:不需要安装,在浏览器中输入url就可以测试。 3. 页面元素操作 app端:只会显示在手机页面里加载出来的部分,比方说有的页面比较长,需要向下滑动才能看到更多信息,此时需要保证不可见的元素显示在手机页面才能对它进行操作。 web端:不太一样,如果控件不是下拉产生的异步加载,那么我们是可以对屏幕内不可见的元素做操作的。因为虽然页面上没有加载出来,但是html页面实际上已经有了。如果遇到需要下拉才能加载的页面,可以用js操作滚动条。 4. 元素定位 app端:基本操作和web端差不多,不过部分定位方式不支持,比如css_selector和link_text,使用的时候会提示方法还没实现,也多出了accessible_id这种新的定位方式,不过目前我还没有用到,以后再补充。 web端:基础的就name,id,class_name,css,xpath这几种了。 5. 启动 app端:需要制定desired_caps内容,因为里面包含了设备信息等。 web端:通过启动webdriver不同的浏览器类,获取driver,如webdriver.Chrome(),也可以模拟手机端加载wap页面做wap页面的测试。 6. 关于元素的属性 app端:查找到元素以后,查看元素对象,发现里边基本上只有元素的text属性,也没有相关的方法修改,这个区别还是很大的。不过appium有set_text和set_value的方法,目前还没有尝试,用的还是send_keys()。 web端:web端简直就是天堂了,比起修改,读取元素属性。比如我要获取input标签的name,我可以用get_attribute方法,也可以自行写js代码改变这些属性。 7. 使用js app端:似乎是支持了,但是执行任何命令server端都会提示404的错误。 web端:支持非常好,因为本身js就是负责网页交互的,所以会很方便。 8. 关于滑动 app端:关于滑动是会用得很多的,比如页面很长,或者打开通知栏,这种需要在屏幕上滑动的,用到的还比较多。 web端:用到的比较少,之前基本上没有用到过。 9. 异常 app端:需要注意的是其他apk给你带来的影响,目前没有找到很好的方式去处理这些问题,因为其他apk给你做了弹窗,比如qq异地登陆,或者短信这种推送,会影响到目前的流程。办法肯定是有解决的,我个人理解,可以在出错之后比对一下是否在当前apk,如果不在的话则进入当前apk再做一次相关操作。 web端:很少被影响,可以边跑用例边聊qq,当然我只是举个例子,总之个人体会就是影响比较小,因为浏览器的driver完全只是控制浏览器,别的地方和它无关。 其他内容的话有待大家补充啦~暂时只想到这些。
以后会陆续补充 偶然在Python Cookbook看到一个format操作,想到一个问题, 感觉用了!r之后,会把传入的对象按照原来形式保留 d = {"foo": "bar"} "value in d is {!r}".format(d["bar"]) 想到一个用处,举个例子: params = ("woody", "suxiaoji", "zy") sql = "select userId from user where username in {!r}".format(params) print(sql)
坑之初体验 在Appium的初体验中,遇到了一些坑坑洼洼。将他们记录下来,以后方便查阅。 1. session大于60秒没接收到命令自动关闭 通过Appium-Python-Client连接到appium的session,60秒内没有操作的话,session就会被自动关闭,操作指的是元素的定位、获取、点击、输入等。 解决办法: desired_caps中加入超时时间配置 Python代码如下: Python desired_caps = {} # 在启动配置里面加入newCommandTimeout参数 desired_caps['newCommandTimeout'] = 200 2. Appium通过npm安装不上,咋个办 解决办法: 使用淘宝镜像cnpm安装, 具体可见我前一篇文章里的安装appium但是注意cnpm默认在当前目录安装npm包。 3. UIAutomationviewer不支持动态页面 解决办法: 先不启动Appium,直接开UIAutomationviewer,先获取到想要的控件信息。 启动安卓虚拟机,在虚拟机里边获取(但是虚拟机需要安装自己想要的包,甚是麻烦啊); 用driver.page_source查看源码,然后正则抓取相关控件信息(但是我好像没发现id信息), 尽量不要用xpath定位xml页面,查找非常慢; hierarchyviewer代替此工具,前提是设备开启ViewServer(虚拟机是默认开启的),真机一般只有工程机能开启,市面上目前发现只有小米设备可以开启 4. 怎么查看设备是否连接 解决办法: 设备通过usb连接上计算机以后,在cmd窗口输入adb devices 可获取到设备信息。 5. 获取包名 解决办法: 一般情况可以用查看日志的办法,可以先清空日志 adb logcat -c 然后获取正在展示app,前提是手机端要打开此app adb logcat | grep display 然后可以从正在展示的内容里提取 package和activity了,但是我能获取到支付宝的,QQ却不可以,很神奇。
通过上篇的安装,我们的环境大体上是搭建完成了。 以Python版本为例,我们需要先安装Python-appium端。 这时候我们的真机通过USB接入到电脑上,而且保证手机的USB调试模式打开。 启动appium 由于上一篇,我们改动了appium的名字为run_appium, 所以我这里启动方式变了。 简单点吧,简单点。 如果想查看具体的,请查看!参数详情 我们直接启动啦!可以看到我直接启用的时候报了个错,意思是端口被占用,所以我很机智换到了4723端口,请记住这个端口!这样appium就开始运行了! 一个Python demo 查看设备信息 打开cmd窗口,输入如下命令: adb devices 可以查看设备的信息。 可以看到我连接了一台设备,标识是HExxxxxxxxx 获取系统细带计算器的安卓版本、包名等信息(已知了,下篇讲解如何获取) 已知我版本是6.0 安卓 from appium import webdriver # 启动配置环境 desired_caps = {} # 系统名 desired_caps['platformName'] = 'Android' # 安卓版本 desired_caps['platformVersion'] = '6.0' # 设备名称 desired_caps['deviceName'] = 'HEE6R15C17002984' # app 包名 desired_caps['appPackage'] = 'com.eg.android.AlipayGphone' # app主页 desired_caps['appActivity'] = '.AlipayLogin' driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) driver.implicitly_wait(30) driver.quit() 这里没有做任何关于支付宝的操作,但是我们可以看到手机上打开了支付宝软件,是不是很棒??
由于虫师那边的源估计到期了,我又找了一波。 打开SDK Manager.exe, 就在安卓目录下。 点击Tools--Options进入配置页面 mirrors.neusoft.edu.cn 配置如下,然后就可以看到刷刷的一些安卓包了。。。 tools建议安装这3个包 安卓版本的话,比较多,我选的是6.0
说明 步骤可能比较简洁,因为手头上有安卓测试机,所以需要配置虚拟机的童鞋请去虫师博客园,因为我也是从那儿学的,哈哈。点我飞到虫师那儿 但是如果你要搭建真机测试环境的话,本教程将是最简单实用的。 1. 下载安装node.js 首先进入node.js官网,选择对应机器的版本下载并安装。下载地址 如图,我选择的是左侧的LTS版本,熟悉ubuntu系统的童鞋应该知道这个意思,它是Long Term Support的缩写,简单的说就是能获得长期支持的版本。右边的版本是最新的,可能会有部分问题,我们还是用较为稳定的版本吧。 下载之后开始安装。 目前我是放到C盘下,默认目录,你也可以放到其他目录,不影响。 如果这里显示 一定要将X改为上上图的状态,因为默认是添加到PATH变量里的。 然后一步步安装就可以了。 安装完js后,我们打开cmd窗口,输入"npm", 出现下图就代表安装成功了。 2. 安装java 点我进入java下载页 windows下有x86和x64两种选择,如果是64位系统,2者都可以用,但是建议选x64,如果是32位,建议选x86安装包。 下载完成后就开始安装了~ 我这里是默认安装在C盘默认路径的。 然后在弹出的jre窗口把jre同样安装到默认目录下。 安装完成~~~ 验证一下,打开cmd窗口,输入java 代表安装成功!接下来还有重要任务呢! 3. 安装Appium 我这里讲一下,怎么按照官方的步骤安装吧,简单方便快捷倍儿爽。 由于官方的appium源被wall给隔离开了,所以下载会失败,但是不要紧,我们的马云爸爸给我们提供了淘宝镜像。 具体介绍可以进淘宝镜像 我们需要现安装淘宝镜像的cnpm,一步步来,莫慌。 我们在cmd窗口输入命令: npm install -g cnpm --registry=https://registry.npm.taobao.org 然后静静地等待安装完成,如果把npm理解成pip,那么就可以把cnpm理解成自行封装的pip,里面下载的包都是马云爸爸服务器上的。 安装完成后图示: 现在我们就可以用cnpm来安装appium啦!! 切记: 使用cnpm的话,会在当前目录安装你需要的包 我们必须把它装回node目录下 还记得我们刚才的nodejs安装目录么,如果是默认的,往上翻 如果不是默认的 打开cmd窗口,输入where node cmd里切换到nodejs目录。 现在我的node目录是C:\Program Files\nodejs 输入命令: cd C:\Program Files\nodejs 如果没有跳转到该目录,说明你当前盘符与目标盘符不一致。比如你在D盘,要想跳转到C盘此目录,需要在输入上述命令后补充输入: C: 那我们现在就开始正式操作了!!! 使用cnpm安装appium, 输入"cnpm install appium",注意一定是cnpm哦,不然我们上面做那么多操作就白费了。安装过程可能会出现一些报错,但是不要惊慌,静候佳音。 将appium加入PATH 进入到C:\Program Files\nodejs\node_modules.bin目录 由于我配置了环境变量还是报错,我把里面的appium.cmd改为了run_appium.cmd(所以以后运行的时候,在cmd窗口没有路径限制,直接输入run_appium就行) 在箭头所指的空白处点击鼠标左键,然后复制目录路径 返回到桌面,对着我的电脑点击鼠标右键,选择属性,进入 点击高级系统设置,然后点击环境变量 在下面的系统变量里找到path这一项,然后点击编辑,之后点击新建按钮,如果你是win7,环境变量都是用英文分号隔开的,你可以在这一系列路径的最前方,添加如下内容,比如我本机就是 C:\Program Files\nodejs\node_modules.bin; 别忘了添加分号哦!!! win10直接新建,然后添加 因为win10是每个变量都单独一行的,所以不需要分号隔离了。 然后点击确定(之前打开的设置的确定都要点哦) 验证是否成功: WIN+R,输入cmd,回车 然后输入run_appium 可以看到appium成功启动了,之所以要把appium.cmd改名为run_appium.cmd 原因是node自动配置了环境变量,我们可以查一下appium到底是什么。 可以看到,找到了3处,所以我们只要运行我们想要的那个就可以了,为了避免意外,我这里改了名字,影响不大。。 4. 安装安卓开发工具 安卓SDK下载地址http://dl.google.com/android/android-sdk_r23.0.2-windows.zip 新建Android目录,目前我这边在D盘建立,如图 下载完成后解压了之后放入刚建立的安卓目录即可。 在此目录下新建一个文件夹加build-tools,里面不需要放置内容,目录结构如图所示。 配置安卓环境变量,以下内容按照我本地目录操作。 还是进入环境变量页面,不懂的看回放。。 选择新建系统变量 变量名: ANDROID_HOME 变量值: D:\Android\android-sdk-windows 找到path编辑,添加一项: win7: ;%ANDROID_HOME%\platform-tools;%ANDROID_HOME%\tools; win10: 安卓工具下载地址 找到platform tools 下载后解压,把整个platform tools放到安卓目录下。 配置好了之后,后面模拟器的内容,想了解的话去看虫师的博客。。地址之前已经给出了。 5. 配置java home (本节内容摘自虫师博客园!) 类似于步骤4,具体不赘述了。 还是where大法好,刚才不是安装过java么。使用一下where命令 发现在 C:\Program Files (x86)\Java\jdk1.8.0_131目录下。 下面设置环境变量: “我的电脑”右键菜单--->属性--->高级--->环境变量--->系统变量-->新建.. 变量名:JAVA_HOME 变量值:C:\Program Files (x86)\Java\jdk1.8.0_131 变量名:CLASS_PATH 变量值:.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar; 找到path变量名—>“编辑”添加: 变量名:PATH 变量值:%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin; WIN10: 下一篇--教你如何模拟启动支付宝
总结一下最近包括之前遇到的一些pymongo操作的问题。 #需求1: 搜索文档数组里边是否存在某元素 数据: data1 = { '_id': xxxxxxxxxxxxxx, 'dataList': [ 'apple', 'grape', 'banana' ] } data2 = { '_id': xxxxxxxxxxxxxx, 'dataList': [ 'watermelon', 'mango' ] } 关键字: $elemMatch 查询方法: db.find({'$elemMatch': {'dataList': 'mango'}}) 这样就可以找到水果数据列表里边mango所在的document了。 #需求2: 删除文档的某个字段的某些信息 数据: data = { '_id': "xxxxxxxx" 'userInfo': {"name": "Woody", "age": 24, "weight": 10} } 现在我想删除userInfo里边的weight信息。 关键字: $unset db.update({'_id': 'xxxxxxxx'}, {'$unset': {'userInfo.weight': 10}}) 这样就可以删除掉userInfo里边的weight信息了,补充一点,userInfo和weight之间的连接用“.”来表示。 需求3: 更新一条数据,如果数据不存在则插入此数据 关键字: upsert参数置为True 在update, update_one, update_many里边都包含这个参数,现在贴一下源码。 可以看到源码里的解释是,如果upsert参数为True,则会在没有找到文档的时候插入这条数据(此时是可以代替insert操作的)。 #需求4: 使用正则表达式查询文档里的文本 关键字: $regex data = { '_id': "xxxxxxxx", 'content': 'hello, this is a url for baidu, it is https://www.baidu.com' } db.find({"content": {"$regex": r"https://[\w\.]+"}}) #需求5: 过滤不需要的字段 关键字: projection参数 例如我想过滤掉id,我只要content和name字段 data = { '_id': "xxxxxxxx", 'content': 'hello, this is a url for baidu, it is https://www.baidu.com', 'name': '百度首页' } db.find({"content": {"$regex": r"https://[\w\.]+"}}, projection={'_id':0, 'name':1, 'content':1})
首先引用一下廖雪峰Python教程里关于sqlalchemy的话, 这里我们要讲的是flask_sqlalchemy的用法。 1. 安装 用pip安装即可, 进入cmd控制台输入 pip install Flask-SQLAlchemy 2. 引用 引用方法有2种,旧的和新的。 from flask_sqlalchemy import SQLAlchemy # 推荐 from flask.ext.sqlalchemy import SQLAlchemy # 也能用,但是console窗口会弹出一些提示 3. 使用 声明: 参考资料出自http://www.pythondoc.com/flask-sqlalchemy/quickstart.htmlhttp://blog.csdn.net/werewolf_st/article/details/45933949 早上做了个小的尝试,尝试将现有数据库的表转为Python的数据结构。 话不多说, 直接开干! from flask_sqlalchemy import SQLAlchemy from flask import Flask app = Flask(__name__) # 此处是配置SQLALCHEMY_DATABASE_URI, 前面的mysql+mysqlconnetor指的是数据库的类型以及驱动类型 # 后面的username,pwd,addr,port,dbname分别代表用户名、密码、地址、端口以及库名 app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+mysqlconnector://username:pwd@addr:port/dbname' # 创建1个SQLAlichemy实例 db = SQLAlchemy(app) # 定义1个类(由db.Model继承),注意这个类是数据库真实存在的,因为我是针对已有数据库做转化 # 我的数据库结构见下图 其中role是数据库的一张表名 class role(db.Model): # id是主键db.Column是字段名, db.INT是数据类型 id = db.Column(db.INT, primary_key=True) name = db.Column(db.String(99), unique=False) name_cn = db.Column(db.String(99), unique=False) def __init__(self, id, name, name_cn): self.id = id self.name = name self.name_cn = name_cn def __repr__(self): return '<User %r>' % self.name # 初始化role 并插入数据库 test_role1 = role(6, 'supervisol', '超超超超级管理员哦') test_role2 = role(7, 'your try', '你试试哦') db.session.add(test_role1) db.session.add(test_role2) db.session.commit() #查询数据库 db.session.query(role).filter_by(id=2).first() # 查询role表中id为2的第一个匹配项目,用".字段名"获取字段值 db.session.query(role).all() # 得到一个list,返回role表里的所有role实例 db.session.query(role).filter(role.id == 2).first() # 结果与第一种一致 # 获取指定字段,返回一个生成器 通过遍历来完成相关操作, 也可以强转为list db.session.query(role).filter_by(id=2).values('id', 'name', 'name_cn') # 模糊查询 db.session.query(role).filter(role.name_cn.endswith('管理员')).all() # 获取role表中name_cn字段以管理员结尾的所有内容 # 修改数据库内容 user = db.session.query(role).filter_by(id=6).first() # 将role表中id为6的name改为change user.name = 'change' db.session.commit() 至于删除,我就暂时不研究啦,以后再补充~~~懒人啊哈哈哈。
前言 由于最近在做文件管理模块的功能,所以难免会遇到文件上传下载这块的功能。不过文件上传那块是调用的OSS api,所以接触的不多。 文件的下载: 1. 接口返回真实的文件 这种情况比较简单, flask里带有此类api, 可以用send_from_directory和send_file. 核心代码如下: from flask import send_file, send_from_directory import os @app.route("/download/<filename>", methods=['GET']) def download_file(filename): # 需要知道2个参数, 第1个参数是本地目录的path, 第2个参数是文件名(带扩展名) directory = os.getcwd() # 假设在当前目录 return send_from_directory(directory, filename, as_attachment=True) 后边那个as_attachment参数需要赋值为True,不过此种办法有个问题,就是当filename里边出现中文的时候,会报如下错误: 解决办法: 使用flask自带的make_response 代码修改如下 from flask import send_file, send_from_directory import os from flask import make_response @app.route("/download/<filename>", methods=['GET']) def download_file(filename): # 需要知道2个参数, 第1个参数是本地目录的path, 第2个参数是文件名(带扩展名) directory = os.getcwd() # 假设在当前目录 response = make_response(send_from_directory(directory, filename, as_attachment=True)) response.headers["Content-Disposition"] = "attachment; filename={}".format(file_name.encode().decode('latin-1')) return response 使用make_response函数建立一个response对象,然后将filename编码转为latin-1,可以看到server.py里边会严格按照latin-1编码来解析filename,所以我这里的做法是先将utf8编码的中文文件名默认转为latin-1编码。 2. 接口返回文件数据流 这种情况比较适合我现在的需求,因为我这边是用requests库,先请求一个oss链接,获取到文件的数据,然后我发现目前flask没有这样的api实现,这里还是使用make_response方法实现。 代码如下: import mimetypes @app.route('/fileManager/download/<projId>/<id>/<filename>', methods=['GET']) def download_file(projId, id, filename): try: url = "your url" r = requests.get(url, timeout=500) if r.status_code != 200: raise Exception("Cannot connect with oss server or file is not existed") response = make_response(r.content) mime_type = mimetypes.guess_type(filename)[0] response.headers['Content-Type'] = mime_type response.headers['Content-Disposition'] = 'attachment; filename={}'.format(filename.encode().decode('latin-1')) return response except Exception as err: print('download_file error: {}'.format(str(err))) logging.exception(err) return Utils.beop_response_error(msg='Download oss files failed!') 解释一下: make_response很强大,下载一个文件,需要在response的headers里边添加一些信息,比如文件的类型,文件的名字,是否以附件形式添加,这3个是比较关键的信息。 mime_type是文件的类型,我观察send_file的源代码发现里边用到了mimetypes.guess_type()这个方法,也就是猜测文件的类型,然后这里我就直接搬过来用了哈哈,r.content其实就是文件的数据流,之前我是通过 with open(filename, 'wb') as file: file.write(r.content) 这样实现下载文件到本地的,所以其实r.content是一个文件数据流,也不清楚我的名词用的是否恰当哈哈。 之所以不用第一种方式,是因为我本地生成文件了之后,需要删除他,但是删除的时候总是会提示该文件已经被另一个程序使用,所以猜测是send_file这个api还在使用该文件,为了达到更好的效果,找到了第二种解决办法。 其实还有一种解决办法: 3. 发送静态文件 其实原来和第一种差不多,调用的api不一样,api是 from flask import app import os @app.route("/download/<filepath>", methods=['GET']) def download_file(filepath): # 此处的filepath是文件的路径,但是文件必须存储在static文件夹下, 比如images\test.jpg return app.send_static_file(filepath)
先埋个雷, 最近在做通过excel读取接口测试用例~ 流程等都是自己制定的,打算做完了之后放到GitHub上去哈哈哈。 正式进入正题~ 在写这个框架的时候,遇到了一个问题,就是同一个接口,需要为他准备很多组参数,那么我该在excel里怎么处理呢,本身是想另起一行,但是又觉得同样的内容过多,比如接口地址、name、id、headers等这些信息都肯定是一致的。那么我想到了,合并单元格! 但是新的问题又出现了,在我合并单元格以后,我逐行读取用例的时候,发现,被合并的单元格读取的结果是'', 这就很令人尴尬了。 但是不要紧 def merge_cell(sheet): rt = {} if sheet.merged_cells: # exists merged cell for item in sheet.merged_cells: for row in range(item[0], item[1]): for col in range(item[2], item[3]): rt.update({(row, col): (item[0], item[2])}) return rt def get_merged(filename): # 这里本应该做filepath的判断,但是我先省略了 book = xlrd.open_workbook(filename) sheets = book.sheets() # 所有sheets for index in range(len(sheets)): sheet = book.sheet_by_index(index) # 获取合并的单元格 merged = merge_cell(sheet) # 获取sheet的行数(默认每一行就是一条用例) rows = sheet.nrows # 如果sheet为空,那么rows是0 if rows: for row in range(rows): data = sheet.row_values(row) # 单行数据 for index, content in enumerate(data): if merged.get((row, index)): # 这是合并后的单元格,需要重新取一次数据 data[index] = sheet.cell_value(*merged.get((row, index))) 这样每行的数据data, 就是正确的数据了! xlrd里面有个merged_cells方法,可以获取到所有合并的单元格~ 像如图的,G列2,3,4行都合并到了第2行,所以导致取3,4行数据的时候会取到"". 我这边的case_info是每一行的数据,用的row_values()方法取出的数据。 相当于做了1次更新,merge_cell方法是用来获取哪些单元格是被合并了的,并且找到他们合并到的那个单元格。(已知缺陷,第一行本身就能去到数据,我做了多余的更新。) merged是调用merge_cell()方法后返回的1个dict,里面存放了key: 被合并的单元格地址, value: 合并到的单元格地址。 这样就解决了同一个接口, 多参数的问题~
我们在做UI自动化测试的过程中,某些情况会遇到,需要操作WebElement属性的情况。 假设现在我们需要获取一个元素的title属性,我们可以先找到这个元素,然后利用get_attribute方法获取属性的值。 举个栗子: from selenium import webdriver driver = webdriver.Chrome() driver.get("http://www.baidu.com") search_button = driver.find_element_by_id("su") # 百度搜索按钮 # 现在我们获取百度一下的值 value = search_button.get_attribute("value") # 获取input标签的value,也就是百度一下那4个字 print(value) # 打印 百度一下 但是现在我们有了新的需求,我们需要改变百度一下这个按钮里边显示的值。 先说一下原理,原理是利用js的dom(document object model),也就是文档对象模型,获取到input标签, 然后通过js来改变这个input标签的value属性。 js如下: var button = document.getElementById("su"); button.setAttribute("su", "你猜一下"); //或者直接给value属性赋值 document.getElementById("su").value = "你猜一下"; 我们在Chrome DevelopmentTools里边可以看到,“百度一下”变成了“你猜一下”~ 那么为什么我们不直接用driver.execute_script()这个方法来执行上述js语句呢,但是要知道,因为dom里获取元素的方式有限,并不如selenium那么方便,什么link_text这类的api都是无法使用的。 昨天偶然发现2个问题,第一是execute_script函数是可以传脚本参数进去的,第二个是selenium抓取到的元素可以作为js的dom元素处理。有了这2点之后呢,就可以干活了! 现在用WebElement的方法做到同样的事情 from selenium import webdriver driver = webdriver.Chrome() driver.get("http://www.baidu.com") search_button = driver.find_element_by_id("su") # 百度搜索按钮 # arguments[0]对应的是第一个参数,可以理解为python里的%s传参,与之类似 driver.execute_script("arguments[0].value = '你猜一下';", search_button) 补充一点, 如果需要获取js语句执行后的返回值,在js语句前加"return" 就行了,例如: button_value = driver.execute_script("return arguments[0].value;", search_button)
2022年05月