在做单接口的时候,比如有很多异常场景。之前在做测试时候,可以将数据保存在csv文件中,通过读取csv 文件的方式来进行参数化操作。 同样,在Python中也支持这样的操作。

注册接口

有不同的用户名密码
普通的做法,是写一个 for 循环,通过循环的方式来进行操作。

  1. """
  2. 测试注册接口
  3. """
  4. # 定义测试数据 放在列表中, 总共有 5组数据。
  5. userdata =[
  6. ("13211112222","123456"),
  7. ("","1234567"),
  8. ("13212341234",""),
  9. ("13212341234","12345678901234567890"),
  10. ("12345","123456")
  11. ]
  12. import requests
  13. # 使用上面的数据测试注册接口
  14. base_url = "http://49.233.108.117:28019"
  15. def test_regiseter():
  16. for user in userdata:
  17. register_url = base_url + "/api/v1/user/register"
  18. jsondata = {
  19. "loginName": user[0],
  20. "password": user[1]
  21. }
  22. # 发送json格式数据
  23. r = requests.post(url=register_url, json=jsondata)
  24. # 打印状态码
  25. print(r.status_code)
  26. # 打印返回结果
  27. print(r.json())

执行的时候虽然可以执行,但是结果中只有一条用例。
image.png
这样肯定是不行的。因为我准备5条数据, 希望运行的时候是5个用例。

pytest 参数化

pytest 框架提供了一种专门的参数化功能。只需要调用pytest 对应的方法就可以。
https://docs.pytest.org/en/7.1.x/how-to/parametrize.html#pytest-mark-parametrize-parametrizing-test-functions

使用pytest 内置的参数化功能。

"""
测试注册接口
"""
# 定义测试数据 放在列表中, 总共有 5组数据。
userdata =[
    ("13211112222","123456"),
    ("","1234567"),
    ("13212341234",""),
    ("13212341234","12345678901234567890"),
    ("12345","123456"),
    ("中文","123456")
]
import requests
import pytest
# 使用上面的数据测试注册接口
base_url = "http://49.233.108.117:28019"

@pytest.mark.parametrize("username,password",userdata)
def test_user_regisgter(username,password):
    # 执行打开 还是和原来一样
    url = base_url + "/api/v1/user/register"
    bodydata = {
        "loginName": username,
        "password": password
    }
    r = requests.post(url=url,json=bodydata)
    print(f"请求数据: {r.request.body}")
    print(f'返回结果: {r.status_code},  {r.json()}')

image.png
执行,可以看到 userdata 中有多少组数据, 就生成多少个测试用例。
image.png

这个就是 pytest 参数的功能。

  • @pytest.mark.parametrize 固定写法
  • “username,password” 跟 列表中的每一组数据 对应 (“13211112222”,”123456”)

username — 13211112222 password —123456
里面编写自动化代码还是不变。

添加对应的断言

不同的请求数据,断言结果也不一样,也可以将断言结果直接放在定义的数据中。

userdata =[
    ("13211112222","123456",500,"用户名已经存在!"),
    ("","1234567",510,"登录名不能为空!"),
    ("13212341234","",510,"密码不能为空")
]

因为又添加了两个数据,所以引用的时候,数据要保持一致。

"""
测试注册接口
"""
# 定义测试数据 放在列表中, 总共有 5组数据。
userdata =[
    ("13211112222","123456",500,"用户名已存在!"),
    ("","1234567",510,"登录名不能为空"),
    ("13212341234","",510,"密码不能为空")
]
import requests
import pytest
# 使用上面的数据测试注册接口
base_url = "http://49.233.108.117:28019"

@pytest.mark.parametrize("username,password,errCode,errMsg",userdata)
def test_user_regisgter(username,password,errCode,errMsg):
    # 执行打开 还是和原来一样
    url = base_url + "/api/v1/user/register"
    bodydata = {
        "loginName": username,
        "password": password
    }
    r = requests.post(url=url,json=bodydata)
    print(f"请求数据: {r.request.body}")
    print(f'返回结果: {r.status_code},  {r.json()}')
    # 添加断言
    assert r.json()["resultCode"] == errCode
    assert r.json()["message"] == errMsg

添加对应的断言。执行结果。
image.png

当数据量比较少的时候,可以将数据直接写在代码中,但是如果异常场景的数据很多,数据比较多的时候就不太适合放在代码中。
可以考虑将数据放在文件中。

csv数据

代码生成测试数据

将可能的数据放在先列举出来。通过python代码值生成不同排列组合的场景。

username = ["13212341234","","1234","132123412341","123456785678"]
password = ["","123456","1","1234567890123456789"]

testuser = []
for name in username:
    for passwd in password:
        # 组合一个场景的数据
        user = (name,passwd)
        # 将数据放在列表中
        testuser.append(user)

# 循环完成之后,查看生成的数据
print(testuser)
# 将生成的数据使用参数化
import pytest
import requests

base_url = "http://49.233.108.117:28019"

# 使用参数化功能
@pytest.mark.parametrize("username,password",testuser)
def test_data_register(username,password):
    url = base_url + "/api/v1/user/register"
    bodydata = {
        "loginName": username,
        "password": password
    }
    r = requests.post(url=url, json=bodydata)
    print(f"请求数据: {r.request.body}")
    print(f'返回结果: {r.status_code},  {r.json()}')

运行,可以看到结果。
image.png

代码生成添加断言

不同的数据 服务器返回结果也不一样。
编写编写对应的代码


username = ["13212341234","","1234","132123412341","123456785678"]
password = ["","123456","1","1234567890123456789"]

testuser = []
for name in username:
    for passwd in password:
        # 添加message 断言
        if name == "":
            message = "登录名不能为空"
        elif passwd == "":
            message = "密码不能为空"
        elif len(name) != 11:
            message = "请输入正确的手机号!"
        else:
            message= "用户名已存在!"
        # 组合一个场景的数据
        user = (name,passwd,message)
        # 将数据放在列表中
        testuser.append(user)

# 循环完成之后,查看生成的数据
print(testuser)
# 将生成的数据使用参数化
import pytest
import requests

base_url = "http://49.233.108.117:28019"

@pytest.mark.parametrize("username,password,message",testuser)
def test_data_register(username,password,message):
    url = base_url + "/api/v1/user/register"
    bodydata = {
        "loginName": username,
        "password": password
    }
    r = requests.post(url=url, json=bodydata)
    print(f"请求数据: {r.request.body}")
    print(f'返回结果: {r.status_code},  {r.json()}')
    # 针对message 断言
    assert r.json()["message"] == message

运行,可以看到执行用例,并发现一个bug。
image.png

数据保存到文件

最好也将测试数据保存到csv文件中。csv数据文件可以jmeter 或者postman 结合一起使用。

import pytest
import requests
import csv


username = ["13212341234", "", "1234", "132123412341", "123456785678"]
password = ["", "123456", "1", "1234567890123456789"]

test_user = []
for name in username:
    for passwd in password:
        # 添加message 断言
        if name == "":
            message = "登录名不能为空"
        elif passwd == "":
            message = "密码不能为空"
        elif len(name) != 11:
            message = "请输入正确的手机号!"
        else:
            message = "用户名已存在!"
        # 组合一个场景的数据
        user = (name, passwd, message)
        # 将数据放在列表中
        test_user.append(user)

# 循环完成之后,查看生成的数据
print(test_user)
# 将生成的数据使用参数化


base_url = "http://49.233.108.117:28019"


@pytest.mark.parametrize("username,password,message", test_user)
def test_data_register(username, password, message):
    url = base_url + "/api/v1/user/register"
    body_data = {
        "loginName": username,
        "password": password
    }
    r = requests.post(url=url, json=body_data)
    print(f"请求数据: {r.request.body}")
    print(f'返回结果: {r.status_code},  {r.json()}')
    # 针对message 断言
    assert r.json()["message"] == message

    # 运行的时候,运行一条用例 保存一条数据
    with open('register_data.csv',encoding='utf8',mode='a',newline='') as f:
        cw = csv.writer(f)
        cw.writerow([username,password,message])

执行,完成之后可以看到,数据已经放在测试数据已经生成。
image.png
但是,我们希望将测试数据文件 都放在 testdata 目录下。便于统一管理。

os 模块处理路径

默认数据文件使用的是相对路径生成。 我们希望数据放在指定的目录下。一种解决办法使用绝对路径
。这样的写法,有问题。 当代码在我的电脑上可以执行。但是在你的电脑就不能执行了。image.png
因为你的电脑里没有这样的路径。

file 全局变量

python中内置的有 全局变量。 返回文件在自己电脑上的绝对路径。

def get_root_dir():
    """
    获取项目的根目录
    :return:
    """
    # 当前文件在系统中的绝对路径
    print(__file__)  # C:\Users\zengy\PycharmProjects\pythonProject13\common\file_dir.py


if __name__ == '__main__':
    get_root_dir()

os.path.dirname() 文件目录

使用os模块中的 path中的dirname 可以返回目录路径。

"""
处理文件路径
"""
import os
def get_root_dir():
    """
    获取项目的根目录
    :return:
    """
    # 当前文件在系统中的绝对路径
    print(__file__)  # C:\Users\zengy\PycharmProjects\pythonProject13\common\file_dir.py
    # 当前文件所在的目录
    dir_name = os.path.dirname(__file__)
    print(dir_name) # C:\Users\zengy\PycharmProjects\pythonProject13\common
    # 当前目录的上一层目录
    root_dir = os.path.dirname(dir_name)
    print(root_dir) # C:\Users\zengy\PycharmProjects\pythonProject13

if __name__ == '__main__':
    get_root_dir()
  • dirname() 返回文件所在的目录路径。

    os.path.jion() 路径拼接

    找到项目的根目录之后,可以写代码来指定文件路径。 ```python “”” 处理文件路径 “”” import os def get_root_dir(): “”” 获取项目的根目录 :return: “””

    当前文件在系统中的绝对路径

    print(file) # C:\Users\zengy\PycharmProjects\pythonProject13\common\file_dir.py

    当前文件所在的目录

    dirname = os.path.dirname(_file) print(dir_name) # C:\Users\zengy\PycharmProjects\pythonProject13\common

    当前目录的上一层目录

    root_dir = os.path.dirname(dir_name) print(root_dir) # C:\Users\zengy\PycharmProjects\pythonProject13

    返回

    return root_dir

def demo():

# 将数据保存在 testdata 目录下
root_dir = get_root_dir()
# testdata 的路径
data_dir = os.path.join(root_dir,"testdata")
print(data_dir)  # C:\Users\zengy\PycharmProjects\pythonProject13\testdata
csvfiles = os.path.join(data_dir,"单接口测试数据")
print(csvfiles)

if name == ‘main‘: demo()


- join 将两个路径拼接在一起。
<a name="LHcIg"></a>
## os.mkdir() 创建目录
```python
"""
处理文件路径
"""
import os
def get_root_dir():
    """
    获取项目的根目录
    :return:
    """
    # 当前文件在系统中的绝对路径
    print(__file__)  # C:\Users\zengy\PycharmProjects\pythonProject13\common\file_dir.py
    # 当前文件所在的目录
    dir_name = os.path.dirname(__file__)
    print(dir_name) # C:\Users\zengy\PycharmProjects\pythonProject13\common
    # 当前目录的上一层目录
    root_dir = os.path.dirname(dir_name)
    print(root_dir) # C:\Users\zengy\PycharmProjects\pythonProject13
    # 返回
    return root_dir

def demo():
    # 将数据保存在 testdata 目录下
    root_dir = get_root_dir()
    # testdata 的路径
    data_dir = os.path.join(root_dir,"testdata")
    print(data_dir)  # C:\Users\zengy\PycharmProjects\pythonProject13\testdata
    csvfiles = os.path.join(data_dir,"单接口测试数据")
    print(csvfiles)
    # 创建目录  如果路径已经存在,再次创建,会报错
    if not os.path.exists(csvfiles):  # 如果这个路径不存在
        os.mkdir(csvfiles)    # 创建目录


if __name__ == '__main__':
    demo()
  • os.path.exists(csvfiles) 判断文件路径是否存在
  • os.mkdir(csvfiles) 创建目录

    修复之前的代码bug

    之前读取 csv文件的时候,路径使用绝对路径,现在改为 获取根目录之后进行路径拼接。 ```python “”” 这个文件中存放自己定义的一些常用的工具函数。 生成测试数据 “”” import random import csv from common.file_dir import get_root_dir import os def get_china_code():

    获取项目的根目录

    root_dir = get_root_dir()

    路径拼接

    gb2260 = os.path.join(root_dir,’testdata’,’gb2260.csv’) print(gb2260)

    as f 别名 f相当于是 前面open打开的文件 绝对路径字符串 前添加r

    with open(file=gb2260,
            encoding='utf8',mode='r') as f:
      # 读取完成之后会将所有的内容放在一个列表对象中
      rows = csv.reader(f)
      # 定义空列表
      allcodes = []
      # 循环列表
      for row in rows:
          # print(row,type(row))
          code = "".join(row)
          # 每次取到一个数据,讲这个数据放在列表中
          allcodes.append(code)
          # print(code,type(code))
      # for循环执行完,查看列表的内容
      # print(allcodes)
      # 随机从这么多的地区码中选择一个 返回。
      random_code = random.choice(allcodes)
      return random_code
    

def get_phone(): “”” 自动随机生成一个手机号码

:return:
"""
pre_phones = ["130", "131", "132", "133", "134", "135", "136", "137", "138", "139",
              "170", "172", "179",
              "150", "151", "155", "156",
              "188",
              "199"]
# 随机取号段
pre_3 = random.choice(pre_phones)
# print(f"随机选择号段 {pre_3}")
# 生成后8位
last_8 = ""
for i in range(8):
    # 随机生成一个数字n
    n = random.randint(0, 9)
    # 拼接字符串 将n转换为字符串
    last_8 = last_8 + str(n)  # 每次随机取到一个数字,转换字符串之后 放到last_8 后面
# print(f"生成后8位 {last_8}")
# 将前3位后8位 组合一起
phone = pre_3 + last_8
return phone

def get_id(): “”” 随机生成身份证号

:return:
"""
# 函数定义在函数的内部  闭包
def pre_6():
    n = get_china_code()
    # n = ""
    # for i in range(6):
    #     # 循环6次 每次生成1个随机数字,转换位字符串进行拼接
    #     n = n + str(random.randint(0, 9))
    # 循环执行完成
    return n

def ymd_8(startyear=1922, endyear=2021):
    # 生成出生的年  随机生成
    year = random.randint(startyear, endyear)
    # print(f"生成年份 {year}")
    # 生成月份  1-12
    month = random.randint(1, 12)

    # 如果月份在1-9 之间,前面需要补0
    if month < 10:
        month = "0" + str(month)  # 转换位字符串之后前面 补0
    # print(f"生成的月份 {month}")
    # 1,3,5,7,8,10,12 天数 31天
    if int(month) in [1, 3, 5, 7, 8, 10, 12]:
        day = random.randint(1, 31)  #
    # 4,6,9,11 月 30天
    elif int(month) in [4, 6, 9, 11]:
        day = random.randint(1, 30)
    # 闰年 2月份 29天
    elif (year % 4 == 0 and year % 100 != 0) or year % 400 == 0:
        day = random.randint(1, 29)
    # 以上都不符合,那就是平年
    else:
        day = random.randint(1, 28)

    # 如果day 在1-9 之间 前面补0
    if day < 10:
        day = "0" + str(day)
    # print(f"生成的日期 {day}")
    # 转换为字符串 进行拼接
    ydm = str(year) + str(month) + str(day)  # 等价于 f"{year}{month}{day}"
    return ydm

def last_4():
    nums = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'X']
    # 前三位 数字
    n3 = ""
    for i in range(3):
        n3 = n3 + str(random.randint(0, 9))
    # 第四位 随机从 nums 选择一个
    n4 = random.choice(nums)
    # 将n3 和 n4 拼接之后返回
    return n3 + n4

# 生成身份证id
id = pre_6() + ymd_8() + last_4()

return id

if name == ‘main‘: print( get_china_code())

# 测试生成手机号
# print(get_phone())
# 测试生成身份证id
# print(get_id())
<a name="WvNS8"></a>
# csv 数据保存到指定路径下
```python
import pytest
import requests
import csv
from common.file_dir import get_root_dir
import os


username = ["13212341234", "", "1234", "132123412341", "123456785678"]
password = ["", "123456", "1", "1234567890123456789"]

test_user = []
for name in username:
    for passwd in password:
        # 添加message 断言
        if name == "":
            message = "登录名不能为空"
        elif passwd == "":
            message = "密码不能为空"
        elif len(name) != 11:
            message = "请输入正确的手机号!"
        else:
            message = "用户名已存在!"
        # 组合一个场景的数据
        user = (name, passwd, message)
        # 将数据放在列表中
        test_user.append(user)

# 循环完成之后,查看生成的数据
print(test_user)
# 将生成的数据使用参数化


base_url = "http://49.233.108.117:28019"


@pytest.mark.parametrize("username,password,message", test_user)
def test_data_register(username, password, message):
    url = base_url + "/api/v1/user/register"
    body_data = {
        "loginName": username,
        "password": password
    }
    r = requests.post(url=url, json=body_data)
    print(f"请求数据: {r.request.body}")
    print(f'返回结果: {r.status_code},  {r.json()}')
    # 针对message 断言
    assert r.json()["message"] == message

    # 将数据保存到 testdata 目录下
    root_dir = get_root_dir() # 项目根目录
    # 保存数据的目录 testdata/test_regsiter
    data_dir = os.path.join(root_dir,"testdata","test_register")
    # 如果目录不存在 创建目录
    if not os.path.exists(data_dir):
        os.mkdir(data_dir)
    # 数据文件路径  testdata/test_regsiter/register_data.csv
    rigeter_data_file = os.path.join(data_dir,"register_data.csv")
    # 运行的时候,运行一条用例 保存一条数据
    with open(rigeter_data_file,encoding='utf8',mode='a',newline='') as f:
        cw = csv.writer(f)
        cw.writerow([username,password,message])

执行完成之后,数据保存到指定的目录中。
image.png

总结

数据驱动 主要就是使用 @pytest.mark.parametrize(“username,password,message”, test_user)
pytest中内置的 参数化功能进行数据驱动操作。