Day 29 Unittest

在做完了程序之後,就要来测试一下是否正常运作对吧。不过当你做完了数十个 API 之後,我相信你一定不会有心情去一个一个慢慢使用 Swagger 来测试的。所以就出现了单元测试,能够一次解决需要一个一个测试的问题。

首先,在 Python 的标准库中(安装 Python 时会顺便安装的),有一个 unittest 的套件,看名字也知道是用在单元测试;後来出现了 pytest 这个更加热门的框架,同样用於单元测试,在 Flask 官方文件中也是使用这个方式;而 Flask 也出现一个专门用於单元测试的插件 Flask-Testing,Flask-Testing 是基於 unittest 所建立的插件。

总而言之
unittest: Python 标准库中的单元测试套件。
pytest: Python 的第三方单元测试框架。
Flask-Testing: Flask 中基於 unittest 的单元测试插件。

Flask-Testing & pytest

首先当然还是要先安装对吧!不过因为我想讲一下这两个之间的特殊关系,需要将两个套件一起安装。

$ pipenv install pytest Flask-Testing

然後我们使用上一篇的架构改一下继续使用,改完之後变这样:

ithome
├── apis
│   ├── __init__.py
│   └── api.py
├── base
│   └── __init__.py
├── db
│   └── __init__.py
├── test  # 单元测试的东东
│   ├── __init__.py  # tests 基本的东西
│   └── test_api.py  # 测试 api.py 里面所有 API 的档案
├── app.py
├── config.py
├── Pipfile
└── Pipfile.lock

先来说一下测试的原理,简单说就是拿着资料实际去做一次,然後看回传的结果是否正确,就是这麽简单。

test/__init__.py

from flask_testing import TestCase

from app import app
import config


class BaseTestCase(TestCase):

    def create_app(self):
		# 必要。须回传 Flask 实体。
        app.config.from_object(config.TestingConfig)
        return app

    def setUp(self):
		# 可不写。测试前会执行的东西,相当於 pytest 中 @pytest.fixture 这个装饰器
		# 可以用於生出一个乾净(没有资料)的资料库之类的,不过因为我是用奇怪的方式弄出类似资料库的东东,所以就没有写
        pass

    def tearDown(self):
		# 可不写。测试後会执行的东西,相当於 pytest 中 @pytest.fixture 这个装饰器 function 内 yield 之後的程序
		# 可以用於删除不乾净(测试後被塞入资料)的资料库之类的
        pass
	
	@classmethod
	def setUpClass(self):
		# 可不写。相当於 setUp ,不过不同於 setUp 是执行一个 Function ,而是先执行一个 Class,详细用法参考 @classmethod 或是下面的网址
		# https://docs.python.org/zh-tw/3/library/unittest.html#unittest.TestCase.setUpClass
		pass
	
	@classmethod
	def tearDownClass(self):
		# 可不写。相当於 tearDown ,不过 setUpClass 同样为执行一个 Class,详细用法参考 @classmethod 或是下面的网址
		# https://docs.python.org/zh-tw/3/library/unittest.html#unittest.TestCase.tearDownClass
		pass
	
	def setUpModule():
		# 可不写。同样相当於 setUp ,不过不同於 setUp 以及 setUpClass 是执行一个 Function 或是 Class,而是先执行一个 Module,详细用法参考下面的网址
		# https://docs.python.org/zh-tw/3/library/unittest.html#setupmodule-and-teardownmodule
		pass

	def tearDownModule():
		# 可不写。相当於 tearDown ,不过 setUpModule 同样为执行一个 Module,详细用法参考 setUpModule 的网址
		pass

test/test_apis.py

import json

from . import BaseTestCase


class TestAccountApi(BaseTestCase):
    def test_register(self):
        # 实际送出请求
        response = self.client.post(
            '/account/register',
            data=json.dumps({
                'email': '[email protected]',
                'password': 'test'
            }),
            content_type='application/json'
        )

        # 判断回应是否正确
        self.assertEqual(response.json, {'status': '0', 'message': ''})
        self.assertEqual(response.status_code, 200)

在这边要注意一点,测试文件档名必须以 test 为开头,以及测试文件内的 Class 以及 Function 也必须以 test 开头,否则不会自动加载(就是你想偷懒少打一些指令那就用 test 开头,不然那个东西就要多打指令去测)。

在开始执行测试前我要先说一下为什麽用了 Flask-Testing 这个插件之後,还需要安装 pytest 。如果你有留意 import 进来的东西的话,应该有注意到几乎都只有用到 Flask-Testing 而已,那为什麽还需要安装 pytest 呢?

这是因为 pytest 能够支援 unittest 的测试写法,而且可以少打一些测试时的执行指令与程序码(蛤,你问说为什麽不全部改 pytest 就好,因为谁让我先学的是 Flask-Testing 的写法,虽然 pytest 的装饰器也不错用)。

然後就可以在 terminal 中输入下方的指令开始测试(因为奇怪的资料库写法,导致测试没有写有关生出资料库的部分,所以 redis-server 要先执行)。

$ pipenv run pytest

结果就会像这样:

如果想要看到较详细的东西,可以这样输入:

$ pipenv run pytest -v

如果想要看到较简单的东西,可以这样输入:

$ pipenv run pytest -q

Coverage

虽然做完了测试,但是如何知道测试有没有完整,如果做了数十个 API ,但是指测试了一个 API ,其他的 API 有没有出问题也不知道。所以就必须测试覆盖率,而 pytest 有一个测试覆盖率的插件 pytest-cov。

同样使用 pip 安装。

$ pipenv install pytest-cov

安装完後使用下面的指令就可以测试覆盖率了。

# --cov 为指定计算覆盖率的范围,不加的话会连同 import 的所有套件一起计算
$ pipenv run pytest --cov=./

测完会像这样:

虽然看出来 api.py 有 2 行没测到(对,单位是行,包括 import 、 def 及 class ,但不含装饰器),还有 app.py 有 1 行没测到,但是要在大海里摸针确实有点困难,所以就要生出一个测试报告。就要使用下面的指令。

$ pipenv run pytest --cov=./ --cov-report=html

测完的报告会以 html 格式存放在 htmlcov 里面,打开 index.html 就可以看到总报告,像这样:

接着点 apis/account/api.py ,就可以看到里面有哪行没有测试到了。像这样:

会这样的原因是给了正确的资料所以正常回传,当然有错误发生时才会执行的程序码当然没有测试到,所以在 test/test_apis.py 里面多加一个测试错误发生时的回传就让它消失了。像这样:

    def test_register_error(self):
		# 实际送出请求
        response = self.client.post(
            '/account/register',
            data=json.dumps({}),
            content_type='application/json'
        )

        # 判断回应是否正确
        self.assertEqual(response.json, {'status': '1', 'message': 'error'})
        self.assertEqual(response.status_code, 200)

加上了这几行测试错误发生时的程序码之後,在执行一次就会发现那边的 miss 消失了(app.py 我不修的原因自己做完後看一下是哪行产生 miss 就知道了)。像这样:

参考资料

Pytest测试框架(三):pytest fixture 用法

pytest文档57-计算单元测试代码覆盖率(pytest-cov)

什么是静态代码分析?

那麽就大概这样,单元测试以及覆盖率是开发要结束之前几乎必须要做的最後一个动作,不过单元测试只能够测试是否有按照预期的结果回传而已,而覆盖率只能够测试多少部分的程序码有测试过而已,并不代表是最有效率或是最简洁的写法。 pytest 还有能够测试是否有按照 pep8 格式以及静态分析的插件,pep8 只要 Day 02 有正确设定基本上不需要测了;静态分析是啥以及须不需要测试就看上面的 什么是静态代码分析? (竟然是 MatLab ?) 说明後自行评估了 (恩对,我懒)。

大家掰~掰~


<<:  [Day29] Cost Plaining

>>:  Day 29:利用 NPM 来安装 Next 布景主题

Code Generator 结构

接续上一篇的 annotation processor 实作,我们的 annotation proc...

Day-09 版面配置Layout

本篇内容想介绍<activity_main.xml>配置档,版面配置档让使用者可在这个环...

[Day12] 於DialogFlow中实践对话流设计

范例:询问用户喜欢的颜色 在这个范例里,我们假设一个要蒐集使用者偏好颜色的资料集。 并透过语音助理来...

全端入门Day05_何谓全端之後端首篇

今天要来介绍後端,所谓的後端简单来说就是负责资料的部分,因为有关於资料都会是他们处理,而要让资料显示...

[Day 28] 来做一个人脸互动的程序吧!

在我完成人脸关键点与人脸对齐的学习後,觉得眼睛有点累想要休息 -- 这时一个应用就出来了! 我们每...