[测试]Python如何优雅的生成测试数据

1,160 阅读6分钟

前言

在软件开发测试过程,经常需要测试数据。这些场景包括:

  • 后端开发
    新建表后,需要构造数据库测试数据,生成接口数据提供给前端使用。
  • 数据库性能测试
    生成大量测试数据,测试数据库性能

如何写好一个测试数据的生成脚本

第一步,制定数据的结构,包括名称、类型、范围等

数据存在依赖关系的,可以预先构想最终的数据形态,再根据最终的数据形态,反推它依赖的数据的形态。例如,先假定需要用户标签-寿险客户价值。这个标签是依赖于寿险保单金额标签、寿险最近购买或续保时间标签、寿险2年内购买或续保频率标签。寿险保单金额标签又依赖于一连串的事件属性。通过用户标签-寿险客户价值,就能反向推出大量的数据结构了。

第二步,设计数据在不同范围的分布

数据质量影响最终效果,所以数据的分布要合理。数据的分布跟业务是有关联的,最优是参照原有的业务数据的分布。其余方式包括,根据业务人员的经验构想分布、从网上搜罗类似业务的分布、开发人员主观臆想各个数据的分布。

第三步,指定数据生成的方式,包括数据的数量、数据的先后顺序、保存方式等

第四步,确认数据的导入方式

相关框架

调研了python的各项框架,最后选用了 # joke2k/faker
查阅了文档后,找到能够根据权重生成数据的方法,这一点能满足我的需要。

文档地址 : faker.readthedocs.io/en/stable/p…

简单使用

  1. 依赖
 pip install faker 
  1. 本地化,中文faker
   fake = Faker('zh_CN')
  1. 简单api调用
from faker import Faker
fake = Faker()

fake.name()
# 'Lucy Cechtelar'

fake.address()
#  Cartwrightshire, SC 88120-6700'

fake.text()
# 'Sint velit eveniet. Rerum atque repellat voluptatem quia rerum. Numquam excepturi
#  beatae sint laudantium consequatur. Magni occaecati itaque sint et sit tempore. Nesciunt

更多faker的使用,请查阅文档。

具体示例

生成如下用户属性的数据,并保存为json文件

用户属性

属性名属性显示名类型范围分布比例
name用户名STRING随机
age年龄NUMBER0-18、18-26、26-35、36-45、45-55、55以上(2,8,20,34,21,15)
sex性别NUMBER男女(50.03,49.97)
city生活城市STRING北上广深、沈阳、济南、天津、西安、呼和浩特、温州、黄山(40,30,20,10)
province省份STRING北上广、辽宁、山东、天津、陕西、内蒙古、江苏、安徽(40,30,20,10)
annual_income年收入NUMBER0-6w、6-15w、15-30w、30w-80w、80w及以上(15,45,33,5,2)
married婚姻状态STRING未婚、已婚、离异(20,70,10)
occupation职业STRING白领、教师、工人、公务员、销售(45,10,20,10,15)
work_state工作状态STRING在职、退休、自由职业(45,35,20)
family_size家庭人口数量NUMBER1-6,其它(5,15,18,22,22,15,5)
children_size子女数量NUMBER0-3,其它(33,30,20,12,5)
have_car是否有车BOOL(20,80)
vip_level会员等级STRING0-5 普通会员-钻石会员(40,30,15,10,5)
membership_points会员积分NUMBER0、1-1000、1001-2000、2000-5000、5000以上(20,30,30,15,5)
is_valid是否在保BOOL(30,70)
education学历STRING高中及以下、本科、硕士、博士(35,45,15,5)

相关代码

user_faker.py

import json
from collections import OrderedDict
from datetime import datetime, date
from typing import Optional

from pydantic import BaseModel

from faker_config import fake
from snowflake import id_worker


class User(BaseModel):
    user_id: int
    first_id: int = None
    second_id: int = None
    time: Optional[datetime] = None
    name: str
    age: int
    sex: str
    city: str
    province: str
    annual_income: int
    married: str
    occupation: str
    work_state: str
    family_size: int
    children_size: int
    have_car: int
    vip_level: str
    membership_points: str
    is_valid: int
    education: str
    create_time: Optional[datetime]
    create_date: Optional[date]


def generate_user():
    time = fake.past_datetime(start_date='-2y')
    user = {
        "user_id": id_worker.get_id(),
        "first_id": None,
        "second_id": None,
        "time": time,
        "name": fake.name(),
        "age": user_faker.age(),
        "sex": user_faker.sex(),
        "annual_income": user_faker.annual_income(),
        "married": user_faker.married(),
        "occupation": user_faker.occupation(),
        "work_state": user_faker.work_state(),
        "family_size": user_faker.family_size(),
        "children_size": user_faker.children_size(),
        "have_car": user_faker.have_car(),
        "vip_level": user_faker.vip_level(),
        "membership_points": user_faker.membership_points(),
        "is_valid": user_faker.is_valid(),
        "education": user_faker.education(),
        "create_time": time,
        "create_date": time.date()
    }
    user.update(json.loads(user_faker.province_and_city()))
    user = User(**user)
    return user


class UserFaker:

    def age(self):
        elements = OrderedDict(
            [(fake.random_int(min=0, max=18), 0.02), (fake.random_int(min=19, max=26), 0.08),
             (fake.random_int(min=27, max=35), 0.2), (fake.random_int(min=36, max=45), 0.34),
             (fake.random_int(min=46, max=55), 0.21), (fake.random_int(min=55, max=99), 0.15)])
        return fake.random_element(elements=elements)

    def province_and_city(self):
        elements = OrderedDict([('{"province": "北京", "city": "北京"}', 0.4)
                                   , ('{"province": "辽宁", "city": "沈阳"}', 0.3)
                                   , ('{"province": "陕西", "city": "西安"}', 0.2)
                                   , ('{"province": "安徽", "city": "黄山"}', 0.1), ])
        return fake.random_element(elements=elements)

    def annual_income(self):
        elements = OrderedDict(
            [(fake.random_int(min=0, max=6), 0.15), (fake.random_int(min=7, max=15), 0.45),
             (fake.random_int(min=16, max=30), 0.33), (fake.random_int(min=31, max=80), 0.02),
             (fake.random_int(min=80, ), 0.02)])
        return fake.random_element(elements=elements)

    def married(self):
        elements = OrderedDict([('未婚', 0.2)
                                   , ('已婚', 0.7)
                                   , ('离异', 0.1), ])
        return fake.random_element(elements=elements)

    def sex(self):
        elements = OrderedDict([('男', 0.52)
                                   , ('女', 0.48)
                                ])
        return fake.random_element(elements=elements)

    def occupation(self):
        elements = OrderedDict([('白领', 0.45)
                                   , ('教师', 0.1)
                                   , ('工人', 0.2)
                                   , ('公务员', 0.1)
                                   , ('销售', 0.15), ])
        return fake.random_element(elements=elements)

    def work_state(self):
        elements = OrderedDict([('在职', 0.45)
                                   , ('退休', 0.35)
                                   , ('自由职业', 0.20), ])
        return fake.random_element(elements=elements)

    def family_size(self):
        elements = OrderedDict([(1, 0.05)
                                   , (2, 0.15)
                                   , (3, 0.18)
                                   , (4, 0.22)
                                   , (5, 0.22)
                                   , (6, 0.15)
                                   , (fake.random_int(min=7, max=10), 0.05), ])
        return fake.random_element(elements=elements)

    def children_size(self):
        elements = OrderedDict([(1, 0.33)
                                   , (2, 0.35)
                                   , (3, 0.20)
                                   , (4, 0.07)
                                   , (5, 0.05)])
        return fake.random_element(elements=elements)

    def have_car(self):
        elements = OrderedDict([(0, 0.80)
                                   , (1, 0.20)])
        return fake.random_element(elements=elements)

    def vip_level(self):
        elements = OrderedDict([(1, 0.40)
                                   , (2, 0.30)
                                   , (3, 0.15)
                                   , (4, 0.10)
                                   , (5, 0.05)])
        return fake.random_element(elements=elements)

    def membership_points(self):
        elements = OrderedDict(
            [(fake.random_int(min=0, max=0), 0.2), (fake.random_int(min=1, max=1000), 0.3),
             (fake.random_int(min=1001, max=2000), 0.3), (fake.random_int(min=2001, max=5000), 0.15),
             (fake.random_int(min=5001, ), 0.05)])
        return fake.random_element(elements=elements)

    def is_valid(self):
        elements = OrderedDict([(0, 0.30)
                                   , (1, 0.70)])
        return fake.random_element(elements=elements)

    def education(self):
        elements = OrderedDict([('高中及以下', 0.35)
                                   , ('本科', 0.45)
                                   , ('硕士', 0.15)
                                   , ('博士', 0.05), ])
        return fake.random_element(elements=elements)


user_faker = UserFaker()

faker_config.py

from faker import Faker

fake = Faker('zh_CN')

main.py

import datetime
import json
from user_faker import generate_user, User


class DateEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            return obj.strftime("%Y-%m-%d %H:%M:%S")
        if isinstance(obj, User):
            return obj.dict()
        if isinstance(obj, datetime.date):
            return obj.strftime("%Y-%m-%d")
        else:
            return json.JSONEncoder.default(self, obj)


def generate_data(row):
    print(f"正在生成数据========>{row}条   ", datetime.datetime.now())

    users = []
    for i in range(row):
        user = generate_user()
        users.append(user)
    with open('./user.json', 'w', encoding='utf-8') as fObj:
        json.dump(users, fObj, ensure_ascii=False, cls=DateEncoder)
    print(f"测试数据生成========>{row}条,已完成   ", datetime.datetime.now())


if __name__ == '__main__':
    # 生成条数
    row = 10000
    generate_data(row)

效果

正在生成数据========>10000条    2021-07-23 11:13:44.739249
测试数据生成========>10000条,已完成    2021-07-23 11:13:48.923505

user.json

[{
	"user_id": 1418409069177348096,
	"first_id": null,
	"second_id": null,
	"time": "2019-12-31 12:27:59",
	"name": "鲁兵",
	"age": 38,
	"sex": "女",
	"city": "沈阳",
	"province": "辽宁",
	"annual_income": 2,
	"married": "已婚",
	"occupation": "白领",
	"work_state": "在职",
	"family_size": 4,
	"children_size": 2,
	"have_car": 0,
	"vip_level": "2",
	"membership_points": "275",
	"is_valid": 1,
	"education": "高中及以下",
	"create_time": "2019-12-31 12:27:59",
	"create_date": "2019-12-31"
}]

常用api

  • bothify 生成字符串和数字

bothify(text='## ??', letters='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
Number signs (‘#’) are replaced with a random digit (0 to 9). Question marks (‘?’) are replaced with a random character from letters.

eg:
for _ in range(5):
    fake.bothify(letters='ABCDE')
  • lexify(text='????', letters='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') 随机字母
  Generate a string with each question mark (‘?’) in text replaced with a random character from letters.
  • numerify(text='###') 随机数字

Number signs (‘#’) are replaced with a random digit (0 to 9). Percent signs (‘%’) are replaced with a random non-zero digit (1 to 9). Exclamation marks (‘!’) are replaced with a random digit or an empty string. At symbols (‘@’) are replaced with a random non-zero digit or an empty string.

>>> Faker.seed(0)
>>> for _ in range(5):
...     fake.numerify(text='Intel Core i%-%%##K vs AMD Ryzen % %%##X')
  • random_digit() 随机数字
    Generate a random digit (0 to 9).
  • random_choices(elements=('a', 'b', 'c'), length=None) 随机选择
    length 表示个数
  • random_elements(elements=('a', 'b', 'c'), length=None, unique=False, use_weighting=None) 根据权重随机选择
    fake.random_elements(
    elements=OrderedDict([
        ("variable_1", 0.5),        # Generates "variable_1" 50% of the time
        ("variable_2", 0.2),        # Generates "variable_2" 20% of the time
        ("variable_3", 0.2),        # Generates "variable_3" 20% of the time
        ("variable_4": 0.1),        # Generates "variable_4" 10% of the time
    ]), unique=False
)
  • random_int(min=0, max=9999, step=1) 随机数字
  • random_letter() 随机字母
  • date_between_dates(date_start=None, date_end=None) 随机日期
  • past_datetime(start_date='-30d', tzinfo=None) 过去随机时间