我们是如何做测试的
如何界定测试的层次
随着科技的进步,测试技术日新月异,测试工具种类纷繁,而测试的层次的界定,直接关系到测试覆盖的范围和程度,是开展工作的首要条件。我们将测试分为中小型测试,以及大型测试。何为中小型测试,何为大型测试呢?
顾名思义,中小型测试主要针对一个单独函数,一个接口,或者独立功能模块的代码是否按照预期工作,着重于典型功能性问题、数据损坏、错误条件等方面的验证。
大型测试就是使用真实用户使用场景,和实际用户数据,进行系统的测试,这种端到端的使用场景以及在整体产品或服务之上的操作行为,即是大型测试关注的重点。
测试运行要求
1、每个测试和其他测试之间都是独立的,使它们就能够以任意顺序来执行
2、测试不做任何数据持久化方面的工作。在这些测试用例离开测试环境的时候,要保证测试环境的状态与测试用例开始执行之前的状态是一样的
3、测试比重符合,大型测试/中小型测试=2/8,因为将近80%的bug发生在百分之20的代码模块里,并且中小型测试测出的bug更容易发现和修改,带来的影响也较小,收益更高
测试流程
场景/用例设计 + 测试脚本编写 + 测试报告 + 发送邮件 + 持续化集成
基于上边的分析,此文主要介绍对于 Pytest 5.1.1 + Python 3.7.4 + Flask + mysql 的后端中小型测试
场景/用例设计
首先是场景/用例设计,主要是在理解需求的基础上,对用户行为进行的分析和定量为业务场景校验,还包含参数校验,功能校验,完整性校验,健壮性校验。 例如:房间筛选,我们从上述分析中可以设计测试用例简略如下(包含但不仅限于):
测试脚本编写
上文已经介绍了测试遵循的规则,我们需要在保证测试和其他测试之间都是独立的,并且需要根据数据库和一些外部依赖中的值,进行健壮性测试,所以对数据库中的存储,不直接使用mock,而使用和用户相同的环境,来准备好基础数据,来确保测试的准确性,以及灵活性。。好的,步入正题 我们知道,Fixtures可以再每一个 test_case启动之前就运转起来,并且能够在测试用例结束的时候,做好环境的恢复,完美的保证了测试之间的独立特性,并且能够为一批测试用例,进行基础数据的准备。下边就是官方文档的链接
目录结构
tests
├── integration // 内部保持和项目相同的结构
├── __init__.py
├── conftest.py # 全局环境配置,包含 redis,mysql,mongo,app,db.....
├── run_test.py # 用例执行,输出测试报告
├── data_center.py # 用于简化用例,一些处理数据的存在
├── utils.py # 用于简化用例,一些处理数据的存在
├── common # 目录结构维持跟项目保持一致
├── api # 目录结构维持跟项目保持一致
├── test_xxxx.py # 对应目录下的单元测试,集成测试等内容
├── mod # 目录结构维持跟项目保持一致
└── test_xxxx.py # 对应目录下集成测试等内容
├── unit // 单元测试
└── test_xxxx.py # 对应目录下单元测试等内容
创建数据
其中 数据贮备阶段
fixture scope
function:每个test都运行,默认是function的scope
class:每个class的所有test只运行一次
module:每个module的所有test只运行一次
session:每个session只运行一次
class DbData:
""" 存储数据准备阶段中插入的数据 """
def __init__(self):
""" 完成数据的准备,以及数据库的初始化"""
self.parameter = None
# 进行数据的插入,创建,属性的赋值,入口
self.setup()
def setup(self):
""" 按照对应顺序进行数据的插入,比如增加用户,增加订单"""
self.xxx()
pass
def example():
""" 逻辑撰写"""
pass
@classmethod
def xxx(cls, parameter):
""" 复用度不高,仅供此module使用的类方法,用于数据插入和读取,和数据库无关的不要写在这里面 """
pass
@pytest.fixture(scope='function', autouse=True)
def db_data():
""" 根据测试的是 test_Client 还是函数测试,指定数据隔离方式,clean_table均可以兼容 """
“”“ 此module中的数据准备存储在DbData里 ”“”
p = DbData()
yield p
数据销毁
其中进行数据销毁阶段
SQLAlchemy 1.3 Documentation State Management 文档
@pytest.fixture(scope='function', autouse=True)
def clean_table(db):
""" 通过fixture 拿到所有操作过的实例,并且session隔离 """
yield
if db.session.info.get("refs"):
tables = {k.__table__.name for k in db.session.info["refs"]}
db.session.execute("SET FOREIGN_KEY_CHECKS = 0")
for table in tables:
db.session.execute("TRUNCATE {}".format(table) + ";")
db.session.execute("SET FOREIGN_KEY_CHECKS = 1")
db.session.rollback()
db.session.remove()
db.session.expunge_all()
db.session.close()
def strong_reference_session(session):
@event.listens_for(session,"pending_to_persistent)
@event.listens_for(session,"deleted_to_persistent)
@event.listens_for(session,"detached_to_persistet)
@event.listens_for(session,"loaded_as_persistent")
def strong_ref_object(sess, instance):
if 'refs' not in sess.info:
sess.info['refs'] = refs = set()
else:
refs = sess.info['refs']
refs.add(instance)
@event.listens_for(session,"persistent_to_detache")
@event.listens_for(session,"persistent_to_deleted)
@event.listens_for(session,"persistent_to_transiet")
def deref_object(sess, instance):
sess.info['refs'].discard(instance)
这就可以保证,无论上一个测试中怎么修改数据,或者添加数据,每次测试之间都是互相独立的
创建app
@pytest.fixture(scope='session')
def app():
"""application fixture"""
# 进行配置拦截,防止进行数据库的错误抹除!笔者这里曾经两次不小心删除了测试线数据库
if not ConfigClass.TESTING:
pytest.exit("***" * 10 + "你执行了错误的环境,请注意!!!!" + "***" * 10)
app = create_app()
yield app
创建db,test_client
@pytest.fixture(scope='session', autouse=False)
def db(app):
"""db fixture"""
from spacebox.database import db
url = str(db.engine.url)
assert 'root' in url, url
db.session.configure(autoflush=False)
#将session用钩子进行绑定,绑定操作过的orm实例
strong_reference_session(db.session)
yield db
@pytest.fixture(scope='session')
def test_client(app):
"""test client"""
yield app.test_client()
测试用例的编写
通用化输入
我们把接口的访问方式进行封装起来,可以实现实现对多种的接口测试。
def api_test(client, url, data=None, request_type=TestCase.POST, return_code=200):
""" 封装测试接口的方法 """
if request_type == TestCase.POST:
res = client.post(url, json=data)
elif request_type == TestCase.PUT:
res = client.put(url, json=data)
elif request_type == TestCase.DELETE:
res = client.delete(url, json=data)
else:
res = client.get(url)
assert res.status_code == 200, res.status_code
rv = res.get_json()
assert rv.get('status') == return_code, rv
return rv
测试用例中需要多组数据库字段中的参数,怎么办?我们上已经说过fixture了,可以通过fixture 进行,通用化处理
@pytest.fixture(scope='function')
def input_form(db_data):
""" 函数的通用化输入 """
input_form = {
"reservation_id": db_data.reservation.id,
"reservation_no": db_data.reservation.reservation_no,
"refund_no": db_data.refund.refund_no,
"all_amount": 200,
"refund_amount": 200,
"refund_reason": "测试退款",
"refund_detail": "autotest",
"refund_memo": "autotest",
"refund_at": str(db_data.refund.created_at),
"start_at": str(db_data.reservation.start_at),
"leisure_card_hours": None,
"type": "yes"
}
return input_form
用例
# 实现了接口的场景测试
def test_refund_twice(test_client, app, db, db_data, input_form):
""" 测试amount参数, 并且一笔订单重复退款"""
with user_set(app, db_data.admin_user):
rv = api_test(test_client, url, data=input_form)
assert rv.get('status') == 200
# 一笔订单再次去退款
rv = api_test(test_client, url, data=input_form, return_code=201)
assert rv == {'status': 201, 'msg': '不存在待退款订单,请刷新重试'}
我们发现有的用例需要设置登陆用户,才能进行测试,怎样实现这一个点呢?
@contextmanager
def user_set(app, user):
"""为每一个测试连接,提供一个current_user对象"""
def handler(sender, **kwargs):
# _request_ctx_stack.top.user = user
setattr(_app_ctx_stack.top, 'jwt_user', user)
with request_started.connected_to(handler, app):
yield
@contextmanager
def function_user_set(app, user):
"""为每一个测试连接,提供一个current_user对象"""
if not _app_ctx_stack.top:
_app_ctx_stack.push(app)
app.app_context().push()
setattr(_app_ctx_stack.top, 'jwt_user', user)
yield
mock
# 使用pytest-mocker
# 情况一:同文件下
def func()
pass
def test_func(mocker):
global func
func = mocker.Mock(return_value = 5)
# 情况二:不同文件
# file1.py
def func1()
pass
# file2.py
from file1 import func1
def func2():
a = func1()
# file3.py
def test_func2(mocker):
a = mocker.patch('file2.func1', return_value=5)
# 情况三:在函数内倒入
# file1.py
def func1()
pass
# file2.py
def func2():
from file1 import func1
a = func1()
# file3.py
def test_func2(mocker):
a = mocker.patch('file1.func1', return_value=5)
测试报告
测试报告生成
我们用pytest-html 进行生成系统报告,然后进行邮件发送,同时进行一些特性化配置,这里的细节不展开讲,可以单独作为一篇文章展开。
通过指令生成html 报告文件
pytest -q tests/ --html=tests/report/report.html --self-contained-html
那我们有了报告,如何进行推送,发送到指定的人手里,首先我们通过钩子实现测试的监控,只有测试结果为成功的时候,才会触发邮件
def pytest_sessionfinish(session, exitstatus):
""" 测试结束之后的查看 """
if exitstatus != 0:
# 测试结果失败,发送邮件
logger.info("失败结果是{}".format(exitstatus))
send_email()
else:
logger.info("测试结果成功,不需要发送邮件!!!")
return exitstatus
测试报告发送
我们需要定制接收邮件的人,同时将邮件发送出去
def send_email():
""" 发送邮件 """
# 读取目录
try:
ROOT_DIR = dirname(dirname(abspath(__file__)))
# 检测报告是否正常生成
LOGFILE_DIR = ROOT_DIR + '/tests/report/report.html'
if not os.path.exists(LOGFILE_DIR):
logger.info("本地执行不发送邮件!")
return
f = open(LOGFILE_DIR, 'rb')
report_test = f.read()
f.close()
# 收件人
mail_receiver = ['xxxx@gmal.com']
# 根据不同邮箱配置 host,user,和pwd
mail_user = ''
mail_pwd = LocalConfig.MAIL_PWD
mail_to = ','.join(mail_receiver)
mail_title = "自动化测试报错!请及时查看"
message = MIMEText(report_test, 'html', 'utf-8')
message['To'] = mail_to
message['from'] = mail_user
message['subject'] = Header(mail_title, 'utf-8')
# Tencert 企业邮箱
s = smtplib.SMTP_SSL('smtp.exmail.qq.com')
s.connect('smtp.exmail.qq.com', '465')
s.login(mail_user, mail_pwd)
s.sendmail(mail_user, mail_receiver, message.as_string())
s.close()
logger.info("自动化测试邮件用执行成功!!")
except Exception as e:
logger.error(e)
logger.info("发送测试邮件报错!")
我们如何实现持续化集成的呢?
可以看这篇文章:gitlab-runner 到此为止,我们基本上整个的流程已经实现,希望对大家有所帮助~