测试

按照我们平时的开发习惯,是不是接下来就应该开发业务代码了?当然这样是可以的,但是我们这里不这样做,我们让测试先行,让测试来驱动开发,这样有什么好处呢?TDD最重要的功能就是保障代码的正确性,能够迅速发现、定位bug。关于TDD更多的知识可以自行Google。提供一个篇IBM关于TDD介绍的文章:浅谈测试驱动开发(TDD)

首先新增测试用的依赖包:Flask-Testingrequirements.txt文件下面(记得用pip命令安装哦~):

Flask-Testing==0.7.1

project目录下面新建一个tests的目录,然后在该目录使用下面新增几个文件:

(tdd3)$ touch __init__.py base.py test_config.py test_users.py

如果你对Flask-Testing还不太熟悉,在编写测试文件之前,可以先简单查看下文档

更新base.py文件:

# project/tests/base.py
from flask_testing import TestCase
from project import app, db

class BaseTestCase(TestCase):
    def create_app(self):
        app.config.from_object('project.config.TestingConfig')
        return app

    def setUp(self):
        db.create_all()
        db.session.commit()

    def tearDown(self):
        db.session.remove()
        db.drop_all()

首先,我们确保测试使用的数据库URI不是生产环境的,避免对线上环境产生任何影响。每次测试的时候创建数据表、完成后删除表,确保能够清理测试数据。

必须指定一个create_app 方法,并且返回flask app 实例,如果没有定义将会抛出NotImplementedError异常。

另外在tearDown方法中增加db.session.remove()方法,这样能够确保每次测试完成的时候能够将SQLAlchemysession属性移除掉,在下一次测试的时候始终是一个新的session

更新test_config.py文件:

from flask import current_app
from flask_testing import TestCase
from project import app

class TestDevelopmentConfig(TestCase):
    def create_app(self):
        app.config.from_object('project.config.DevelopmentConfig')
        return app

    def test_app_is_development(self):
        self.assertTrue(app.config['SECRET_KEY'] == 'secret')
        self.assertTrue(app.config['DEBUG'] is True)
        self.assertFalse(current_app is None)
        self.assertTrue(
            app.config['SQLALCHEMY_DATABASE_URI'] ==
            'mysql+pymysql://root:root321@users-db:3306/users_dev'
        )

class TestTestingConfig(TestCase):
    def create_app(self):
        app.config.from_object('project.config.TestingConfig')
        return app

    def test_app_is_testing(self):
        self.assertTrue(app.config['SECRET_KEY'] == 'secret')
        self.assertTrue(app.config['DEBUG'])
        self.assertTrue(app.config['TESTING'])
        self.assertFalse(app.config['PRESERVE_CONTEXT_ON_EXCEPTION'])
        self.assertTrue(
            app.config['SQLALCHEMY_DATABASE_URI'] ==
            'mysql+pymysql://root:root321@users-db:3306/users_test'
        )

class TestProductionConfig(TestCase):
    def create_app(self):
        app.config.from_object('project.config.ProductionConfig')
        return app

    def test_app_is_production(self):
        self.assertTrue(app.config['SECRET_KEY'] == 'secret')
        self.assertFalse(app.config['DEBUG'])
        self.assertFalse(app.config['TESTING'])

更新test_users.py文件:

import json
from project.tests.base import BaseTestCase

class TestUserService(BaseTestCase):
    def test_users(self):
        """确保ping的服务正常."""
        response = self.client.get('/ping')
        data = json.loads(response.data.decode())
        self.assertEqual(response.status_code, 200)
        self.assertIn('pong', data['message'])
        self.assertIn('success', data['status'])

然后我们在manage.py中新增一个测试的命令行支持:

import unittest
from flask_script import Manager
from project import app, db

manager = Manager(app)

@manager.command
def recreate_db():
    """重新创建数据表."""
    db.drop_all()
    db.create_all()
    db.session.commit()

@manager.command
def test():
    """运行测试."""
    tests = unittest.TestLoader().discover('project/tests', pattern='test_*.py')
    result = unittest.TextTestRunner(verbosity=2).run(tests)
    if result.wasSuccessful():
        return 0
    return 1

if __name__ == '__main__':
    manager.run()

unittest支持非常简单的测试发现,为了兼容测试发现,所有的测试文件(test_xxx.py)必须是可以从项目的顶级目录导入的包或者模块,我们这里就是利用TestLoader.discover()方法去发现project/tests包下面的所有的test_xxx.py测试文件。然后使用TextTestRunner用来执行测试用例,对测试进行编排并把结果返回给用户。

如果是单个的测试文件,推荐你将所有的测试方法放在同一个文件中,这样能够方便的使用unittest.main()方法执行:

import unittest
import flask_testing

# TODO your test cases

if __name__ == '__main__':
    unittest.main()

然后可以执行命令python test_xxx.py进行测试。

由于我们这里有test_config.pytest_users.py两个业务不一样的测试用例,所以我们使用test runner来收集所有的测试结果并生成最后的测试报告。

由于我们更新了依赖包,所以需要重新构建镜像:

(tdd3)$ docker-compose up -d --build

然后运行测试命令:

(tdd3)$ docker-compose run users-service python manage.py test

然后我们可以看到类似于下面的测试没有通过的提示信息:

======================================================================
FAIL: test_app_is_development (test_config.TestDevelopmentConfig)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/src/app/project/tests/test_config.py", line 13, in test_app_is_development
    self.assertTrue(app.config['SECRET_KEY'] == 'secret')
AssertionError: False is not true

这是因为我们的app.config中还没有SECRET_KEY这个key,所以assertTrue肯定是不会通过的,然后我们在config.pyBaseConfig类中新增SECRET_KEY属性:

class BaseConfig:
    """基础配置"""
    DEBUG = False
    TESTING = False
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SECRET_KEY = 'secret'

重新执行测试命令:

(tdd3)$ docker-compose run users-service python manage.py test
Starting users-db ... done
test_app_is_development (test_config.TestDevelopmentConfig) ... ok
test_app_is_production (test_config.TestProductionConfig) ... ok
test_app_is_testing (test_config.TestTestingConfig) ... ok
test_users (test_users.TestUserService)
确保ping的服务正常. ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.124s

OK

测试通过~~~