我们是如何做测试的?

436 阅读7分钟

我们是如何做测试的

如何界定测试的层次

随着科技的进步,测试技术日新月异,测试工具种类纷繁,而测试的层次的界定,直接关系到测试覆盖的范围和程度,是开展工作的首要条件。我们将测试分为中小型测试,以及大型测试。何为中小型测试,何为大型测试呢?

顾名思义,中小型测试主要针对一个单独函数,一个接口,或者独立功能模块的代码是否按照预期工作,着重于典型功能性问题、数据损坏、错误条件等方面的验证。

大型测试就是使用真实用户使用场景,和实际用户数据,进行系统的测试,这种端到端的使用场景以及在整体产品或服务之上的操作行为,即是大型测试关注的重点。

测试运行要求

1、每个测试和其他测试之间都是独立的,使它们就能够以任意顺序来执行

2、测试不做任何数据持久化方面的工作。在这些测试用例离开测试环境的时候,要保证测试环境的状态与测试用例开始执行之前的状态是一样的

3、测试比重符合,大型测试/中小型测试=2/8,因为将近80%的bug发生在百分之20的代码模块里,并且中小型测试测出的bug更容易发现和修改,带来的影响也较小,收益更高

测试流程

场景/用例设计 + 测试脚本编写 + 测试报告 + 发送邮件 + 持续化集成

基于上边的分析,此文主要介绍对于 Pytest 5.1.1 + Python 3.7.4 + Flask + mysql 的后端中小型测试

场景/用例设计

首先是场景/用例设计,主要是在理解需求的基础上,对用户行为进行的分析和定量为业务场景校验,还包含参数校验,功能校验,完整性校验,健壮性校验。 例如:房间筛选,我们从上述分析中可以设计测试用例简略如下(包含但不仅限于):

测试脚本编写

上文已经介绍了测试遵循的规则,我们需要在保证测试和其他测试之间都是独立的,并且需要根据数据库和一些外部依赖中的值,进行健壮性测试,所以对数据库中的存储,不直接使用mock,而使用和用户相同的环境,来准备好基础数据,来确保测试的准确性,以及灵活性。。好的,步入正题 我们知道,Fixtures可以再每一个 test_case启动之前就运转起来,并且能够在测试用例结束的时候,做好环境的恢复,完美的保证了测试之间的独立特性,并且能够为一批测试用例,进行基础数据的准备。下边就是官方文档的链接

官网文档 fixture

目录结构

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-mock docs

测试报告

测试报告生成

我们用pytest-html 进行生成系统报告,然后进行邮件发送,同时进行一些特性化配置,这里的细节不展开讲,可以单独作为一篇文章展开。

pytest-html PyPI

通过指令生成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 到此为止,我们基本上整个的流程已经实现,希望对大家有所帮助~