Flask測試和部署

一 藍圖Blueprinthtml

爲何學習藍圖?前端

咱們學習Flask框架,是從寫單個文件,執行hello world開始的。咱們在這單個文件中能夠定義路由、視圖函數、定義模型等等。但這顯然存在一個問題:隨着業務代碼的增長,將全部代碼都放在單個程序文件中,是很是不合適的。這不只會讓代碼閱讀變得困難,並且會給後期維護帶來麻煩。python

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

 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中的模塊製做和導入就是基於實現功能模塊的封裝的需求。nginx

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

# 文件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'。由於模塊間產生了死鎖。sql

 

什麼是藍圖?數據庫

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

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

簡單來講,藍圖就是一個存儲操做路由映射方法的容器,主要用來實現客戶端請求和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。

相關文章
相關標籤/搜索