---
title: Flask - Email, Large app structure
tags: python, flask, Web Form, Email, large app structure
---
> [TOC]
>
> Reference Website:
> 1. [smtp交易指令教學,可以用來測試smtp郵件伺服器是否正常](http://www.dreamjtech.com/content/smtp%E4%BA%A4%E6%98%93%E6%8C%87%E4%BB%A4%E6%95%99%E5%AD%B8%E5%8F%AF%E4%BB%A5%E7%94%A8%E4%BE%86%E6%B8%AC%E8%A9%A6smtp%E9%83%B5%E4%BB%B6%E4%BC%BA%E6%9C%8D%E5%99%A8%E6%98%AF%E5%90%A6%E6%AD%A3%E5%B8%B8)
> 2. [SMTP 實例](https://ithelp.ithome.com.tw/articles/10189886)
> 3. [SMTP 協定](http://www.tsnien.idv.tw/Internet_WebBook/chap14/14-4%20SMTP%20%E5%8D%94%E5%AE%9A.html)
> 4. [Structuring a Flask Project - Blueprints](https://www.patricksoftwareblog.com/structuring-a-flask-project/)
> 5. [A Better Application Structure](https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xv-a-better-application-structure)
---
# ***Flask - Email***
* 學習目標:使用Flask app傳送email。
* 套件的選擇:(二擇一)
1. [Python標準函式庫的`smtplib套件`](https://docs.python.org/3/library/smtplib.html)
2. `Flask-Mail`擴充套件 (已包含smtplib,且與Flask做很好的整合)
:::info
* ==SMTP 簡易郵件傳輸通訊協定== (Simple Mail Transfer Protocol)
* 用於從"來源地址"到"目的地址"傳輸郵件的規範,通過它來控制郵件的中轉方式。
* 幫助每台電腦在發送或中轉信件時找到下一個目的地。
* SMTP認證的目的:
* 避免用戶受到垃圾郵件的侵擾。
* ==SMTP伺服器==,就是遵循SMTP協定的發送郵件伺服器。
:::
## **用Flask-Mail來支援email**
1. 使用`pip`安裝`Flask-Mail`擴充套件
```
(venv) $ pip install flask-mail
```
* 此擴充套件會連接簡易郵件傳輸通訊協定(SMTP)伺服器,並把欲寄出的郵件送給它。
* Flask-Mail SMTP伺服器組態鍵:
| 組態鍵 | 預設值 | 說明 |
| -------- | -------- | -------- |
| MAIL_SERVER | localhost | email伺服器的主機名稱或IP位址 |
| MAIL_PORT | 25 | email伺服器的連接埠 |
| MAIL_USE_TLS | False | 啟用傳輸層安全性(TLS) |
| MAIL_USE_SSL | False | 啟用安全通訊端層(SSL) |
| MAIL_USERNAME | None | 郵件帳號的使用者名稱 |
| MAIL_PASSWORD | None | 郵件帳號的密碼 |
* 預設:無身分驗證的情況下,送出郵件。
2. 連接外部的SMTP(使用Gmail帳號傳送email)
* 為了保護帳號資訊,故不把帳號資訊直接寫在腳本裡,而是從環境變數匯入。
```python=
### db_demo.py:設置Flask-Mail來使用Gmail ###
import os
# ...
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
```
3. 設定Gmail以接受SMTP身份驗證
* 基於安全理由,Gmail帳號會要求外部app使用OAuth2驗證,來連接email伺服器。然而,Python的smtp程式庫並不支援。
* 若要讓Gmail帳號接受標準的SMTP身份驗證,設定如下:
* [Google帳號設定網頁](https://myaccount.google.com/) -> 安全性 -> 開啟!

4. 初始化Flask-Mail
```python=
### db_demo.py ###
from flask_mail import Mail
mail = Mail(app)
```
5. Windows設定環境變數
* 匯入email伺服器`使用者名稱與密碼`的環境變數
```
(venv) $ set MAIL_USERNAME=pcsh110576@gmail.com
(venv) $ set MAIL_PASSWORD=**************
```
* 若是Linux或macOS:
```
(venv) $ export MAIL_USERNAME=pcsh110576@gmail.com
(venv) $ export MAIL_PASSWORD=**************
```
### ***從Python Shell寄送email***
* 目的:透過Python Shell測試設置的結果。
* 做法:必須每次手動建立email訊息。
* 郵件內容的呈現方式:(二擇一)
* `msg.body` 純文字
* `msg.html` HTML
```python=
(venv) $ set FLASK_APP=db_demo.py
(venv) $ flask shell
>>> from flask_mail import Message
>>> from db_demo import mail
>>> msg = Message('test_email 0.0', sender='pcsh110576@mail.fju.edu.tw', recipients=['404040523@mail.fju.edu.tw'])
>>> msg.body = 'This is the plain text body'
>>> msg.html = 'This is the <b>flask test</b> mail. ==='
>>> with app.app_context():
... mail.send(msg)
...
>>>
```
:::success
### 資料庫 (上次增加的部分)
```
pip install flask-sqlalchemy
pip install flask-migrate
```
```python=
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://flaskdb_chia:gibe258deny700@localhost:3306/flaskdb'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
migrate = Migrate(app, db)
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key = True)
name = db.Column(db.String(64), unique = True)
users = db.relationship('User', backref='role', lazy='dynamic')
def __repr__(self):
return '<Role %r>' % self.name
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key = True)
username = db.Column(db.String(64), unique = True, index = True)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
def __repr__(self):
return '<User %r>' % self.username
@app.route('/name_form', methods=['GET', 'POST'])
def name_form():
form = NameForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first()
if user is None:
user = User(username=form.name.data)
db.session.add(user)
db.session.commit()
session['known'] = False
else:
session['known'] = True
session['name'] = form.name.data
form.name.data = ''
return redirect(url_for('name_form'))
return render_template('webform.html', form=form, name=session.get('name'), known=session.get('known', False))
```
:::
### ***整合email與app***
* 目的:避免每次手動建立email訊息,故將email寄送程式寫成一個函式。
* 優點:彈性高,且可將Jinja2模板轉譯成email內文。
```python=
### db_demo.py:支援email ###
from flask_mail import Message
#主旨的開頭文字
app.config['MAIL_SUBJECT_PREFIX'] = '[Flasky]'
#寄件者的地址,同欲驗證的gmail帳號
app.config['MAIL_SENDER'] = 'Flasky Admin <pcsh110576@gmail.com>'
#send_email(收件人地址, 主旨, email內文模板, 關鍵字引數)
def send_email(to, subject, template, **kwargs):
msg = Message(app.config['MAIL_SUBJECT_PREFIX'] + subject, sender = app.config['MAIL_SENDER'], recipients=[to])
#msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
mail.send(msg)
```
* 建立email內文模板
1. 純文字模板`templates/mail/new_user.txt`
```htmlmixed=
User {{ user.username }} has joined. (txt)
```
2. HTML模板`templates/mail/new_user.html`
```htmlmixed=
User <b>{{ user.username }}</b> has joined. (html)
```
> `關鍵字引數`會被送給render_template(),讓email內文模板將他們當成模板變數使用。(亦即`{{ user.username }}`)
* 擴充`db_demo.py`name_form()的view函式
* 當收到表單送來的新名字時,寄一封email給管理者。
```python=
### db_demo.py ###
# ...
#收件者
app.config['MAIL_ADMIN'] = os.environ.get('FLASKY_ADMIN')
# ...
@app.route('/name_form', methods=['GET', 'POST'])
def name_form():
form = NameForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first()
if user is None:
user = User(username=form.name.data)
db.session.add(user)
db.session.commit() #表單的輸入會寫進database
session['known'] = False
### 新增
if app.config['MAIL_ADMIN']:
send_email(app.config['MAIL_ADMIN'], 'New User', 'mail/new_user', user=user)
else:
session['known'] = True
session['name'] = form.name.data
form.name.data = ''
return redirect(url_for('name_form'))
return render_template('webform.html', form=form, name=session.get('name'), known=session.get('known', False))
```
* Windows設定環境變數
```
(venv) $ set MAIL_USERNAME=pcsh110576@gmail.com
(venv) $ set MAIL_PASSWORD=**************
(venv) $ set FLASKY_ADMIN=bessy110576@gmail.com
```
* 若是Linux或macOS:
```
(venv) $ export MAIL_USERNAME=pcsh110576@gmail.com
(venv) $ export MAIL_PASSWORD=**************
(venv) $ export FLASKY_ADMIN=bessy110576@gmail.com
```

* 確認資料有無寫入資料庫
```
> mysql -u flaskdb_chia -p
MariaDB [(none)]> show databases;
MariaDB [(none)]> use flaskdb;
MariaDB [flaskdb]> SELECT * FROM users;
```
### ***寄送非同步email***
* 目的:避免處理請求時產生延遲,故將email寄送函式移往背景執行緒。
> [Flask的Context機制](https://blog.tonyseek.com/post/the-context-mechanism-of-flask/)
> [flask學習筆記 -- 上下文環境與執行緒隔離](https://www.itread01.com/content/1548687065.html)
* 切記:若app需要寄送大量email,應該使用專門的工作來寄送,而非寄出每一封信時就啟動一個新的執行緒。
* 可將send_async_email()函式的執行工作送到 [Celery](http://www.celeryproject.org/) 工作佇列。
```python=
### db_demo.py:支援非同步email ###
#當你要同時做很多事情時,就可以用到threading達成多執行緒。
from threading import Thread
def send_email(to, subject, template, **kwargs):
msg = Message(app.config['MAIL_SUBJECT_PREFIX'] + subject, sender = app.config['MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
#msg.html = render_template(template + '.html', **kwargs)
thr = Thread(target=send_async_email, args=[app, msg])
thr.start()
return thr
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
```
---
# ***Flask - Large App Structure***
* 原因:`db_demo.py`腳本變大,不便使用。
* 學習目標:架構較大型的App,以增加擴充性。
* Flask VS. 多數其他web框架
* Flask不會限制大型專案的組織架構。(完全由開發者主導)
:::warning
## App基本結構
```
(venv) $ tree /f > tree.txt
C:\Users\pcsh1\flask_proj
│ config.py --- 組態設定
│ db_demo.py --- 定義Flask App實例 & 協助管理App的工作
│ requirements.txt --- 套件依賴項目,以便重建一致的虛擬環境
│
├─app --- (app套件) Flask App
├─migrations --- 資料庫遷移腳本
├─tests --- (tests套件) 單元測試
└─venv --- Python虛擬環境
```
:::
## 1. 組態設定選項`config.py`
* app通常需要多組的組態設定。
> email伺服器的組態:從環境變數匯入,預設指向Gmail伺服器。
> 通常在開發期間,使用預設值。但是,在產品伺服器上,應設置對應的環境變數。
* 分離`Development(開發)`、`Testing(測試)`與`Production(產品)`期間所使用的不同資料庫,以免相互干擾。
* 簡單的字典式組態:`app.config[''] = ''` -> 改為【組態類別階層】
> [flask web 開發中 config 文件中 init_app 函數的作用](https://blog.csdn.net/a447685024/article/details/52254134)
```python=
import os
basedir = os.path.abspath(os.path.dirname(__file__))
#Config基礎類別:所有組態共同的設定
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string' #可以從環境變數匯入,或是使用預設值
MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.googlemail.com')
MAIL_PORT = int(os.environ.get('MAIL_PORT', '587'))
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
MAIL_SUBJECT_PREFIX = '[Flasky]'
MAIL_SENDER = 'Flasky Admin <pcsh110576@gmail.com>'
MAIL_ADMIN = os.environ.get('FLASKY_ADMIN')
SQLALCHEMY_TRACK_MODIFICATIONS = False
#實作空的init_app()方法
@staticmethod
def init_app(app):
pass
#子類別:分別定義特定組態專屬的設定,讓app在各個組態設置中使用不同的資料庫
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
'mysql://flaskdb_chia:gibe258deny700@localhost:3306/flaskdb'
#每一個組態都會試著從環境變數匯入資料庫URL。若無法匯入,則使用預設的資料庫。
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
'sqlite://'
#預設為【記憶體內部資料庫】,測試完成後將不保留資料。
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
#將各種組態註冊到config字典
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig #將Development組態,註冊為預設值
}
```
## 2. App套件
:::warning
* App套件內存放:
* app 的 所有程式碼
* `templates` 模板
* `static` 靜態檔案
* `models.py` 資料庫模型
* `email.py` email支援函式
```
C:\Users\pcsh1\flask_proj
├─app
│ email.py
│ models.py
│ __init__.py
│
├─main
│ │ errors.py
│ │ forms.py
│ │ views.py
│ │ __init__.py
│
├─static
├─templates
│ 404.html
│ 500.html
│ base.html
│ base_index.html
│ webform.html
│
└─mail
│ new_user.html
│ new_user.txt
```
:::
### I. ==使用App工廠==:`app/__init__.py`
* 用單一檔案來建立App,等於是在全域範圍內建立App,固然方便,但無法動態套用組態的改變。
* 當腳本開始執行時,App實例就被建立,此時進行組態的改變已經太晚了。這一點對於「單元測試」特別重要!
* `App套件建構式`的寫法:
* 目的:
* 讓腳本有時間設定組態 (動態套用組態的改變)
* 亦可建立多個App實例
* 作法:
* 透過延遲App的建立,將它移至工廠函式內,再利用腳本呼叫它。
```python=
### app/__init__.py:App套件建構式 ###
##匯入多數目前正在使用的Flask擴充套件
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
#匯入config.py
from config import config
#尚未初始化
bootstrap = Bootstrap()
db = SQLAlchemy()
mail = Mail()
moment = Moment()
#create_app()為"App工廠函式",回傳建立好的App實例。
def create_app(config_name): #config_name接收組態名稱,以讓App使用
#建立App實例
app = Flask(__name__)
#將config.py內定義的組態類別,其所儲存的組態設定直接匯入App
app.config.from_object(config[config_name])
#App初始化,使用init_app()
config[config_name].init_app(app)
bootstrap.init_app(app)
db.init_app(app)
mail.init_app(app)
moment.init_app(app)
### 註冊主藍圖 ###
# 在這裡指派"路由"與"錯誤頁面處理函式" #
# 直到藍圖被app註冊時,才成為app的一部分。
return app
```
### II. ==在藍圖中實作App功能==
* 單一腳本app:
* App實例位於全域範圍內,故能用`app.route裝飾器`來定義路由。
* 但是,現在App是在執行期建立的,`app.route裝飾器`在create_app()執行後才存在,此時為時已晚。
* 自訂錯誤頁面處理函式,由`app.errorhandler裝飾器`所定義,亦面臨相同問題。
* `藍圖(blueprint)`的寫法:
* 與App的相似之處:可以定義路由和錯誤處理函式。
* 與App的差異之處:當在藍圖裡定義時,處於休眠狀態。直到藍圖被app註冊時,才成為app的一部分。
* 方式:(二擇一)
1. 使用單一檔案來定義所有的藍圖。
2. 使用結構化的方式。(高彈性)
* 在一個app套件內,用多個模組建立他們。
* 做法:
* 藉由實例化Blueprint類別來建立的。
#### 1. ==**建立主藍圖**== `app/main/__init__.py`
> [Flask Blueprint 藍圖](https://blog.csdn.net/jmilk/article/details/53342517)
> 目標:在app套件內,建立子套件來乘載app的第一個藍圖。
> 特別留意:
> * 最後才將`app/main/__init__.py`腳本匯入模組,以避免循環的依賴關係造成錯誤。
> * main必須先被定義,`views` `errors`才能匯入main藍圖物件,否則將會匯入失敗。
```python=
from flask import Blueprint
#實例化Blueprint類別。
#此類別的建構式必須接收兩個引數:藍圖名稱與藍圖所在的模組或套件。
main = Blueprint('main', __name__)
#匯入app套件的模組,以建立與藍圖的關係 (包含app路由&錯誤頁面處理函式)
# . 代表目前的套件; .. 代表目前套件的父代
from . import views, errors #相對匯入
```
#### 2. ==**註冊主藍圖**== `app/__init__.py`
> 藍圖是使用`create_app()工廠函式`內的app來註冊。
```python=
# ...
### 註冊主藍圖 ###
#連接到app/main/__init.py
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
```
#### 3. ==**主藍圖裡的錯誤處理函式**== `app/main/errors.py`
> * 當使用`errorhandler裝飾器`時,處理函式只會在"錯誤是在藍圖定義的路由中發生"時執行。
> * 若要設定遍及app範圍的錯誤處理函式,則須改用`app_errorhandler裝飾器`。
```python=
from flask import render_template
from . import main
# 全域(app_errorhandler裝飾器)的錯誤處理函式 #
@main.app_errorhandler(404) #路由裝飾器來自藍圖(main.route)
def page_not_found(e):
return render_template('404.html'), 404
@main.app_errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
```
#### 4. ==**主藍圖裡的app路由**== `app/main/views.py`
> 在藍圖裡編寫view函式,差異有二:
> 1. 路由裝飾器來自藍圖,故使用`main.route`,而非`app.route`
> 2. `url_for()`函式的用法:
> * 第一個引數(路由的端點名稱),從預設的view函式名稱`index`,改為套用`藍圖名稱(main)`的`main.index`。
> * Flask會將`命名空間(藍圖的名稱)`套用到在藍圖中定義的所有端點。
> 各個藍圖可使用相同的端點名稱來定義view函式,且不會造成衝突。
```python=
from flask import render_template, session, redirect, url_for
from datetime import datetime
from flask import flash
from . import main
from .forms import NameForm #表單物件
from .. import db
from ..models import User
from ..email import send_email
from flask import current_app
#路由裝飾器來自藍圖(main.route)
@main.route('/name_form', methods=['GET', 'POST'])
def name_form():
form = NameForm()
if form.validate_on_submit():
# 用表單收到的name在資料庫中查詢
user = User.query.filter_by(username=form.name.data).first()
# 查無此姓名的話,將該姓名寫入資料庫
if user is None:
user = User(username=form.name.data)
db.session.add(user)
db.session.commit() #把表單所輸入的,寫進資料庫
session['known'] = False
if current_app.config['MAIL_ADMIN']:
send_email(current_app.config['MAIL_ADMIN'], 'New User', 'mail/new_user', user=user) ###
else:
session['known'] = True
session['name'] = form.name.data
form.name.data = ''
old_name = session.get('name')
if old_name is not None and old_name != form.name.data:
flash('Looks like you have changed your name!')
# 將命名空間套用到在藍圖中所定義的所有端點
return redirect(url_for('main.name_form')) #等同於 '.name_form'
return render_template('webform.html', form=form, name=session.get('name'), current_time=datetime.utcnow(), known=session.get('known', False))
```
#### 5. ==**表單物件**== `app/main/forms.py`
```python=
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class NameForm(FlaskForm):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
```
### `app/email.py`
```python=
from threading import Thread
from flask import render_template, current_app ###
from flask_mail import Message
from . import mail
def send_email(to, subject, template, **kwargs):
app = current_app._get_current_object() ###
msg = Message(app.config['MAIL_SUBJECT_PREFIX'] + subject, sender = app.config['MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
thr = Thread(target=send_async_email, args=[app, msg])
thr.start()
return thr
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
```
### `app/models.py`
```python=
from . import db
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key = True)
name = db.Column(db.String(64), unique = True)
users = db.relationship('User', backref='role', lazy='dynamic')
def __repr__(self):
return '<Role %r>' % self.name
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key = True)
username = db.Column(db.String(64), unique = True, index = True)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
def __repr__(self):
return '<User %r>' % self.username
```
## 3. App主腳本 `db_demo.py`
* 定義App實例
```python=
# 主腳本 #
import os
from app import create_app, db
from app.models import User, Role
from flask_migrate import Migrate
#建立一個App,從環境變數或預設來取得組態設置。
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
##初始化Flask-Migrate
migrate = Migrate(app, db)
##Python殼層的自訂context
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
### 單元測試 ###
```
* Windows設定環境變數 (讓flask命令找到App實例)
```
(venv) $ set FLASK_APP=db_demo.py
(venv) $ set FLASK_DEBUG=1
```
* 若是Linux或macOS:
```
(venv) $ export FLASK_APP=db_demo.py
(venv) $ export FLASK_DEBUG=1
```
## 4. requirements檔
> 紀錄所有的套件依賴項目,以及確切的版本號。
* 透過Windows cmd,自動產生requirements.txt
```
(venv) $ pip freeze > requirements.txt
```
* 複製完全一樣的虛擬環境
```
(venv) $ deactivate
$ cd ..
$ mkdir test
$ cd test
$ virtualenv venv_test
$ venv_test\Scripts\activate
(venv_test) $ pip install C:\Users\pcsh1\OneDrive\桌面\mysqlclient-1.4.2-cp37-cp37m-win32.whl
(venv_test) $ pip install -r requirements.txt
(venv_test) $ pip freeze
```
## 5. 單元測試
:::warning
* 建立`tests`資料夾和`tests/__init__.py`
```
C:\Users\pcsh1\flask_beginner_1.2.3.4
├─tests
│ test_basics.py
│ __init__.py --- 讓測試目錄成為有效的套件 (空檔案)
```
:::
### I. ==單元測試== `tests/test_basics.py`
* 使用Python標準程式庫的[unittest套件](https://docs.python.org/3/library/unittest.html)
* `__init__.py` 可以為空檔案,因為`unittest套件`會掃描所有的模組來尋找測試程式。
* 目標:
* 定義兩個簡單的測試(`test_app_exists`、`test_app_is_testing`)
```python=
import unittest
from flask import current_app
from app import create_app, db
class BasicsTestCase(unittest.TestCase):
#在每次測試之前執行,幫測試程式建立一個類似運行中App的環境
def setUp(self):
#建立一個testing組態的App,並啟動context
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all() #為測試程式建立全新的資料庫
#在每次測試之後執行
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
# 開頭為"test_"的方法,是我們要執行的測試 #
#確認App實例的存在
def test_app_exists(self):
self.assertFalse(current_app is None)
#確認App在測試組態下運行
def test_app_is_testing(self):
self.assertTrue(current_app.config['TESTING'])
```
### II. 單元測試啟動命令 `db_demo.py`
> [python腳本中verbosity是什麼意思?](https://zhidao.baidu.com/question/306632744408746084.html)
```python=
# ...
#自訂命令
@app.cli.command()
def test1():
"""Run the unit tests."""
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
```
* 透過Windows cmd,執行test1測試
```
(venv) $ flask test1
```
## 6. 設定資料庫
> [搞懂Flask-Migrate](https://ithelp.ithome.com.tw/articles/10205751?sc=iThelpR)
> [Flask-Migrate](https://blog.burn-i.com/20180418/flask-migrate/)
* App會先從環境變數取得資料庫URL,若找不到則使用預設的資料庫。
* 透過Windows cmd,當Model的結構有異動時自動更新資料庫。
```
(venv) $ flask db upgrade
```
## 7. 執行App
* 透過Windows cmd,啟動App腳本 & 匯入環境變數
```
(venv) $ set FLASK_APP=db_demo.py
(venv) $ set FLASK_DEBUG=1
(venv) $ set MAIL_USERNAME=pcsh110576@gmail.com
(venv) $ set MAIL_PASSWORD=**************
(venv) $ set FLASKY_ADMIN=bessy110576@gmail.com
(venv) $ flask run
```