白盒测试 给 Flask 增加单元测试

不二家的小球迷 · 2017年09月15日 · 最后由 jacexh 回复于 2017年10月09日 · 1020 次阅读

前言

单元测试的重要性不言而喻,比如经典的测试金字塔,最底层的便是单元测试,它越接近代码层,收益越大。

之前在 Flask 编写微服务的过程中,并未注意过单元测试,最近准备重构代码的过程中,发现之前的代码还是有很大的优化空间,而且不适合写单元测试,为了方便单元测试的开展,还需要重新整理之前的框架,自己感觉非常得不偿失。

虽然是测试出身,但是自己却从来未真正实践单元测试。真的负罪感满满。

最近看了很多单元测试方面的知识,有一句话印象颇深,与大家共勉。

Martin Fowler 提到"在你不知道如何测试代码之前,就不该编写程序。而一旦你完成了程序,测试代码也应该完成。除非测试成功,你不能认为你编写出了可以工作的程序。"

简介

先介绍一下 Python 的单元测试常用框架

  • unittest
  • pytest
  • nose
unittest

unittest 是 Python 内置的标准类库。它的 API 跟 Java 的 JUnit、.net 的 NUnit,C++ 的 CppUnit 很相似。

unittest 中最核心的四个概念是:test case, test suite, test runner, test fixture。通过继承 unittest.TestCase 来创建一个测试用例。

unittest 的使用有一些潜规则

  • 每一个测试文件都需要写一个类,对于需要 set_up 和 tear_down 的方法,每一个测试文件里面都需要加上 set_up 和 tear_down
  • unittest 默认是按照字母和数字的顺序运行,倘若需要按照我们指定的顺序执行,需使用 suit.addTest 的方式去指定
pytest

pytest 是一个功能丰富、灵活的测试框架,但是它的语法很简单。创建一个单元测试就像编写一个模块一样。相比 unittest,实现相同的测试功能,py.test 做的事情更少。

pytest 有一些特点:

  • pytest 在命令行中有彩色输出
  • 不需要使用特定类模板
  • setup/teardown 语法与 unittest 的兼容性不如 nose 高,实现方式也不如 nose 直观
nose

nose 是对 unittest 的扩展,使得 python 的测试更加简单。nose 自动发现测试代码并执行,nose 提供了大量的插件,比如测试输出的 xUnitcompatible,覆盖报表等等。

nose 不使用特定的格式、不需要一个类容器,甚至不需要 import nose ~(这也就意味着它在写测试用例时不需要使用额外的 api)。

nose 的使用非常简单,自带光环:

  • 不用动不动就写个类,而只是写测试函数;
  • 自动查找和搜集测试,不需要自己手动搭建测试集;
  • 支持插件,可以搭配其他非常实用的标准化插件(coverage, output capture, drop into debugger on errors, doctests support, profiler)
  • 为测试打标签,并且可以根据标签非常灵活的选择测试集;
  • 并行测试;
  • 更好的支持 fixtures;
  • 产生器测试。

粉一下 nose 的宣言

nose extends unittest to make testing easier.

Flask 中加入 nose 的单测用例

先简单看一下我 Flask 的服务是

image

app 里面是主框架,main 里面分别有 api 和 agent,schedule,agent 是封装了邮件发送的异步操作,schedule 是封装了定时执行的后台运行操作,app 下面的 models 是数据库相关的表结构,而和 app 同级的 tests 下面,则是我新增的单测用例文件。

首先新建一个 tests 的包结构,然后在init.py 里面,增加一个类似 setup 的动作,

from app import create_app, db


app = create_app('testing')
app_ctx = app.app_context()
app_ctx.push()

test_app = app.test_client()
db.drop_all()
db.create_all()


def tearDown():
    app_ctx.pop()

在跑单元测试的时候,需要先启动一个 testing 配置的 mock 服务,然后先删除数据库,并初始化一个新的数据库,在测试结束之后,再释放初始化 push 的 app_context。

一般而言,Flask 服务需要测试的关注点主要是 models,functions,以及 views。当然 views 就是一些页面的测试,更加类似于用 selenium 实现。

先简单介绍对一个 api 和 models 的测试。

def test_logs_api():
    resp = tests.test_app.get('/api/test')
    assert_equal(resp.status_code, 200)
def test_log():
    log = models.Log(
        date_created=datetime.datetime.now(),
        result=111,
        job_name=111,
    )
    db.session.add(log)
    db.session.commit()

    log_id =models.Log.query.filter(models.Log.result ==
    111).first()
    assert log_id is not None

都很简单一目了然,我的经验就是对一个方法或者实例进行测试的时候,只用一个测试。这样方便以后维护,也使得逻辑更加清晰。

然后在命令行用 nose 调用执行:

nosetests -v --with-coverage --cover-package=app

然后可以看到测试结果和覆盖率的结果

Name                   Stmts   Miss  Cover
------------------------------------------
app/__init__.py           44      0   100%
app/config.py             58      3    95%
app/main/__init__.py       3      0   100%
app/main/agent.py         37     16    57%
app/main/api.py          135     53    61%
app/main/schedule.py      37     25    32%
app/models.py             84     13    85%
app/util/__init__.py       0      0   100%
app/util/util.py         130     33    75%
------------------------------------------
TOTAL                    528    143    73%
----------------------------------------------------------------------
Ran 6 tests in 6.905s

OK

谈谈收获

当真正去写单元测试的时候,才会意识到自己之前只是为了实现业务而工作,虽然基本实现了业务,但是很多 function 无法开展单元测试,才真正理解解耦,函数式编程。

我们在编程中应该以测试的思想去影响设计结构,就是我们常说的测试驱动开发,缺乏测试的思想会给软件项目带来巨大的潜在隐患。

当然测试不是万灵药,软件开发是艰难的工作。为了争取成功,我们必须时刻牢记真正的目标。不但要解决问题,而且要简洁,高效,优雅的去实现。

参考文献

nose 官网

pytest 官网

监视代码复杂度

Python 编写高质量代码

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 2 条回复 时间 点赞
不二家的小球迷 某 APP 跑步模块性能测试 中提及了此贴 09月28日 11:03

对 python 的几个主要测试框架介绍得都挺不错的。目前工作中用 pytest 比较多,nose 用得不多,但从介绍看来 nose 比 pytest 更好用,有空哥也要是试一下!

其实 pyunit 足够好用,nose、pytest 我这边一般只作为测试执行器

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册