注释:运行环境linux+python3.7.3+pytest5.2.2+postgresql+flask-sqlalchemy2.4.1
公司之前用的NoSQL作数据管理,最近让我把数据库使用关系型数据库翻译一下,老大决定使用postgresql并采用ORM管理数据库,数据库翻译完,老大说为了保证数据稳定迁移,让我用pytest写一下测试用例,第一次写啊,遇到各种坑,第一就是测试项目的目录搭建,因为之前会一点unittest,unittest实现自动化测试可以写在同一个类中进行测试,当时不知道怎么搭合适,各种实验,最终采用下边的目录结构,很好的做到解耦。第二坑就是转化过来的数据表各种外键约束,直接使用pytest运行测试他会自动的搜索test开头的测试用例,这样没有顺序,我某些表中用到的外键这张表输入还没有插入,就会测试失败,后来又使用pytest按照表的外键关系一个个的添加,等价于手动排序
1:搭建测试项目结构(目录结构)这里每个文件都是包,是因为你在使用pytest执行用例的时候他会自动搜索包中test开头的文件
.
├── manage.py # 项目启动文件
├── moduls # 模型类存放包
│ ├── __init__.py
│ └── moduls.py # 模型类py文件
└── test # 单元测试包
├── conftest.py # pytest配置(初始化及测试后的清理工作)
├── data # 测试用例使用到的数据管理包
│ ├── __init__.py
│ └── test_db_add_delete # 测试用例使用到的数据管理包(名字以test开头+什么测试+测试了什么功能)
│ ├── __init__.py
│ └── test_user.json # 数据json文件 (名字以test开头+测试的那个模型类)
├── __init__.py
├── tests # 测试用例管理包
│ ├── __init__.py
│ └── test_db_add_delete # 测试用例管理包(名字以test开头+什么测试+测试了什么功能, 与测试数据管理包名字一致)
│ ├── __init__.py
│ └── test_user.py # 测试用例py文件 (名字以test开头+测试的那个模型类,与json文件一致)
└── utils # 工具管理包
├── get_data.py # 用例与数据的解耦(获取测试用例使用到的数据)
└── __init__.py
2:文件解释及demo
2-1:conftest.py
注释:这个文件我用了pytest.fixture(scope="session", autouse=True)这个装饰器内的scope参数可以设置被装饰的函数什么时候执行及执行多少次(session:整个测试开始到全部测试完成只执行一次。->class:表示你测试用例如果写在类中,那么每执行一个类中的用例就会执行一次这个函数。->function:表示你用例是写在函数中,那么每执行一个函数用的用例,这个函数就会被执行一次。->model:表示你执行这个模块中的所有用例的时候,这个函数就会执行一次),我这里要使用数据库的关联关系,所有我用的是session,意思就是在我使用pytest执行用例的时候,这个函数只执行一次,因为我使用这个被装饰的函数做了数据库的初始化和测试完成后的清理工作。这个函数中的yield类似于unittest.TestCase中的setUp和tearDown方法yield之前的等价于setUp方法中的逻辑,之后的等价于tearDown中的逻辑
import os import pytest @pytest.fixture(scope="session", autouse=True) def app_db_session(): """ 数据库fixture :return: """ # 引入项目中的app和db from manage import app, db # 测试数据库与开发数据库的解耦 app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql+psycopg2://dbname:password@127.0.0.1:5432/test" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False # 配置数据库(数据库的开销设置为False) app.testing = True db.drop_all() # 执行数据库的清理工作确保测试使用的是新的数据库,如果要用到原始数据请忽略 db.create_all() # 执行数据表的创建工作 yield db.session.remove() # 关闭pytest数据库连接 db.drop_all() # 清理数据表
2-2:moduls.py 我就不贴代码了,这个就是你创建的模型类,唉还是贴一个简单的demo吧
# -*- coding:utf-8 -*- import datetime from sqlalchemy import Column, TIMESTAMP, SmallInteger, String, func, Integer from sqlalchemy.dialects.postgresql import ARRAY, array from manage import db class BaseModel(db.Model): __abstract__ = True create_time = Column(TIMESTAMP, default=datetime.datetime.now(), comment="创建时间") update_time = Column(TIMESTAMP, default=datetime.datetime.now(), onupdate=func.now(), comment="更新时间") def to_dict(self): """ 单对象序列化 :return: """ _dict = {} _dict.update(self.__dict__) if "_sa_instance_state" in _dict: del _dict['_sa_instance_state'] return _dict class User(BaseModel): id = Column(Integer, primary_key=True, comment="主键ID") name = Column(String(32), comment="名字") age = Column(SmallInteger, comment="年龄")
2-3:utils->get_data.py 这个里边的代码还是要贴出来的,这个里边我定义了2个方法,一个方法是获取data中的数据路径和tests中的测试用例路径,另外一个方法是读取测试数据用的数据,将json数据处理成为元祖套列表的形式返回
import os
import json
def get_data_path(case_path: os.path) -> os.path:
file_name = os.path.dirname(case_path).split(os.sep + 'tests' + os.sep, 1)
return os.sep.join([file_name[0], 'data', file_name[1], os.path.basename(case_path).replace('.py', '.json')])
def get_test_data(case_path: os.path) -> list:
test_data_path = get_data_path(case_path)
with open(test_data_path, encoding='utf-8') as f:
dat = json.loads(f.read())
return [tuple(i.values()) for i in dat['test']]
2-4:test_user.py文件,这个文件编写测试用例,我觉得要明白一点,你要测试什么,拿数据库来说,我要测试的是正确的数据类型可以插入到数据库中,数据类型、约束、外键不对就无法插入数据,我这里测试的就是数据插入功能和删除功能,那么我这里测试用例的编写就明确了,我初始化一个模型类对象,给对象添加属性,然后在add->commit插入这条数据,然后我能够查询到刚才插入的这条数据,assert 取出来的值和我存入的值一直,那么我就认为是测试成功的。下边来看代码
这里我要用红字标明一些注意事项,一定要注意,要了解更详细的操作,请关注我pytest框架讲解
1:parametrize第一个参数是字符串,参数用逗号分隔
2:parametrize第二个参数是[(), (), ()] 有几个元祖就表示这个用例执行几次
3:被装饰的函数中的个数要和注意事项1的参数个数一样
4:注意事项1,和注意事项2,他们的参数位置要对应起来和后边要写的json文件一样,如果使用我的架构来测试,只要第一个参数的位置和json文件内的位置对应就可以了
import pytest import datetime from manage import db from moduls.moduls import User from test.utils.get_data import get_test_data data = get_test_data(__file__) # 这里我们使用了数据与用例的解耦,我们就使用paramtrize来传参 # 注意 1:它的第一个参数是字符串, 第二个参数是[(), ()] 这个类型的数据,列表中几个元祖就会执行几次测试(注意:他要和字符串参数位置对应) # 2:字符串内的参数要和函数中的参数个数一致 @pytest.mark.parametrize("name, age", data) def test_user(name, age): obj = User() obj.name = age obj.age = age db.session.add(obj) db.session.commit() obj = User.query.filter_by(name=name).first() # 最好这里用unique类型的字段查询,我这里demo中只有2个字段,所以就这样写了
assert obj.age == age
2-5:test_user.json文件,上边也提了一下,这个json文件你key的顺序要和你paramtrize的第一个参数位置一致, 我这里test中[ ] 中有5个字典,那么我用例在运行起来的时候就会执行5次测试
{ "test": [ { "name": "5ee4ef28013f48f69a76bc16c7089245", "age": 30 }, { "name": "5ee4ef28013f48f69a76bc16c7089246", "age": 40 }, { "name": "5ee4ef28013f48f69a76bc16c7089247", "age": 50 }, { "name": "5ee4ef28013f48f69a76bc16c7089248", "age": 60 },{ "name": "5ee4ef28013f48f69a76bc16c7089249", "age": 70 } ] }
3:运行 在test文件内直接pytest就可以执行用例了
到这里本次测试就讲完了,如果想要更详细的讲解,后续我会出一个pytest框架的讲解