使用python unittest从零构建web应用的自动化测试用例

文章目录

  • 必要性
  • 使用pycharm搭建unittest框架
  • selenium
    • 下载web driver
    • web driver的基本使用
    • driver 定位元素
    • driver使用事件
    • 处理下拉框
    • 处理复选框
  • ssh
  • ftp
  • 数据库
    • sqlserver
    • oracle
      • 安装
      • 使用
    • mongodb
  • excel
  • pycurl

必要性

大部分团队起始对于要不要投入资源进行UI自动化测试的开发都是持怀疑态度,特别是小型公司,觉得投入的产出比不成正比。
我自己的判断是基于下面几个考虑(基于小软件企业,中大型的另说):

  1. 做产品的公司可以考虑,做项目(外包)的公司一般不考虑,除非已经将一些组件抽象的非常high level,相对固定了。
  2. 产品中相对稳定的固定流程,在整个团队中消耗了较多测试人力的流程可以考虑使用UI测试化来替代。
  3. 产品最起码还有2-3年或者更长的生命周期,否则也不考虑。
  4. 投入的测试人力基本上是属于流程化作业。
  5. 从某个点开始做起,不要一开始就说要覆盖多少多少业务。

使用pycharm搭建unittest框架

pycharm环境默认支持unittest项目的创建。

  1. 创建一个python项目
  2. 在项目上右键创建一个python文件,选择Python unit test
    使用python unittest从零构建web应用的自动化测试用例_第1张图片
  3. 创建出来的文件为:
import unittest


class MyTestCase(unittest.TestCase):
    def test_something(self):
        self.assertEqual(True, False)


if __name__ == '__main__':
    unittest.main()
- MytestCase为测试类
- test_something为测试用例函数
- self.assertEqual为unittest提供的断言方法,判断两个参数是否相等,自动化测试的逻辑就是判断理论值和实际值是否相同,来判断用例是否通过。
  1. 在运行配置里增加一项:
    使用python unittest从零构建web应用的自动化测试用例_第2张图片

  2. 点击运行即可。
    如果是上例,结果为用例不通过,结果为false
    使用python unittest从零构建web应用的自动化测试用例_第3张图片
    如果把断言改成:

self.assertEqual(True, True)

结果就变成:
使用python unittest从零构建web应用的自动化测试用例_第4张图片

  • pycharm还提供了重新跑失败用例等功能。
  • unittest提供了丰富的断言功能,比如不相等,容器直接比较等,就不一一说了,有兴趣的朋友可以自己去试。

selenium

selenium现在是一套完整的框架,我使用到的就是web driver这个模块,实际上我理解就是一个爬虫引擎:

  • 获取页面
  • 解析DOM,可以通过get_element等一系列函数获取DOM中的元素
  • 事件驱动,可以对DOM上的一些组件事件进行点击。

driver现在支持多种浏览器,包括edge,firefox,chrome等。
我自己用到的是chrome。

下载web driver

下载地址:
https://chromedriver.storage.googleapis.com/index.html
找到所需浏览器的版本,版本号找一个相近的即可。

web driver的基本使用

from selenium import webdriver

driver = webdriver.Chrome(executable_path=‘d:\xxxx.exe’) # 确定driver的地址
driver.get('http://localhost:8080')  # 获取http地址
driver.maximize_window()  # 窗口最大化
driver.implicitly_wait(10) # 等待时长

上面代码中最后一句设置等待时长是很关键的,因为因为网络传输和DOM处理加载都是需要时间的,可能自动化代码执行的时候,某个DOM元素还没有被加载好,driver就会有获取不到的可能,这个数字就是表名在多少秒之内进行等待DOM完成构建。

也可以通过显式的等待来处理:
import time

driver.get(xxxx)
time.sleep(10)
driver.find_element()

driver 定位元素

selenium获取元素可以根据DOM元素的ID,name,class或者精确的XPATH来进行定位。
当然最好的方式是使用ID,但是需要在开发的时候对对应的元素进行“埋点”处理。
在selenium3的时候,定位元素是通过

element = find_element_by_id('xxx')
element = find_element_by_name('xxx')
element = find_element_by_xpath('xxx')

来实现的。

而在selenium4的时候,这几个函数整合成一个函数了:
find_element
但是还是需要通过指定参数来确认定位方式:

from selenium.webdriver.common.by import By

element = find_element(by=By.ID, value='xxx')
element = find_element(by=By.NAME, value='xxx')
element = find_element(by=By.XPATH, value='xxx')

driver使用事件

  • 文本输入
driver.find_element(by=By.XPATH, value=‘xxx’).send_keys('abcde')

# 或者
element = find_element(by=By.XPATH, value='xxx')
element.send_keys('abcde')
  • 鼠标左键
driver.find_element(by=By.XPATH, value=‘xxx’).click('abcde')

# 或者
element = find_element(by=By.XPATH, value='xxx')
element.click('abcde')
  • 鼠标右键
from selenium.webdriver.common import action_chains

element = driver.find_element(by=By.XPATH, value=‘xxx’)

ac_1 = action_chains.ActionChains(driver)
ac_1.context_click(element).perform()
  • 下拉框
    下拉框实际上就是两次点击

处理下拉框

处理下拉框实际上就是两次点击,第一次点击是下拉框组件,第二次点击是其中的内容

driver.find_element(by=By.XPATH, value='dropbox_file').click()
# 等待一下
time.sleep(3)

# 选择文本
driver.find_element(by=By.XPATH, value='dropbox_txt').click()

处理复选框

复选框上也是一个点击动作:

driver.find_element(by=By.XPATH, value='item_checkbox').click()

自动化框架的基本逻辑就是:

  • 从界面上获取相应组件的信息,然后把这些信息和数据库,分布在各个地方的文件进行比对,如果比对的上,那么就是用例通过。那么在搭建一个完整的用例系统里,就必须用到python的ssh,ftp,各种数据库的库函数。我这里用到了下面几种,我理解基本覆盖了一个web系统能用到的基本组件。
  • ssh,对应的是paramiko库
  • ftp,对应的是ftplib库
  • sqlserver,对应的是pymssql库
  • oracle,对应的是cx_oracle库
  • excel,对应的是openpyxl库
  • seaweed,对应的是pycurl库,因为seaweed对外提供了访问数据的http服务。
  • mongodb,对应的是pymongo库。

ssh

我使用的库是paramiko库:

  1. 第一步就是初始化ssh连接:
import paramiko
ssh_conn = paramiko.SSHClient()

# 设置信任远程机器,允许访问
# 设置远程服务器没有在know_hosts文件中记录时的应对策略。目前支持三种策略:
#     AutoAddPolicy 自动添加主机名及主机密钥到本地HostKeys对象,不依赖load_system_host_key的配置。即新建立ssh连接时不需要再输入yes或no进行确认
#     WarningPolicy 用于记录一个未知的主机密钥的python警告。并接受,功能上和AutoAddPolicy类似,但是会提示是新连接
#     RejectPolicy 自动拒绝未知的主机名和密钥,依赖load_system_host_key的配置。此为默认选项
ssh_conn.set_missing_host_key_policy(paramiko.AutoAddPolicy())
  1. 进行连接
try:
    # ssh
    self.ssh_conn.connect(config.ssh_ip,
                          config.ssh_port,
                          config.ssh_username,
                          config.ssh_pwd)

    # sftp,如果使用sftp协议上传的时候可以使用
    transport = paramiko.Transport(config.ssh_ip, config.ssh_port)
    # 这里一定要使用username=和password=,因为第一个参数是hostkey
    transport.connect(username=config.ssh_username, password=config.ssh_pwd)
    self.sftp_conn = paramiko.SFTPClient.from_transport(transport)

except:
    print("Failed to connect to remote server") 
  1. 通过连接对象执行shell命令,我没有用sftp,用的是ftp协议。
shell_cmd = "mv /home/x.txt /home/y.txt"
stdin, stdout, stderr = ssh_conn.exec_command(shell_cmd )
  1. 通过stdout获得执行情况
    stdout实际上是一个输出流,就是把服务器上执行shell命令的输出作为字节流输出:
print(stdout.read())

输出为:b’abc\n’

ftp

ftp使用的是ftplib库,是python自带的库。
操作和ssh类似:

  1. 初始化和连接,多了一个登录操作
from ftplib import FTP
conn = FTP()
conn.connect(config.ftp_ip, config.ftp_prot)
conn.login(config.ftp_user, config.ftp_pwd)
  1. 上传文件:
buff_size = 1024 * 1024 * 1024   # 默认大小
fp = open(local_file_path, 'rb')
try:
    conn.storbinary('STOR %s' % remote_file_path, fp, buff_size)
except Exception as e:
    print('发送文件error:{}'.format(e))

相当于是要打开本地文件,然后读取出来,以字节流的方式向远端的ftp服务器写入。

  1. 下载文件,和上传类似
# 切换到目标目录,相当于cd命令
conn.cwd(path_name)

tmp_file = open('./temp/temp.txt', 'wb')
conn.retrbinary('RETR ' + remote_file_name, tmp_file.write)
tmp_file.close()

相当于打开一个本地文件,以字节流的方式从远端把文件内容读写到本地文件中。

  1. 上传和下载文件夹,这个库中没有直接上传文件夹的api,必须自己写递归,把文件夹中的子文件夹和文件都一次上传或下载。
    下面是一个递归上传的函数,下载的类似
def upload_data_dir(self, local_file_path, remote_file_path):
files = os.listdir(local_file_path)
        if remote_file_path:
            try:
                self.conn.mkd(remote_file_path)
            except Exception as e:
                pass
            finally:
                self.conn.cwd(remote_file_path)

        for file_name in files:
            print(local_file_path + r'/{}'.format(file_name))
            if os.path.isfile(local_file_path + r'/{}'.format(file_name)):
                self.upload_file_indir(local_file_path, file_name)
            elif os.path.isdir(local_file_path + r'/{}'.format(file_name)):
                try:
                    self.conn.mkd(file_name)
                except Exception as e:
                    pass
                finally:
                    local_file_path = "%s/%s" % (local_file_path, file_name)
                    remote_file_path = "%s/%s" % (remote_file_path, file_name)

                self.upload_data_dir(local_file_path, remote_file_path)

def upload_file_indir(self, path, file_name, target_dir=None, callback=None):
    # 记录当前 ftp 路径
    cur_dir = self.conn.pwd()
    if target_dir:
        try:
            self.conn.mkd(target_dir)
        except:
            pass
        finally:
            self.conn.cwd(os.path.join(cur_dir.title(), target_dir))
    file = open(os.path.join(path, file_name), 'rb')  # file to send
    self.conn.storbinary('STOR %s' % file_name, file, callback=callback)  # send the file
    file.close()  # close file
    self.conn.cwd(cur_dir)
  1. 删除文件和文件夹
try:
    self.conn.delete(file_name)
    time.sleep(3)
except Exception as e:
    print(e)

同样的,删除文件夹也需要自己写递归,删除文件夹的命令为:

conn.rmd("%s/%s" % (remote_dir, dir_name)

如果删除的这个文件夹不为空的话,是会失败的,所以需要先递归删除文件夹中的所有内容,最后删除文件夹

  1. 最关键的一点,所有的上传,下载,删除都是一个异步操作,也就是说调用之后,ftplib库会直接返回成功,然后再去删除。所以函数执行成功之后,不一定是已经将文件上传了的。

数据库

sqlserver

sqlserver用的是pymssql库,这个库安装的时候需要安装基础的visual studio运行环境,否则会安装失败。
使用起来比较简单,我暂时只使用了查询功能。

  1. 连接:
import pymssql
self.conn = pymssql.connect(host='xxxx',  # 这里的host='_'可以用本机ip或ip+端口号
                               server="master",  # 本地服务器
                               port="1433",  # TCP端口
                               user="xx", password="xxxx",
                               database="xxxx",
                               charset="GBK"
                               # 这里设置全局的GBK,如果设置的是UTF—8需要将数据库默认的GBK转化成UTF-8
                               )
        if self.conn:
            print('连接数据库成功!')  # 测试是否连接上
        else:
            print('未连接')  # 测试是否连接上
  1. 查询:
# 查询语句
        cursor = self.conn.cursor()  # 使用cursor()方法获取操作游标
        sql_select = "SELECT * FROM " + table_name + " where colName = \'" \
                     + queryValue + "\'"   # 数据库查询语句

        cursor.execute(sql_select)  # 执行语句
        results = cursor.fetchall()  # 获取所有记录列表

        for result in results:
        	dosomething()

oracle

安装

我使用的是cx_oracle库,这个库底层是调用的oci的库,所以最好本地安装了pl-sql这样的软件。或者单独下载oracle的客户端运行环境进行安装,然后把安装目录中的一些dll(windows的开发环境)拷贝到python环境的根目录下:
使用python unittest从零构建web应用的自动化测试用例_第5张图片
使用python unittest从零构建web应用的自动化测试用例_第6张图片
我用的是anaconda工具。

使用

  1. 连接
import cx_Oracle
conn_str = f"{user}/{password}@{host}/{service_name}"  # 
self.conn = cx_Oracle.connect(conn_str)
  1. 查询
cursor = self.conn.cursor()
        sql = 'select * from ' + view_name + \
              ' where "colname" = \'' + colValue + '\''

        cursor.execute(sql)
        results = cursor.fetchall()

        for result in results:
            dosomething()

mongodb

使用的是pymongo库

  1. 连接
import pymongo

mongo_url = 'mongodb://ip:27017/'
self.conn = pymongo.MongoClient(mongo_url)
self.db = self.conn[config.mongo_db_name]
self.config_table = self.db['collectionName']
  1. 查询
dblist = self.config_table.find({'key': keyValue})
  1. 更新
self.config_table.update_one({'key': key}, {'$set': {'value': value}})

excel

使用的是openpyxl 库

from openpyxl import load_workbook

wb = load_workbook(file_name)
ws = wb.active

# I2就是单元格的索引
print(ws['I2'].value)

还有很多操作,我在用例里没有用到,就不一一介绍了,网上内容一搜一大把。

pycurl

相当于执行curl命令的库,我们系统中使用到了seaweed,seaweed提供了http服务,用于上传文件:

  1. 访问ip:port/dir/assign地址可以获得一个id
  2. 使用这个ID,通过另外一个url上传一个文件。

使用pycurl实现第一步:

def write_func_for_file_id(self, buffer):
        self.result = eval(buffer)
        self.file_id_url = 'http://' + self.result['url'] + '/' + self.result['fid']
        self.file_id = self.result['fid']
        
c = pycurl.Curl()
c.setopt(pycurl.URL, "http://ip:port/dir/assign")

## 在opt中设置pycurl.WRITEFUNCTION,需要提供一个回调函数,这个函数就负责处理http返回的response
c.setopt(pycurl.WRITEFUNCTION, write_func_for_file_id)
c.setopt(pycurl.FOLLOWLOCATION, 1)

c.perform()

pycurl实现第二步:

file = open(path)

        c = pycurl.Curl()
        c.setopt(pycurl.URL, self.file_id_url)
        # opt设置pycurl.UPLOAD
        c.setopt(pycurl.UPLOAD, 1)
        # opt设置pycurl.READDATA
        c.setopt(pycurl.READDATA, file)
        # 同样需要一个回调,但是这里我不需要处理response
        c.setopt(pycurl.WRITEFUNCTION, self.write_func_for_upload)
        c.setopt(pycurl.FOLLOWLOCATION, 1)

        c.perform()
        file.close()

你可能感兴趣的:(python,前端,unittest,selenium,ui自动化)