# 編寫你的第一個Django應用,第5部分 #
本教程上接教程第4部分。 我們已經建立一個網頁投票應用,現在我們將為它創建一些自動化測試。
## 自動化測試簡介 ##
### 什么是自動化測試? ###
測試是檢查你的代碼是否正常運行的簡單程序。
測試可以劃分為不同的級別。 一些測試可能專注于小細節(某一個模型的方法是否會返回預期的值?), 其他的測試可能會檢查軟件的整體運行是否正常(用戶在對網站進行了一系列的操作后,是否返回了正確的結果?)。這些其實和你早前在教程 1中做的差不多, 使用shell來檢測一個方法的行為,或者運行程序并輸入數據來檢查它的行為方式。
自動化測試的不同之處就在于這些測試會由系統來幫你完成。你創建了一組測試程序,當你修改了你的應用,你就可以用這組測試程序來檢查你的代碼是否仍然同預期的那樣運行,而無需執行耗時的手動測試。
### 為什么你需要創建測試 ###
那么,為什么要創建測試?而且為什么是現在?
你可能感覺學習Python/Django已經足夠,再去學習其他的東西也許需要付出巨大的努力而且沒有必要。 畢竟,我們的投票應用已經活蹦亂跳了; 將時間運用在自動化測試上還不如運用在改進我們的應用上。 如果你學習Django就是為了創建一個投票應用,那么創建自動化測試顯然沒有必要。 但如果不是這樣,現在是一個很好的學習機會。
#### 測試將節省你的時間 ####
在某種程度上, ‘檢查起來似乎正常工作’將是一種令人滿意的測試。 在更復雜的應用中,你可能有幾十種組件之間的復雜的相互作用。
這些組件的任何一個小的變化,都可能對應用的行為產生意想不到的影響。 檢查起來‘似乎正常工作’可能意味著你需要運用二十種不同的測試數據來測試你代碼的功能,僅僅是為了確保你沒有搞砸某些事 —— 這不是對時間的有效利用。
尤其是當自動化測試只需要數秒就可以完成以上的任務時。 如果出現了錯誤,測試程序還能夠幫助找出引發這個異常行為的代碼。
有時候你可能會覺得編寫測試程序將你從有價值的、創造性的編程工作里帶出,帶到了單調乏味、無趣的編寫測試中,尤其是當你的代碼工作正常時。
然而,比起用幾個小時的時間來手動測試你的程序,或者試圖找出代碼中一個新引入的問題的原因,編寫測試程序還是令人愜意的。
#### 測試不僅僅可以發現問題,它們還能防止問題 ####
將測試看做只是開發過程中消極的一面是錯誤的。
沒有測試,應用的目的和意圖將會變得相當模糊。 甚至在你查看自己的代碼時,也不會發現這些代碼真正干了些什么。
測試改變了這一切; 它們使你的代碼內部變得明晰,當錯誤出現后,它們會明確地指出哪部分代碼出了問題 —— 甚至你自己都不會料到問題會出現在那里。
#### 測試使你的代碼更受歡迎 ####
你可能已經創建了一個堪稱輝煌的軟件,但是你會發現許多其他的開發者會由于它缺少測試程序而拒絕查看它一眼;沒有測試程序,他們不會信任它。 Jacob Kaplan-Moss,Django最初的幾個開發者之一,說過“不具有測試程序的代碼是設計上的錯誤。”
你需要開始編寫測試的另一個原因就是其他的開發者在他們認真研讀你的代碼前可能想要查看一下它有沒有測試。
#### 測試有助于團隊合作 ####
之前的觀點是從單個開發人員來維護一個程序這個方向來闡述的。 復雜的應用將會被一個團隊來維護。 測試能夠減少同事在無意間破壞你的代碼的機會(和你在不知情的情況下破壞別人的代碼的機會)。 如果你想在團隊中做一個好的Django開發者,你必須擅長測試!
## 基本的測試策略 ##
編寫測試有很多種方法。
一些開發者遵循一種叫做“由測試驅動的開發”的規則;他們在編寫代碼前會先編好測試。 這似乎與直覺不符,盡管這種方法與大多數人經常的做法很相似:人們先描述一個問題,然后創建一些代碼來解決這個問題。 由測試驅動的開發可以用Python測試用例將這個問題簡單地形式化。
更常見的情況是,剛接觸測試的人會先編寫一些代碼,然后才決定為這些代碼創建一些測試。 也許在之前就編寫一些測試會好一點,但什么時候開始都不算晚。
有時候很難解決從什么地方開始編寫測試。 如果你已經編寫了數千行Python代碼,挑選它們中的一些來進行測試不會是太容易的。 這種情況下,在下次你對代碼進行變更,或者添加一個新功能或者修復一個bug時,編寫你的第一個測試,效果會非常好。
現在,讓我們馬上來編寫一個測試。
## 編寫我們的第一個測試 ##
### 我們找出一個錯誤 ###
幸運的是,polls應用中有一個小錯誤讓我們可以馬上來修復它:如果Question在最后一個天發布,Question.was_published_recently() 方法返回True(這是對的),但是如果Question的pub_date 字段是在未來,它還返回True(這肯定是不對的)。
你可以在管理站點中看到這一點; 創建一個發布時間在未來的一個Question; 你可以看到Question 的變更列表聲稱它是最近發布的。
你還可以使用shell看到這點:
```
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> # create a Question instance with pub_date 30 days in the future
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> # was it published recently
>>> future_question.was_published_recently()
True
```
由于將來的事情并不能稱之為‘最近’,這確實是一個錯誤。
### 創建一個測試來暴露這個錯誤 ###
我們需要在自動化測試里做的和剛才在shell里做的差不多,讓我們來將它轉換成一個自動化測試。
應用的測試用例安裝慣例一般放在該應用的tests.py文件中;測試系統將自動在任何以test開頭的文件中查找測試用例。
將下面的代碼放入polls應用下的tests.py文件中:
```
polls/tests.py
import datetime
from django.utils import timezone
from django.test import TestCase
from .models import Question
class QuestionMethodTests(TestCase):
def test_was_published_recently_with_future_question(self):
"""
was_published_recently() should return False for questions whose
pub_date is in the future.
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertEqual(future_question.was_published_recently(), False)
```
我們在這里做的是創建一個django.test.TestCase子類,它具有一個方法可以創建一個pub_date在未來的Question實例。然后我們檢查was_published_recently()的輸出 —— 它應該是 False.
### 運行測試 ###
在終端中,我們可以運行我們的測試:
```
$ python manage.py test polls
```
你將看到類似下面的輸出:
```
Creating test database for alias 'default'...
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionMethodTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
self.assertEqual(future_question.was_published_recently(), False)
AssertionError: True != False
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...
```
發生了如下這些事:
+ python manage.py test polls查找polls 應用下的測試用例
+ 它找到 django.test.TestCase 類的一個子類
+ 它為測試創建了一個特定的數據庫
+ 它查找用于測試的方法 —— 名字以test開始
+ 它運行test_was_published_recently_with_future_question創建一個pub_date為未來30天的 Question實例
+ ... 然后利用assertEqual()方法,它發現was_published_recently() 返回True,盡管我們希望它返回False
這個測試通知我們哪個測試失敗,甚至是錯誤出現在哪一行。
### 修復這個錯誤 ###
我們已經知道問題是什么:Question.was_published_recently() 應該返回 False,如果它的pub_date是在未來。在models.py中修復這個方法,讓它只有當日期是在過去時才返回True :
```
polls/models.py
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
```
再次運行測試:
```
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
```
在找出一個錯誤之后,我們編寫一個測試來暴露這個錯誤,然后在代碼中更正這個錯誤讓我們的測試通過。
未來,我們的應用可能會出許多其它的錯誤,但是我們可以保證我們不會無意中再次引入這個錯誤,因為簡單地運行一下這個測試就會立即提醒我們。 我們可以認為這個應用的這一小部分會永遠安全了。
### 更加綜合的測試 ###
在這里,我們可以使was_published_recently() 方法更加穩定;事實上,在修復一個錯誤的時候引入一個新的錯誤將是一件很令人尷尬的事。
在同一個類中添加兩個其它的測試方法,來更加綜合地測試這個方法:
```
polls/tests.py
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() should return False for questions whose
pub_date is older than 1 day.
"""
time = timezone.now() - datetime.timedelta(days=30)
old_question = Question(pub_date=time)
self.assertEqual(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
"""
was_published_recently() should return True for questions whose
pub_date is within the last day.
"""
time = timezone.now() - datetime.timedelta(hours=1)
recent_question = Question(pub_date=time)
self.assertEqual(recent_question.was_published_recently(), True)
```
現在我們有三個測試來保證無論發布時間是在過去、現在還是未來 Question.was_published_recently()都將返回合理的數據。
再說一次,polls 應用雖然簡單,但是無論它今后會變得多么復雜以及會和多少其它的應用產生相互作用,我們都能保證我們剛剛為它編寫過測試的那個方法會按照預期的那樣工作。
## 測試一個視圖 ##
這個投票應用沒有區分能力:它將會發布任何一個Question,包括 pub_date字段位于未來。我們應該改進這一點。 設定pub_date在未來應該表示Question在此刻發布,但是直到那個時間點才會變得可見。
### 視圖的一個測試 ###
當我們修復上面的錯誤時,我們先寫測試,然后修改代碼來修復它。 事實上,這是由測試驅動的開發的一個簡單的例子,但做的順序并不真的重要。
在我們的第一個測試中,我們專注于代碼內部的行為。 在這個測試中,我們想要通過瀏覽器從用戶的角度來檢查它的行為。
在我們試著修復任何事情之前,讓我們先查看一下我們能用到的工具。
### Django測試客戶端 ###
Django提供了一個測試客戶端來模擬用戶和代碼的交互。我們可以在tests.py 甚至在shell 中使用它。
我們將再次以shell開始,但是我們需要做很多在tests.py中不必做的事。首先是在 shell中設置測試環境:
```
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
```
setup_test_environment()安裝一個模板渲染器,可以使我們來檢查響應的一些額外屬性比如response.context,否則是訪問不到的。請注意,這種方法不會建立一個測試數據庫,所以以下命令將運行在現有的數據庫上,輸出的內容也會根據你已經創建的Question不同而稍有不同。
下一步我們需要導入測試客戶端類(在之后的tests.py 中,我們將使用django.test.TestCase類,它具有自己的客戶端,將不需要導入這個類):
```
>>> from django.test import Client
>>> # create an instance of the client for our use
>>> client = Client()
```
這些都做完之后,我們可以讓這個客戶端來為我們做一些事:
```
>>> # get a response from '/'
>>> response = client.get('/')
>>> # we should expect a 404 from that address
>>> response.status_code
404
>>> # on the other hand we should expect to find something at '/polls/'
>>> # we'll use 'reverse()' rather than a hardcoded URL
>>> from django.core.urlresolvers import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
200
>>> response.content
'\n\n\n <p>No polls are available.</p>\n\n'
>>> # note - you might get unexpected results if your ``TIME_ZONE``
>>> # in ``settings.py`` is not correct. If you need to change it,
>>> # you will also need to restart your shell session
>>> from polls.models import Question
>>> from django.utils import timezone
>>> # create a Question and save it
>>> q = Question(question_text="Who is your favorite Beatle?", pub_date=timezone.now())
>>> q.save()
>>> # check the response once again
>>> response = client.get('/polls/')
>>> response.content
'\n\n\n <ul>\n \n <li><a href="/polls/1/">Who is your favorite Beatle?</a></li>\n \n </ul>\n\n'
>>> # If the following doesn't work, you probably omitted the call to
>>> # setup_test_environment() described above
>>> response.context['latest_question_list']
[<Question: Who is your favorite Beatle?>]
```
### 改進我們的視圖 ###
投票的列表顯示還沒有發布的投票(即pub_date在未來的投票)。讓我們來修復它。
在教程 4中,我們介紹了一個繼承ListView的基于類的視圖:
```
polls/views.py
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.order_by('-pub_date')[:5]
```
response.context_data['latest_question_list'] 取出由視圖放置在context 中的數據。
我們需要修改get_queryset方法并讓它將日期與timezone.now()進行比較。首先我們需要添加一行導入:
```
polls/views.py
from django.utils import timezone
```
然后我們必須像這樣修改get_queryset方法:
```
polls/views.py
def get_queryset(self):
"""
Return the last five published questions (not including those set to be
published in the future).
"""
return Question.objects.filter(
pub_date__lte=timezone.now()
).order_by('-pub_date')[:5]
```
Question.objects.filter(pub_date__lte=timezone.now()) 返回一個查詢集,包含pub_date小于等于timezone.now的Question。
### 測試我們的新視圖 ###
啟動服務器、在瀏覽器中載入站點、創建一些發布時間在過去和將來的Questions ,然后檢驗只有已經發布的Question會展示出來,現在你可以對自己感到滿意了。你不想每次修改可能與這相關的代碼時都重復這樣做 —— 所以讓我們基于以上shell會話中的內容,再編寫一個測試。
將下面的代碼添加到polls/tests.py:
```
polls/tests.py
from django.core.urlresolvers import reverse
```
我們將創建一個快捷函數來創建Question,同時我們要創建一個新的測試類:
```
polls/tests.py
def create_question(question_text, days):
"""
Creates a question with the given `question_text` published the given
number of `days` offset to now (negative for questions published
in the past, positive for questions that have yet to be published).
"""
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text,
pub_date=time)
class QuestionViewTests(TestCase):
def test_index_view_with_no_questions(self):
"""
If no questions exist, an appropriate message should be displayed.
"""
response = self.client.get(reverse('polls:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No polls are available.")
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_index_view_with_a_past_question(self):
"""
Questions with a pub_date in the past should be displayed on the
index page.
"""
create_question(question_text="Past question.", days=-30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
['<Question: Past question.>']
)
def test_index_view_with_a_future_question(self):
"""
Questions with a pub_date in the future should not be displayed on
the index page.
"""
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertContains(response, "No polls are available.",
status_code=200)
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_index_view_with_future_question_and_past_question(self):
"""
Even if both past and future questions exist, only past questions
should be displayed.
"""
create_question(question_text="Past question.", days=-30)
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
['<Question: Past question.>']
)
def test_index_view_with_two_past_questions(self):
"""
The questions index page may display multiple questions.
"""
create_question(question_text="Past question 1.", days=-30)
create_question(question_text="Past question 2.", days=-5)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
['<Question: Past question 2.>', '<Question: Past question 1.>']
)
```
讓我們更詳細地看下以上這些內容。
第一個是Question的快捷函數create_question,將重復創建Question的過程封裝在一起。
test_index_view_with_no_questions不創建任何Question,但會檢查消息“No polls are available.” 并驗證latest_question_list為空。注意django.test.TestCase類提供一些額外的斷言方法。在這些例子中,我們使用assertContains() 和 assertQuerysetEqual()。
在test_index_view_with_a_past_question中,我們創建一個Question并驗證它是否出現在列表中。
在test_index_view_with_a_future_question中,我們創建一個pub_date 在未來的Question。數據庫會為每一個測試方法進行重置,所以第一個Question已經不在那里,因此首頁面里不應該有任何Question。
等等。 事實上,我們是在用測試模擬站點上的管理員輸入和用戶體驗,檢查針對系統每一個狀態和狀態的新變化,發布的是預期的結果。
### 測試 DetailView ###
一切都運行得很好; 然而,即使未來發布的Question不會出現在index中,如果用戶知道或者猜出正確的URL依然可以訪問它們。所以我們需要給DetailView添加一個這樣的約束:
```
polls/views.py
class DetailView(generic.DetailView):
...
def get_queryset(self):
"""
Excludes any questions that aren't published yet.
"""
return Question.objects.filter(pub_date__lte=timezone.now())
```
當然,我們將增加一些測試來檢驗pub_date 在過去的Question 可以顯示出來,而pub_date在未來的不可以:
```
polls/tests.py
class QuestionIndexDetailTests(TestCase):
def test_detail_view_with_a_future_question(self):
"""
The detail view of a question with a pub_date in the future should
return a 404 not found.
"""
future_question = create_question(question_text='Future question.',
days=5)
response = self.client.get(reverse('polls:detail',
args=(future_question.id,)))
self.assertEqual(response.status_code, 404)
def test_detail_view_with_a_past_question(self):
"""
The detail view of a question with a pub_date in the past should
display the question's text.
"""
past_question = create_question(question_text='Past Question.',
days=-5)
response = self.client.get(reverse('polls:detail',
args=(past_question.id,)))
self.assertContains(response, past_question.question_text,
status_code=200)
```
### 更多的測試思路 ###
我們應該添加一個類似get_queryset的方法到ResultsView并為該視圖創建一個新的類。這將與我們剛剛創建的非常類似;實際上將會有許多重復。
我們還可以在其它方面改進我們的應用,并隨之不斷增加測試。例如,發布一個沒有Choices的Questions就顯得傻傻的。所以,我們的視圖應該檢查這點并排除這些 Questions。我們的測試應該創建一個不帶Choices 的 Question然后測試它不會發布出來, 同時創建一個類似的帶有 Choices的Question 并驗證它會 發布出來。
也許登陸的用戶應該被允許查看還沒發布的 Questions,但普通游客不行。 再說一次:無論添加什么代碼來完成這個要求,需要提供相應的測試代碼,無論你是否是先編寫測試然后讓這些代碼通過測試,還是先用代碼解決其中的邏輯然后編寫測試來證明它。
從某種程度上來說,你一定會查看你的測試,然后想知道是否你的測試程序過于臃腫,這將我們帶向下面的內容:
## 測試越多越好 ##
看起來我們的測試代碼的增長正在失去控制。 以這樣的速度,測試的代碼量將很快超過我們的應用,對比我們其它優美簡潔的代碼,重復毫無美感。
沒關系。讓它們繼續增長。最重要的是,你可以寫一個測試一次,然后忘了它。 當你繼續開發你的程序時,它將繼續執行有用的功能。
有時,測試需要更新。 假設我們修改我們的視圖使得只有具有Choices的 Questions 才會發布。在這種情況下,我們許多已經存在的測試都將失敗 —— 這會告訴我們哪些測試需要被修改來使得它們保持最新,所以從某種程度上講,測試可以自己照顧自己。
在最壞的情況下,在你的開發過程中,你會發現許多測試現在變得冗余。 即使這樣,也不是問題;對測試來說,冗余是一件好 事。
只要你的測試被合理地組織,它們就不會變得難以管理。 從經驗上來說,好的做法是:
+ 每個模型或視圖具有一個單獨的TestClass
+ 為你想測試的每一種情況建立一個單獨的測試方法
+ 測試方法的名字可以描述它們的功能
## 進一步的測試 ##
本教程只介紹了一些基本的測試。 還有很多你可以做,有許多非常有用的工具可以隨便使用來你實現一些非常聰明的做法。
例如,雖然我們的測試覆蓋了模型的內部邏輯和視圖發布信息的方式,你可以使用一個“瀏覽器”框架例如Selenium來測試你的HTML文件在瀏覽器中真實渲染的樣子。 這些工具不僅可以讓你檢查你的Django代碼的行為,還能夠檢查你的JavaScript的行為。 它會啟動一個瀏覽器,并開始與你的網站進行交互,就像有一個人在操縱一樣,非常值得一看! Django 包含一個LiveServerTestCase來幫助與Selenium 這樣的工具集成。
如果你有一個復雜的應用,你可能為了實現continuous integration,想在每次提交代碼后對代碼進行自動化測試,讓代碼自動 —— 至少是部分自動 —— 地來控制它的質量。
發現你應用中未經測試的代碼的一個好方法是檢查測試代碼的覆蓋率。 這也有助于識別脆弱的甚至死代碼。 如果你不能測試一段代碼,這通常意味著這些代碼需要被重構或者移除。 Coverage將幫助我們識別死代碼。 查看與coverage.py 集成來了解更多細節。
Django 中的測試有關于測試更加全面的信息。
## 下一步? ##
關于測試的完整細節,請查看Django 中的測試。
當你對Django 視圖的測試感到滿意后,請閱讀本教程的第6部分來 了解靜態文件的管理。
> 譯者:[Django 文檔協作翻譯小組](http://python.usyiyi.cn/django/index.html),原文:[Part 5: Testing](https://docs.djangoproject.com/en/1.8/intro/tutorial05/)。
>
> 本文以 [CC BY-NC-SA 3.0](http://creativecommons.org/licenses/by-nc-sa/3.0/cn/) 協議發布,轉載請保留作者署名和文章出處。
>
> [Django 文檔協作翻譯小組](http://python.usyiyi.cn/django/index.html)人手緊缺,有興趣的朋友可以加入我們,完全公益性質。交流群:467338606。
- 新手入門
- 從零開始
- 概覽
- 安裝
- 教程
- 第1部分:模型
- 第2部分:管理站點
- 第3部分:視圖和模板
- 第4部分:表單和通用視圖
- 第5部分:測試
- 第6部分:靜態文件
- 高級教程
- 如何編寫可重用的應用
- 為Django編寫首個補丁
- 模型層
- 模型
- 模型語法
- 元選項
- 模型類
- 查詢集
- 執行查詢
- 查找表達式
- 模型的實例
- 實例方法
- 訪問關聯對象
- 遷移
- 模式編輯器
- 編寫遷移
- 高級
- 管理器
- 原始的SQL查詢
- 聚合
- 多數據庫
- 自定義查找
- 條件表達式
- 數據庫函數
- 其它
- 遺留的數據庫
- 提供初始數據
- 優化數據庫訪問
- 視圖層
- 基礎
- URL配置
- 視圖函數
- 快捷函數
- 裝飾器
- 參考
- 內建的視圖
- TemplateResponse 對象
- 文件上傳
- 概覽
- File 對象
- 儲存API
- 管理文件
- 自定義存儲
- 基于類的視圖
- 概覽
- 內建顯示視圖
- 內建編輯視圖
- API參考
- 分類索引
- 高級
- 生成 CSV
- 生成 PDF
- 中間件
- 概覽
- 內建的中間件類
- 模板層
- 基礎
- 面向設計師
- 語言概覽
- 人性化
- 面向程序員
- 表單
- 基礎
- 概覽
- 表單API
- 內建的Widget
- 高級
- 整合媒體
- 開發過程
- 設置
- 概覽
- 應用程序
- 異常
- 概覽
- django-admin 和 manage.py
- 添加自定義的命令
- 測試
- 介紹
- 部署
- 概述
- WSGI服務器
- 部署靜態文件
- 通過email追蹤代碼錯誤
- Admin
- 管理操作
- 管理文檔生成器
- 安全
- 安全概述
- 說明Django中的安全問題
- 點擊劫持保護
- 加密簽名
- 國際化和本地化
- 概述
- 本地化WEB UI格式化輸入
- “本地特色”
- 常見的網站應用工具
- 認證
- 概覽
- 使用認證系統
- 密碼管理
- 日志
- 分頁
- 會話
- 數據驗證
- 其它核心功能
- 按需內容處理
- 重定向
- 信號
- 系統檢查框架