Flask測試和部署


一 藍圖Blueprint

為什么學習藍圖?

我們學習Flask框架,是從寫單個文件,執行hello world開始的。我們在這單個文件中可以定義路由、視圖函數、定義模型等等。但這顯然存在一個問題:隨着業務代碼的增加,將所有代碼都放在單個程序文件中,是非常不合適的。這不僅會讓代碼閱讀變得困難,而且會給后期維護帶來麻煩。

如下示例:我們在一個文件中寫入多個路由,這會使代碼維護變得困難。

 from flask import Flask    
    app = Flask(__name__)    
    @app.route('/')
    def index():
        return 'index'

    @app.route('/list')
    def list():
        return 'list'

    @app.route('/detail')
    def detail():
        return 'detail'
View Code

 

問題:一個程序執行文件中,功能代碼過多。就是讓代碼模塊化。根據具體不同功能模塊的實現,划分成不同的分類,降低各功能模塊之間的耦合度。python中的模塊制作和導入就是基於實現功能模塊的封裝的需求。

嘗試用模塊導入的方式解決: 我們把上述一個py文件的多個路由視圖函數給拆成兩個文件:app.py和admin.py文件。app.py文件作為程序啟動文件,因為admin文件沒有應用程序實例app,在admin文件中要使用app.route路由裝飾器,需要把app.py文件的app導入到admin.py文件中。

# 文件app.py
from flask import Flask
# 導入admin中的內容
from admin import *
app = Flask(__name__)

@app.route('/')
def index():
    return 'index'

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


# 文件admin.py    
from app import app


@app.route('/list')
def list():
    return 'list'

@app.route('/detail')
def detail():
    return 'detail'
View Code

啟動app.py文件后,發訪問http://127.0.0.1:5000/list,此時會報錯:ImportError: cannot import name 'app'。因為模塊間產生了死鎖。

 

什么是藍圖?

藍圖:用於實現單個應用的視圖、模板、靜態文件的集合。

藍圖就是模塊化處理的類。

簡單來說,藍圖就是一個存儲操作路由映射方法的容器,主要用來實現客戶端請求和URL相互關聯的功能。 在Flask中,使用藍圖可以幫助我們實現模塊化應用的功能。

 

藍圖的運行機制:

藍圖是保存了一組將來可以在應用對象上執行的操作。注冊路由就是一種操作,當在程序實例上調用route裝飾器注冊路由時,這個操作將修改對象的url_map路由映射列表。當我們在藍圖對象上調用route裝飾器注冊路由時,它只是在內部的一個延遲操作記錄列表defered_functions中添加了一個項。當執行應用對象的 register_blueprint() 方法時,應用對象從藍圖對象的 defered_functions 列表中取出每一項,即調用應用對象的 add_url_rule() 方法,這將會修改程序實例的路由映射列表。

 

藍圖的使用:

1.創建藍圖對象。

#Blueprint必須指定兩個參數,admin表示藍圖的名稱,__name__表示藍圖所在模塊
admin = Blueprint('admin',__name__)

2.注冊藍圖路由。

@admin.route('/admin') def admin_index(): return 'admin_index'

3.在程序實例中注冊該藍圖。

app.register_blueprint(admin,url_prefix='/admin')

示例, 創建myapp.py 和 藍圖:good.py文件

myapp.py:

from flask import Blueprint


get_list = Blueprint("get_list", __name__)


@get_list.route('/get_list')
def goods_list():
    return 'goods_list'
View Code

good.py:

from flask import Flask
from good import get_list

app = Flask(__name__)

app.register_blueprint(get_list)


@app.route('/index')
def index():
    return 'index'


if __name__ == '__main__':
    app.run()
View Code

查看路由:

 

二 單元測試

為什么要測試?

Web程序開發過程一般包括以下幾個階段:[需求分析,設計階段,實現階段,測試階段]。其中測試階段通過人工或自動來運行測試某個系統的功能。目的是檢驗其是否滿足需求,並得出特定的結果,以達到弄清楚預期結果和實際結果之間的差別的最終目的。

測試的分類:

測試從軟件開發過程可以分為:單元測試、集成測試、系統測試等。在眾多的測試中,與程序開發人員最密切的就是單元測試,因為單元測試是由開發人員進行的,而其他測試都由專業的測試人員來完成。所以我們主要學習單元測試。

 

什么是單元測試?

程序開發過程中,寫代碼是為了實現需求。當我們的代碼通過了編譯,只是說明它的語法正確,功能能否實現則不能保證。 因此,當我們的某些功能代碼完成后,為了檢驗其是否滿足程序的需求。可以通過編寫測試代碼,模擬程序運行的過程,檢驗功能代碼是否符合預期。

單元測試就是開發者編寫一小段代碼,檢驗目標代碼的功能是否符合預期。通常情況下,單元測試主要面向一些功能單一的模塊進行。

舉個例子:一部手機有許多零部件組成,在正式組裝一部手機前,手機內部的各個零部件,CPU、內存、電池、攝像頭等,都要進行測試,這就是單元測試。

在Web開發過程中,單元測試實際上就是一些“斷言”(assert)代碼。

斷言就是判斷一個函數或對象的一個方法所產生的結果是否符合你期望的那個結果。 python中assert斷言是聲明布爾值為真的判定,如果表達式為假會發生異常。單元測試中,一般使用assert來斷言結果。

斷言方法的使用:

斷言語句類似於:

if not expression:
    raise AssertionError

常用的斷言方法:

assertEqual     如果兩個值相等,則pass
assertNotEqual  如果兩個值不相等,則pass
assertTrue      判斷bool值為True,則pass
assertFalse     判斷bool值為False,則pass
assertIsNone    不存在,則pass
assertIsNotNone 存在,則pass

 

如何測試?

簡單的測試用例:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,

def fibo(x):
    if x == 0:
        resp = 0
    elif x == 1:
        resp = 1
    else:
        return fibo(x-1) + fibo(x-2)
    return resp
assert fibo(5) == 5
View Code

 

單元測試的基本寫法:

 首先,定義一個類,繼承自unittest.TestCase

import unittest
class TestClass(unitest.TestCase):
    pass

其次,在測試類中,定義兩個測試方法

import unittest
class TestClass(unittest.TestCase):

    #該方法會首先執行,方法名為固定寫法
    def setUp(self):
        pass

    #該方法會在測試代碼執行完后執行,方法名為固定寫法
    def tearDown(self):
        pass

最后,在測試類中,編寫測試代碼

import unittest
class TestClass(unittest.TestCase):

    #該方法會首先執行,相當於做測試前的准備工作
    def setUp(self):
        pass

    #該方法會在測試代碼執行完后執行,相當於做測試后的掃尾工作
    def tearDown(self):
        pass
    #測試代碼
    def test_app_exists(self):
        pass

 

登錄測試:

 login.py:

# coding:utf-8

from flask import Flask, request, jsonify


app = Flask(__name__)


@app.route("/login", methods=["POST"])
def login():
    """登錄"""
    name = request.form.get("name")
    password = request.form.get("password")

    # ""  0  [] () {} None 在邏輯判斷時都是假
    if not all([name, password]):
        # 表示name或password中有一個為空或者都為空
        return jsonify(code=1, message="參數不完整")

    if name == "admin" and password =="123456":
        return jsonify(code=0, message="OK")
    else:
        return jsonify(code=2, message="用戶名或密碼錯誤")


if __name__ == '__main__':
    app.run()
View Code

test_login.py :

# coding:utf-8

import unittest
from login import app
import json


class TestLogin(unittest.TestCase):
    """定義測試案例"""
    def setUp(self):
        """在執行具體的測試方法前,先被調用"""
        # 可以使用python的http標准客戶端進行測試
        # urllib  urllib2  requests

        # 使用flask提供的測試客戶端進行測試
        self.client = app.test_client()

    def test_empty_name_password(self):
        """測試模擬場景,用戶名或密碼不完整"""
        # 使用客戶端向后端發送post請求, data指明發送的數據,會返回一個響應對象
        response = self.client.post("/login", data={})

        # respoonse.data是響應體數據
        resp_json = response.data

        # 按照json解析
        resp_dict = json.loads(resp_json)

        # 使用斷言進行驗證
        self.assertIn("code", resp_dict)

        code = resp_dict.get("code")
        self.assertEqual(code, 1)

        # 測試只傳name
        response = self.client.post("/login", data={"name": "admin"})

        # respoonse.data是響應體數據
        resp_json = response.data

        # 按照json解析
        resp_dict = json.loads(resp_json)

        # 使用斷言進行驗證
        self.assertIn("code", resp_dict)

        code = resp_dict.get("code")
        self.assertEqual(code, 1)

    def test_wrong_name_password(self):
        """測試用戶名或密碼錯誤"""
        # 使用客戶端向后端發送post請求, data指明發送的數據,會返回一個響應對象
        response = self.client.post("/login", data={"name": "admin", "password": "123"})

        # respoonse.data是響應體數據
        resp_json = response.data

        # 按照json解析
        resp_dict = json.loads(resp_json)

        # 使用斷言進行驗證
        self.assertIn("code", resp_dict)

        code = resp_dict.get("code")
        self.assertEqual(code, 2)


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

 

數據庫測試:

#coding=utf-8
import unittest
from author_book import *

#自定義測試類,setUp方法和tearDown方法會分別在測試前后執行。以test_開頭的函數就是具體的測試代碼。

class DatabaseTest(unittest.TestCase):
    def setUp(self):
        app.config['TESTING'] = True
        app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@localhost/test0'
        self.app = app
        db.create_all()

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

    #測試代碼
    def test_append_data(self):
        au = Author(name='test')
        bk = Book(info='python')
        db.session.add_all([au,bk])
        db.session.commit()
        author = Author.query.filter_by(name='test').first()
        book = Book.query.filter_by(info='python').first()
        #斷言數據存在
        self.assertIsNotNone(author)
        self.assertIsNotNone(book)
View Code

 

三 部署

當我們執行下面的hello.py時,使用的flask自帶的服務器,完成了web服務的啟動。在生產環境中,flask自帶的服務器,無法滿足性能要求,我們這里采用Gunicorn做wsgi容器,來部署flask程序。Gunicorn(綠色獨角獸)是一個Python WSGI的HTTP服務器。從Ruby的獨角獸(Unicorn )項目移植。該Gunicorn服務器與各種Web框架兼容,實現非常簡單,輕量級的資源消耗。Gunicorn直接用命令啟動,不需要編寫配置文件,相對uWSGI要容易很多。

區分幾個概念:

WSGI:全稱是Web Server Gateway Interface(web服務器網關接口),它是一種規范,它是web服務器和web應用程序之間的接口。它的作用就像是橋梁,連接在web服務器和web應用框架之間。

uwsgi:是一種傳輸協議,用於定義傳輸信息的類型。

uWSGI:是實現了uwsgi協議WSGI的web服務器。

我們的部署方式: nginx + gunicorn + flask

# hello.py

from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
    return '<h1>hello world</h1>'

if __name__ == '__main__':
    app.run(debug=True)
View Code

使用Gunicorn:

web開發中,部署方式大致類似。簡單來說,前端代理使用Nginx主要是為了實現分流、轉發、負載均衡,以及分擔服務器的壓力。Nginx部署簡單,內存消耗少,成本低。Nginx既可以做正向代理,也可以做反向代理。

正向代理:請求經過代理服務器從局域網發出,然后到達互聯網上的服務器。

特點:服務端並不知道真正的客戶端是誰。

反向代理:請求從互聯網發出,先進入代理服務器,再轉發給局域網內的服務器。

特點:客戶端並不知道真正的服務端是誰。

區別:正向代理的對象是客戶端。反向代理的對象是服務端。

安裝gunicorn

pip install gunicorn

直接運行:

#直接運行,默認啟動的127.0.0.1::8000
gunicorn 運行文件名稱:Flask程序實例名

指定進程和端口號: -w: 表示進程(worker)。 -b:表示綁定ip地址和端口號(bind)

$gunicorn -w 4 -b 127.0.0.1:5001 運行文件名稱:Flask程序實例名                 # 加 -d 守護進程

 

Nginx配置:

默認安裝到/usr/local/nginx/目錄,進入目錄。

啟動nginx:

#啟動
sudo sbin/nginx
#查看
ps aux | grep nginx
#停止
sudo sbin/nginx -s stop

打開/usr/local/nginx/conf/nginx.conf文件

server {
    # 監聽80端口
    listen 80;
    # 本機
    server_name localhost; 
    # 默認請求的url
    location / {
        #請求轉發到gunicorn服務器
        proxy_pass http://127.0.0.1:5001; 
        #設置請求頭,並將頭信息傳遞給服務器端 
        proxy_set_header Host $host; 
    }
}

 

四 Restful

2000年,Roy Thomas Fielding博士在他的博士論文《Architectural Styles and the Design of Network-based Software Architectures》中提出了幾種軟件應用的架構風格,REST作為其中的一種架構風格在這篇論文中進行了概括性的介紹。

REST:Representational State Transfer的縮寫,翻譯:“具象狀態傳輸”。一般解釋為“表現層狀態轉換”。

REST是設計風格而不是標准。是指客戶端和服務器的交互形式。我們需要關注的重點是如何設計REST風格的網絡接口。

  • REST的特點:
  • 具象的。一般指表現層,要表現的對象就是資源。比如,客戶端訪問服務器,獲取的數據就是資源。比如文字、圖片、音視頻等。

  • 表現:資源的表現形式。txt格式、html格式、json格式、jpg格式等。瀏覽器通過URL確定資源的位置,但是需要在HTTP請求頭中,用Accept和Content-Type字段指定,這兩個字段是對資源表現的描述。

  • 狀態轉換:客戶端和服務器交互的過程。在這個過程中,一定會有數據和狀態的轉化,這種轉化叫做狀態轉換。其中,GET表示獲取資源,POST表示新建資源,PUT表示更新資源,DELETE表示刪除資源。HTTP協議中最常用的就是這四種操作方式。

    • RESTful架構:
    • 每個URL代表一種資源;
    • 客戶端和服務器之間,傳遞這種資源的某種表現層;
    • 客戶端通過四個http動詞,對服務器資源進行操作,實現表現層狀態轉換。

如何設計符合RESTful風格的API:

1.域名:

將api部署在專用域名下:

http://api.example.com

或者將api放在主域名下:

http://www.example.com/api/

2.版本:

將API的版本號放在url中。

http://www.example.com/app/1.0/info
http://www.example.com/app/1.2/info

3.路徑:

路徑表示API的具體網址。每個網址代表一種資源。 資源作為網址,網址中不能有動詞只能有名詞,一般名詞要與數據庫的表名對應。而且名詞要使用復數。

錯誤示例:

http://www.example.com/getGoods
http://www.example.com/listOrders

正確示例:

#獲取單個商品
http://www.example.com/app/goods/1
#獲取所有商品
http://www.example.com/app/goods

4.使用標准的HTTP方法:

對於資源的具體操作類型,由HTTP動詞表示。 常用的HTTP動詞有四個。

GET     SELECT :從服務器獲取資源。
POST    CREATE :在服務器新建資源。
PUT     UPDATE :在服務器更新資源。
DELETE  DELETE :從服務器刪除資源。

示例:

#獲取指定商品的信息
GET http://www.example.com/goods/ID
#新建商品的信息
POST http://www.example.com/goods
#更新指定商品的信息
PUT http://www.example.com/goods/ID
#刪除指定商品的信息
DELETE http://www.example.com/goods/ID

5.過濾信息:

如果資源數據較多,服務器不能將所有數據一次全部返回給客戶端。API應該提供參數,過濾返回結果。 實例:

#指定返回數據的數量
http://www.example.com/goods?limit=10
#指定返回數據的開始位置
http://www.example.com/goods?offset=10
#指定第幾頁,以及每頁數據的數量
http://www.example.com/goods?page=2&per_page=20

6.狀態碼:

服務器向用戶返回的狀態碼和提示信息,常用的有:

200 OK  :服務器成功返回用戶請求的數據
201 CREATED :用戶新建或修改數據成功。
202 Accepted:表示請求已進入后台排隊。
400 INVALID REQUEST :用戶發出的請求有錯誤。
401 Unauthorized :用戶沒有權限。
403 Forbidden :訪問被禁止。
404 NOT FOUND :請求針對的是不存在的記錄。
406 Not Acceptable :用戶請求的的格式不正確。
500 INTERNAL SERVER ERROR :服務器發生錯誤。

7.錯誤信息:

一般來說,服務器返回的錯誤信息,以鍵值對的形式返回。

{
    error:'Invalid API KEY'
}

8.響應結果:

針對不同結果,服務器向客戶端返回的結果應符合以下規范。

#返回商品列表
GET    http://www.example.com/goods
#返回單個商品
GET    http://www.example.com/goods/cup
#返回新生成的商品
POST   http://www.example.com/goods
#返回一個空文檔
DELETE http://www.example.com/goods

9.使用鏈接關聯相關的資源:

在返回響應結果時提供鏈接其他API的方法,使客戶端很方便的獲取相關聯的信息。

10.其他:

服務器返回的數據格式,應該盡量使用JSON,避免使用XML。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM