前言
在软件开发测试过程,经常需要测试数据。这些场景包括:
- 后端开发
新建表后,需要构造数据库测试数据,生成接口数据提供给前端使用。 - 数据库性能测试
生成大量测试数据,测试数据库性能
如何写好一个测试数据的生成脚本
第一步,制定数据的结构,包括名称、类型、范围等
数据存在依赖关系的,可以预先构想最终的数据形态,再根据最终的数据形态,反推它依赖的数据的形态。例如,先假定需要用户标签-寿险客户价值。这个标签是依赖于寿险保单金额标签、寿险最近购买或续保时间标签、寿险2年内购买或续保频率标签。寿险保单金额标签又依赖于一连串的事件属性。通过用户标签-寿险客户价值,就能反向推出大量的数据结构了。
第二步,设计数据在不同范围的分布
数据质量影响最终效果,所以数据的分布要合理。数据的分布跟业务是有关联的,最优是参照原有的业务数据的分布。其余方式包括,根据业务人员的经验构想分布、从网上搜罗类似业务的分布、开发人员主观臆想各个数据的分布。
第三步,指定数据生成的方式,包括数据的数量、数据的先后顺序、保存方式等
第四步,确认数据的导入方式
相关框架
调研了python的各项框架,最后选用了 # joke2k/faker
查阅了文档后,找到能够根据权重生成数据的方法,这一点能满足我的需要。
文档地址 : faker.readthedocs.io/en/stable/p…
简单使用
- 依赖
pip install faker
- 本地化,中文faker
fake = Faker('zh_CN')
- 简单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 | 年龄 | NUMBER | 0-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 | 年收入 | NUMBER | 0-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 | 家庭人口数量 | NUMBER | 1-6,其它 | (5,15,18,22,22,15,5) |
| children_size | 子女数量 | NUMBER | 0-3,其它 | (33,30,20,12,5) |
| have_car | 是否有车 | BOOL | (20,80) | |
| vip_level | 会员等级 | STRING | 0-5 普通会员-钻石会员 | (40,30,15,10,5) |
| membership_points | 会员积分 | NUMBER | 0、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) 过去随机时间