Flask测试与部署

感情迁移 提交于 2020-01-29 11:20:37

1. 蓝图

之前的学习都是在单个文件中定义数据模型类、表单模型类、视图函数、路由等,但是对于大型项目来说将所有代码放在一个文件会让代码可读性变差且难以维护。

真正的项目应根据具体不同的功能,划分成不同的模块,降低各功能模块之间的耦合度

使用模块导入解决耦合问题:
    - 即模型类和主程序放在一个模块内、视图函数放在一个模块内(导入app对象):可以降低耦合度,但是不能解决路由映射问题
        - 模块导入出现循环导入问题(死锁)----》推迟一方的导入,让另一方先完成导入
    - 使用app.route()返回装饰器解决:视图函数只定义不进行路由绑定,在主程序导入视图函数后使用app.route()进行路由绑定   



蓝图:用于实现单个应用的视图、模板、静态文件的集合,是一个模块化处理的类(类似于Django中的一个应用模块的所有内容)
简单来说,蓝图就是一个独立模块的抽象代表,可以用来保存在应用对象上执行的操作,主要用来实现客户端请求和URL相互关联的功能

蓝图使用步骤:
     1. 创建蓝图对象:必须指定两个参数:蓝图的名字及蓝图所在的模块(蓝图对象用来在视图函数中注册路由使用;蓝图名字指向当前模块)
     2. 使用蓝图注册路由绑定视图函数:蓝图对象.route(rules, **args)(暂时存储在蓝图对象的defered_functions列表中)
     3. 在主程序app对象上注册蓝图:将蓝图绑定的路由添加到app对象上(类似于Django中添加应用到INSTALLED_APPS)
        第一个参数是蓝图对象,url_prefix参数默认值是根路由,如果指定,会在蓝图注册的url添加配置的url前缀(类似于Django中会在子路由前添加主路由)

蓝图的运行机制:蓝图用来保存可以在应用对象上执行的操作
    注册路由就是一种操作,当在程序实例上调用app.route装饰器注册路由时,该操作将修改app对象的url_map路由映射列表
    当我们在蓝图对象上调用route装饰器注册路由时,其只是在内部执行延迟注册操作:在蓝图对象的defered_functions列表中添加了一个url映射
    当执行应用对象的register_blueprint() 方法(注册蓝图)时,应用对象才会从蓝图对象的defered_functions 列表中取出每一项,即调用应用对象的 add_url_rule() 方法,此时才会真正修改程序实例的路由映射列表
users.py:
    from flask import Blueprint
    
    app_users = Blueprint("users", __name__)  # 创建蓝图对象
    
    
    @app_users.route("/login")
    def login():
        return "login page"
    
    
    @app_users.route("/register")
    def register():
        return "register page"
        
app.py
    from flask import Flask
    from users import app_users  # 导入蓝图对象
    
    app = Flask(__name__)
    app.register_blueprint(app_users)  # 注册蓝图对象:将app_users蓝图指向的模块里面的url映射添加到app对象的url_map中
    
    
    @app.route("/")
    def index():
        return "index page"
    
    
    if __name__ == "__main__":
        app.run(debug=True)
使用目录组织蓝图
在Django中一个应用模块都是一个单独的目录,里面有模板、静态文件、视图函数、路由等,flask使用目录组织蓝图:
    - 1. 创建Python包:创建蓝图:Python包被导入时会自动执行__init__文件,因此将蓝图定义在__init__文件
    - 2. 新建views.py存放视图函数,需要从包中导入蓝图对象进行路由注册
    - 3. 主程序导入蓝图对象,注册蓝图

注意:程序执行时,主程序从包中导入蓝图,导入后继续执行,并不知道views.py中使用蓝图进行了路由注册,因此不能成功将路由映射关系添加到全局url_map中

解决办法:在init文件创建蓝图对象后,导入views.py文件,使蓝图对象知道在views文件中进行了路由注册


蓝图中模板文件的处理:
    一个蓝图就是一个独立的应用模块,以目录(包)的形式组织时,模块可以有独立的静态文件和模板文件目录
    当在视图函数中使用模板文件返回响应时,直接写模板文件名字程序会报错:jinja2.exceptions.TemplateNotFound
    
解决办法:在创建蓝图对象时需要显式指明静态文件目录和模板文件目录(默认均为None)

模板文件优先级:
    - 如果蓝图中模板目录没有该模板文件,flask会继续向工程目录下的模板目录中寻找该模板文件;均未找到报错
    - 当蓝图目录下和工程目录下均存在,则使用工程目录下的模板文件(工程目录优先级较高)
    因此一般情况下建议只在蓝图目录下创建静态目录及模板目录或者仅在工程目录下创建即可(避免模板文件冲突问题)

2. 单元测试

Web程序开发过程一般包括以下几个阶段:需求分析,设计阶段,实现阶段,测试阶段。其中测试阶段通过人工或自动来运行测试某个系统的功能,目的是检验其是否满足需求,并得出特定的结果,以达到弄清楚预期结果和实际结果之间的差别的最终目的。

测试从软件开发过程可以分为:单元测试、集成测试、系统测试等。在众多的测试中,与程序开发人员最密切的就是单元测试,因为单元测试是由开发人员进行的,而其他测试都由专业的测试人员来完成。

什么是单元测试?单元测试就是开发者编写一小段代码,检验目标代码的功能是否符合预期。通常情况下,单元测试主要面向一些功能单一的模块进行。

Web开发过程中,单元测试实际上就是一些“断言”(assert)代码
断言是判断一个函数或对象的一个方法所产生的结果是否符合你期望的那个结果,单元测试中,一般使用assert关键字来断言结果

使用方式:assert后跟表达式,如果表达式为真则断言成功,程序继续执行;表达式为假,断言失败,程序抛出异常AssertionError,终止程序执行

编写单元测试时,我们需要编写一个测试类,从unittest.TestCase继承
    - 以test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行
    - 对每一类测试都需要编写一个test_xxx()方法,最常用的断言就是assertEqual()
    - 另一种重要的断言就是期待抛出指定类型的Error:
        with self.assertRaises(KeyError):
            value = d['empty']
            
运行单元测试:
    方法1:
        if __name__ == '__main__':
            unittest.main()
    方法2:命令行通过参数-m unittest直接运行单元测试:python -m unittest xxx
        
常用的断言方法用:断言方法存在于unittest.TestCase中,类中直接使用self.的形式即可调用
    assertEqual()     如果两个值相等,则pass(常用)
    assertNotEqual()  如果两个值不相等,则pass
    assertTrue()      判断bool值为True,则pass
    assertFalse()     判断bool值为False,则pass
    assertIsNone()    不存在,则pass
    assertIsNotNone() 存在,则pass
    

单元测试的特殊方法:setUp()和tearDown()方法(两个方法在每调用一个测试方法的前后分别被执行),常用在测试前准备工作,测试后处理工作

flask项目代码进行单元测试时,建议激活测试标志:
    app.config['TESTING'] = True
    app.testing = True
    

import unittest
from login import app
import json


class LoginTest(unittest.TestCase):
    """构造单元测试案例"""
    def setUp(self) -> None:
        """单元测试前的准备工作:在单元测试之前进行"""
        # 创建web请求客户端,flask提供
        # 设置flask工作在测试模型下
        app.testing = True
        self.client = app.test_client()

    def test_empty_username_password(self):
        """测试用户名密码不完整情况"""

        # 1. 测试用户名及密码均为空
        # 利用客户端发送web请求,返回结果就是视图函数的响应对象
        ret = self.client.post("/login", data={})
        resp = ret.data  # 从响应对象提取响应体数据(json格式)

        resp = json.loads(resp.decode("utf-8"))  # 转化json格式为对应Python数据类型

        # 断言测试
        self.assertIn("code", resp)  # 判断key code是否在resp中
        self.assertEqual(resp["code"], 1)
        
    def test_wrong_username_password(self):
        """测试用户名或密码错误"""

        # 1. 测试用户名正确,密码错误
        ret = self.client.post("/login", data={"username": "fengdi", "password": "123456"})
        resp = ret.data
        resp = json.loads(resp.decode("utf-8"))

        self.assertIn("code", resp)
        self.assertEqual(resp["code"], 2)


if __name__ == "__main__":
    unittest.main()
# 数据库测试:
import unittest
from book_demo import Author, app, db


class DatabaseTest(unittest.TestCase):
    """数据库测试"""
    def setUp(self) -> None:
        app.testing = True
        app.config["SQLALCHEMY_DATABASE_URI"] = 'mysql://yuxi:199618@127.0.0.1:3306/test0'  # 使用隔离的测试数据库
        app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True

        # 创建数据表
        db.create_all()

    def tearDown(self) -> None:
        """所有测试执行完后进行,通常用来进行清理操作"""
        db.session.remove()  # 清除数据库连接
        db.drop_all()  # 删除测试创建的数据表

    def test_add_author(self):
        """测试添加作者的数据库操作"""
        author = Author(name="小丸子", email="zxqfengdi@163.com")
        db.session.add(author)
        db.session.commit()

        result_author = Author.query.filter_by(name="小丸子").first()
        self.assertIsNotNone(result_author)

3. flask部署

实际生产中,可能有多台业务服务器、单独的数据服务器等,通常使用nginx提供唯一网站入口,由NGINX服务器负载均衡(提供静态文件)并将请求转发到业务服务器进行处理
flask项目使用Gunicorn作为服务器进行部署:Gunicorn服务器与各种Web框架兼容,实现非常简单,轻量级的资源消耗,不需要编写配置文件

Nginx部署简单,内存消耗少成本低,Nginx既可以做正向代理,也可以做反向代理
    - 正向代理:请求经过代理服务器从局域网发出,然后到达互联网上的服务器 特点:服务端并不知道真正的客户端是谁
    - 反向代理:请求从互联网发出,先进入代理服务器,再转发给局域网内的服务器特点:客户端并不知道真正的服务端是谁
    
1. 安装gunicorn:pip install gunicorn
   相关命令:
        gunicorn -h  查看常用参数
        直接运行:gunicorn 文件名:flask程序实例名(默认:127.0.0.1:8000)
        指定进程和端口号运行:gunicorn -w 4 -b 127.0.0.1:5000 文件名:flask程序示例名(bind:ip和端口)
        写访问日志:--acess-logfile 日志文件路径
        
        如:gunicorn -w 4 -b 127.0.0.1:5000 --access-logfile ./logs/log.txt app:app

        后台运行(守护进程):gunicorn -w 4 -b 127.0.0.1:5000 --access-logfile ./logs/log.txt app:app -D
        
2. 结合NGINX部署,配置文件如下:
    # Nginx负载均衡
    upstream flaskproject {
    	server 127.0.0.1:8080;
	    server 127.0.0.1:8081;
    }
    
    # 客户端请求NGINX转发到gunicorn服务器
        # nginx主服务:80端口
        server {
            listen       80;
            server_name  localhost;
    
            #charset koi8-r;
    
            #access_log  logs/host.access.log  main;
    
            # 动态请求转发gunicorn
            location / {
                proxy_pass http://flaskproject;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
            }
        }
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!