[TOC]
本節我們主要介紹如何自定義在第二部分提到過的admin后臺管理站點。
Django的admin站點是自動生成的、高度可定制的,它是Django相較其它Web框架獨有的內容,廣受歡迎。如果你覺得它不夠美觀,還有第三方美化版simpleUI。請一定不要忽略它,相信我,**它值得擁有**!
<br />
## **一、自定義后臺表單**
在前面的學習過程中,通過`admin.site.register(Question)`語句,我們在admin站點中注冊了Question模型。Django會自動生成一個該模型的默認表單頁面。如果你想自定義該頁面的外觀和工作方式,可以在注冊對象的時候告訴Django你的自定義選項。
下面是一個修改admin表單默認排序方式的例子。修改`polls/admin.py`的代碼::
~~~
from django.contrib import admin
from .models import Question
class QuestionAdmin(admin.ModelAdmin):
fields = ['pub_date', 'question_text']
admin.site.register(Question, QuestionAdmin)
~~~
你只需要創建一個繼承`admin.ModelAdmin`的模型管理類,在其中進行一些自定義操作,然后將它作為第二個參數傳遞給`admin.site.register()`,第一個參數則是Question模型本身。
上面的修改讓`Date Publication`字段顯示在`Question`字段前面了(默認是在后面)。如下圖所示:

對于只有2個字段的情況,效果看起來還不是很明顯,但是如果你有很多的字段,選擇一種直觀的符合我們人類習慣的排序方式則非常有用。
但是,當表單含有大量字段的時候,你更多的是想將表單劃分為一些字段的集合。
再次修改`polls/admin.py`:
~~~
from django.contrib import admin
from .models import Question
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {'fields': ['question_text']}),
('Date information', {'fields': ['pub_date']}),
]
admin.site.register(Question, QuestionAdmin)
~~~
字段集合`fieldsets`中每一個元組的第一個元素是該字段集合的標題。它讓我們的頁面看起來像下面的樣子:

<br />
## **二、添加關聯對象**
雖然我們已經有了Question的管理頁面,但是一個Question有多個Choices,如果想顯示Choices的內容怎么辦?有兩個辦法可以解決這個問題。第一個是像Question一樣將Choice注冊到admin站點,這很容易,修改`polls/admin.py`,增加下面的內容:
~~~
from django.contrib import admin
from .models import Choice, Question
# ...
admin.site.register(Choice)
~~~
重啟服務器,再次訪問admin頁面,就可以看到Choice條目了:

點擊它右邊的add按鈕,進入“Add Choice”表單頁面,看起來如下圖:

在這個表單中,Question字段是一個select選擇框,包含了當前數據庫中所有的Question實例。**Django在admin站點中,自動地將所有的外鍵關系展示為一個select框。** 在我們的例子中,目前只有一個question對象存在。
<br />
請注意圖中的綠色加號,它連接到Question模型。每一個包含外鍵關系的對象都會有這個綠色加號。點擊它,會彈出一個新增Question的表單,類似Question自己的添加表單。填入相關信息點擊保存后,Django自動將該Question保存在數據庫,并作為當前Choice的關聯外鍵對象。白話講就是,新建一個Question并作為當前Choice的外鍵。
<br />
但是實話說,這種創建方式的效率不怎么樣。如果在創建Question對象的時候就可以直接添加一些Choice,那會更好,這就是我們要說的第二種方法。下面,讓我們來動手試試。
<br />
首先,刪除`polls/admin.py`中Choice模型對`register()`方法的調用。然后,編輯Question的內容,最后整個文件的代碼應該如下:
~~~
from django.contrib import admin
from .models import Choice, Question
class ChoiceInline(admin.StackedInline):
model = Choice
extra = 3
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {'fields': ['question_text']}),
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
]
inlines = [ChoiceInline]
admin.site.register(Question, QuestionAdmin)
~~~
上面的代碼相當于告訴Django,Choice對象將在Question管理頁面進行編輯,默認情況,請提供3個Choice對象的編輯區域。
<br />
重啟服務器,進入“Add question”頁面,應該看到如下圖所示:

<br />
在3個插槽的最后,還有一個`Add another Choice`鏈接。點擊它,又可以獲得一個新的插槽。如果你想刪除插槽,點擊它最右邊的灰色X圖標即可。
<br />
注意,日期字段被疊藏起來了。
<br />
這里還有點小問題。上面頁面中插槽縱隊排列的方式需要占據大塊的頁面空間,查看起來很不方便。為此,Django提供了一種扁平化的顯示方式,你僅僅只需要修改一下`ChoiceInline`繼承的類為`admin.TabularInline`替代先前的`StackedInline`類(其實,從類名上你就能看出兩種父類的區別)。
~~~
# polls/admin.py
class ChoiceInline(admin.TabularInline):
#...
~~~
重啟服務器,刷新一下頁面,你會看到類似表格的顯示方式:

注意“DELETE”列,它可以刪除那些已有的Choice和新建的Choice。
<br />
## **三、定制實例的列表頁面**
Question的添加和修改頁面我們已經自定義得差不多了,下面讓我們來裝飾一下“實例列表”(change list)頁面,該頁面顯示了當前系統中所有的questions實例。
<br />
默認情況下,該頁面看起來是這樣的:

<br />
通常,Django只顯示`__str()__`方法指定的內容。但是很多時候,我們可能要同時顯示一些別的內容。要實現這一目的,可以使用`list_display`屬性,它是一個由字段組成的元組,其中的每一個字段都會按順序顯示在頁面上,代碼如下:
~~~
# polls/admin.py
class QuestionAdmin(admin.ModelAdmin):
# ...
list_display = ('question_text', 'pub_date', 'was_published_recently')
~~~
<br />
額外的,我們把`was_published_recently()`方法的結果也顯示出來。現在,頁面看起來會是下面的樣子:

你可以點擊每一列的標題,來根據這列的內容進行排序。但是`was_published_recently`這一列除外,不支持這種根據函數輸出結果進行排序的方式。同時請注意,`was_published_recently`這一列的列標題默認是方法的名字,內容則是輸出的字符串表示形式。
<br />
可以通過給方法提供一些屬性來改進輸出的樣式,如下面所示。注意**這次修改的是`polls/models.py`文件**,不要搞錯了!主要是增加了最后面三行內容:
~~~
# polls/models.py
class Question(models.Model):
# ...
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
was_published_recently.admin_order_field = 'pub_date'
was_published_recently.boolean = True
was_published_recently.short_description = 'Published recently?'
~~~
重啟服務器(這個我就不再啰嗦了,大家心里都有數)。刷新頁面,效果如下:

以上的定制功能還不是admin的全部,我們接著往下看!
* * *
我們還可以使用`list_filter`屬性,**對顯示結果進行過濾**!
在`polls/admin.py`的QuestionAdmin中添加下面的代碼:
~~~
list_filter = ['pub_date']
~~~
再次刷新change list頁面,你會看到在頁面右邊多出了一個基于`pub_date`的過濾面板,如下圖所示:

<br />
根據你選擇的過濾條件的不同,Django會在面板中添加不同的過濾選項。由于`pub_date`是一個`DateTimeField`,因此Django自動添加了這些選項:“Any date”, “Today”, “Past 7 days”, “This month”, “This year”。
順理成章的,讓我們再添加一些搜索的能力:
~~~
search_fields = ['question_text']
~~~
<br />
這會在頁面的頂部增加一個搜索框。當輸入搜索關鍵字后,Django會在`question_text`字段內進行搜索。只要你愿意,你可以使用任意多個搜索字段,Django在后臺使用的都是SQL查詢語句的LIKE語法,但是有限制的搜索字段有助于后臺的數據庫查詢效率。

其實,這個頁面還自動提供分頁功能,默認每頁顯示100條,只是我們的實例只有一個,囧,所以看不到分頁鏈接。
<br />
## **四、定制admin整體界面**
很明顯,在每一個項目的admin頁面頂端都顯示`Django administration`是很可笑的,它僅僅是個占位文本。利用Django的模板系統,我們可以快速修改它。

在`manage.py`文件同級下創建一個`templates`目錄。然后,打開設置文件`mysite/settings.py`,在TEMPLATES條目中添加一個DIRS選項:
~~~
# mysite/settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # 添加這一行
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
~~~
DIRS是一個文件系統目錄的列表,是模板的搜索路徑。當加載Django模板時,會在DIRS中進行查找。這里面的目錄往往都是全局性的,區別于app自己內部的templates目錄。
**PS:模板的組織方式**
就像靜態文件一樣,我們可以把所有的模板都放在一起,形成一個大大的模板文件夾,并且工作正常。但是請一定不要這么做!強烈建議每一個模板都應該存放在它所屬應用的模板目錄內(例如polls/templates)而不是整個項目的模板目錄(templates),因為這樣每個應用才可以被方便和正確的重用。只有對整個項目有作用的模板文件才放在根目錄的templates中,比如admin界面。
<br />
回到剛才創建的templates目錄中,再創建一個admin目錄,將`admin/base_site.html`模板文件拷貝到該目錄內。這個HTML文件來自Django源碼,它位于`django/contrib/admin/templates`目錄內。 (在我的windows系統中,它位于`C:\Python38\Lib\site-packages\django\contrib\admin\templates\admin`,請大家參考。事實上,如果你用的是Pycharm建立的虛擬環境,那么直接去`venv`目錄中尋找即可。)
<br />
**Django的源代碼在哪里?**
如果你無法找到Django源代碼文件的存放位置,可以使用下面的命令:
~~~
$ python -c "import django; print(django.__path__)"
~~~
編輯`base_site.html`文件,用你喜歡的站點名字替換掉`{{ site_header|default:_(’Django administration’) }}`(包括兩個大括號一起替換掉),看起來像下面這樣:
~~~
{% extends "admin/base.html" %}
{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">www.liujiangblog.com</a></h1>
{% endblock %}
{% block nav-global %}{% endblock %}
~~~
<br />
在這里,我們使用的是硬編碼,強行改名為"www.liujiangblog.com"。但是在實際的項目中,你可以使用`django.contrib.admin.AdminSite.site_header`屬性,方便的對這個頁面title進行自定義。
<br />
修改完后,刷新頁面,效果如下:

<br />
**提示**:所有Django默認的admin模板都可以被重寫,類似剛才重寫`base_site.html`模板的方法一樣,從源代碼目錄將HTML文件拷貝至你自定義的目錄內,然后修改文件。
<br />
**思考!**
讓我們來回顧一下這一小節的操作,其中包含了很多Django的原理。
1. admin后臺是一個內置的app,本質上和你的polls是一樣的
2. 直接修改Django源碼不是好的做法,所以我們不直接修改`base_site.html`模板
3. 我們復制了一份模板,在其中修改了站點名字
4. 為了讓修改的模板能夠自動替換原來的模板,我們創建了一個templates目錄
5. 這個新建的template目錄之所以能起作用,是因為我們在settings中配置了一個DIRS。
6. 當render需要`base_site.html`的時候,Django執行機制會首先去尋找DIRS中是否有`base_site.html`模板,結果找到了!于是它不再繼續尋找,所以admin源碼中的`base_site.html`模板被忽視了,成功達到了我們的目的。
<br />
## **五、定制admin首頁**
默認情況下,admin首頁顯示所有`INSTALLED_APPS`內并在admin應用中注冊過的app,以字母順序進行排序。
<br />
要定制admin首頁,你需要重寫`admin/index.html`模板,就像前面修改`base_site.html`模板的方法一樣,從源碼目錄拷貝到你指定的目錄內。編輯該文件,你會看到文件內使用了一個`app_list`模板變量。該變量包含了所有已經安裝的Django應用。你可以硬編碼鏈接到指定對象的admin頁面,使用任何你認為好的方法,用于替代這個`app_list`。
<br />
## **六、源碼對照**
至此,Django教程的入門部分已經結束了。下面將`polls/admin.py`的全部代碼貼出來:
~~~
from django.contrib import admin
from .models import Choice, Question
class ChoiceInline(admin.TabularInline):
model = Choice
extra = 3
class QuestionAdmin(admin.ModelAdmin):
list_display = ('question_text', 'pub_date', 'was_published_recently')
fieldsets = [
(None, {'fields': ['question_text']}),
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
]
inlines = [ChoiceInline]
list_filter = ['pub_date']
search_fields = ['question_text']
admin.site.register(Question, QuestionAdmin)
~~~
整個投票項目mysite,在Pycharm中的文件組織結構如下圖所示,對比一下你自己的,看看是否一樣。

<br />
注意`2017.png`是展示用的背景圖,這個可以不一樣....
admin后臺管理站點可以定制得很強大,比如下面是博主站點的評論后臺,完全手工定制,非常實用!

- Linux
- Linux 文件權限概念
- 重點總結
- Linux 文件與目錄管理
- 2.1 文件與目錄管理
- 2.2 文件內容查閱
- 文件與文件系統的壓縮,打包與備份
- 3.1 Linux 系統常見的壓縮指令
- 3.2 打包指令: tar
- vi/vim 程序編輯器
- 4.1 vi 的使用
- 4.2 vim編輯器刪除一行或者多行內容
- 進程管理
- 5.1 常用命令使用技巧
- 5.2 進程管理
- 系統服務 (daemons)
- 6.1 通過 systemctl 管理服務
- Linux 系統目錄結構
- Linux yum命令
- linux系統查看、修改、更新系統時間(自動同步網絡時間)
- top linux下的任務管理器
- Linux基本配置
- CentOS7開啟防火墻
- CentOS 使用yum安裝 pip
- strace 命令
- Linux下設置固定IP地址
- 查看Linux磁盤及內存占用情況
- Mysql
- 關系數據庫概述
- 數據庫技術
- 數據庫基礎語句
- 查詢語句(--重點--)
- 約束
- 嵌套查詢(子查詢)
- 表emp
- MySQL數據庫練習
- 01.MySQL數據庫練習數據
- 02.MySQL數據庫練習題目
- 03.MySQL數據庫練習-答案
- Mysql遠程連接數據庫
- Python
- python基礎
- Python3中字符串、列表、數組的轉換方法
- python字符串
- python安裝、pip基本用法、變量、輸入輸出、流程控制、循環
- 運算符及優先級、數據類型及常用操作、深淺拷貝
- 虛擬環境(virtualenv)
- 網絡編程
- TCP/IP簡介
- TCP編程
- UDP編程
- 進程和線程
- 訪問數據庫
- 使用SQLite
- 使用MySQL
- Web開發
- HTML簡介
- Python之日志處理(logging模塊)
- 函數式編程
- 高階函數
- python報錯解決
- 啟動Python時報“ImportError: No module named site”錯誤
- python實例
- 01- 用python解決數學題
- 02- 冒泡排序
- 03- 郵件發送(smtplib)
- Django
- 01 Web應用
- Django3.2 教程
- Django簡介
- Django環境安裝
- 第一個Django應用
- Part 1:請求與響應
- Part 2:模型與后臺
- Part 3:視圖和模板
- Part 4:表單和類視圖
- Part 5:測試
- Part 6:靜態文件
- Part 7:自定義admin
- 第一章:模型層
- 實戰一:基于Django3.2可重用登錄與注冊系統
- 1. 搭建項目環境
- 2. 設計數據模型
- 3. admin后臺
- 4. url路由和視圖
- 5. 前端頁面設計
- 6. 登錄視圖
- 7. Django表單
- 8. 圖片驗證碼
- 9. session會話
- 10. 注冊視圖
- 實戰二:Django3.2之CMDB資產管理系統
- 1.項目需求分析
- 2.模型設計
- 3.數據收集客戶端
- 4.收集Windows數據
- 5.Linux下收集數據
- 6.新資產待審批區
- 7.審批新資產
- django 快速搭建blog
- imooc-Django全棧項目開發實戰
- redis
- 1.1 Redis簡介
- 1.2 安裝
- 1.3 配置
- 1.4 服務端和客戶端命令
- 1.5 Redis命令
- 1.5.1 Redis命令
- 1.5.2 鍵(Key)
- 1.5.3 字符串(string)
- 1.5.4 哈希(Hash)
- 1.5.5 列表(list)
- 1.5.6 集合(set)
- 1.5.7 有序集合(sorted set)
- Windows
- Win10安裝Ubuntu子系統
- win10遠程桌面身份驗證錯誤,要求的函數不受支持
- hm軟件測試
- 02 linux基本命令
- Linux終端命令格式
- Linux基本命令(一)
- Linux基本命令(二)
- 02 數據庫
- 數據庫簡介
- 基本概念
- Navicat使用
- SQL語言
- 高級
- 03 深入了解軟件測試
- day01
- 04 python基礎
- 語言基礎
- 程序中的變量
- 程序的輸出
- 程序中的運算符
- 數據類型基礎
- 數據序列
- 數據類型分類
- 字符串
- 列表
- 元組
- 字典
- 列表與元組的區別詳解
- 函數
- 案例綜合應用
- 列表推導式
- 名片管理系統
- 文件操作
- 面向對象基礎(一)
- 面向對象基礎(二)
- 異常、模塊
- 05 web自動化測試
- Day01
- Day02
- Day03
- Day04
- Day05
- Day06
- Day07
- Day08
- 06 接口自動化測試
- 軟件測試面試大全2020
- 第一章 測試理論
- 軟件測試面試
- 一、軟件基礎知識
- 二、網絡基礎知識
- 三、數據庫
- SQL學生表 — 1
- SQL學生表 — 2
- SQL查詢 — 3
- SQL經典面試題 — 4
- 四、linux
- a. linux常用命令
- 五、自動化測試
- 自動化測試
- python 筆試題
- selenium面試題
- 如何判斷一個頁面上元素是否存在?
- 如何提高腳本的穩定性?
- 如何定位動態元素?
- 如何通過子元素定位父元素?
- 如果截取某一個元素的圖片,不要截取全部圖片
- 平常遇到過哪些問題?如何解決的
- 一個元素明明定位到了,點擊無效(也沒報錯),如果解決?
- selenium中隱藏元素如何定位?(hidden、display: none)
- 六、接口測試
- 接口測試常規面試題
- 接口自動化面試題
- json和字典dict的區別?
- 測試的數據你放在哪?
- 什么是數據驅動,如何參數化?
- 下個接口請求參數依賴上個接口的返回數據
- 依賴于登錄的接口如何處理?
- 依賴第三方的接口如何處理
- 不可逆的操作,如何處理,比如刪除一個訂單這種接口如何測試
- 接口產生的垃圾數據如何清理
- 一個訂單的幾種狀態如何全部測到,如:未處理,處理中,處理失敗,處理成功
- python如何連接數據庫操作?
- 七、App測試
- 什么是activity?
- Activity生命周期?
- Android四大組件
- app測試和web測試有什么區別?
- android和ios測試區別?
- app出現ANR,是什么原因導致的?
- App出現crash原因有哪些?
- app對于不穩定偶然出現anr和crash時候你是怎么處理的?
- app的日志如何抓取?
- logcat查看日志步驟
- 你平常會看日志嗎, 一般會出現哪些異常
- 抓包工具
- fiddler
- Wireshark
- 安全/滲透測試
- 安全性測試都包含哪些內容?
- 開放性思維題
- 面試題
- 字節測試面試
- 一、計算機網絡
- 二、操作系統
- 三、數據庫
- 四、數據結構與算法
- 五、Python
- 六、Linux
- 七、測試用例
- 八、智力/場景題
- 九、開放性問題
- python3_收集100+練習題(面試題)
- python3_100道題目答案
- 接口測試
- 接口測試實例_01
- python+requests接口自動化測試框架實例詳解
- 性能測試
- 性能測試流程
- 性能測試面試題
- 如何編寫性能測試場景用例
- 性能測試:TPS和QPS的區別
- jmeter
- jmeter安裝配置教程
- Jmeter性能測試 入門
- PyCharm
- 快捷工具
- 1-MeterSphere
- 一、安裝和升級
- 2- MobaXterm 教程
- 3-fiddler抓包
- 4-Xshell
- Xshell的安裝和使用
- Xshell遠程連接失敗怎么解決
- 5-Vmware
- Vmware提示以獨占方式鎖定此配置文件失敗
- Windows10徹底卸載VMWare虛擬機步驟
- VM ware無法關機,虛擬機繁忙
- VMware虛擬機下載與安裝
- 解決VM 與 Device/Credential Guard 不兼容。在禁用 Device/Credential Guard 后,可以運行 VM 的方法
- VMware虛擬機鏡像克隆與導入
- 6-WPS
- 1.WPS文檔里的批注怎么刪除
- 2.wps表格中設置圖表的坐標
- 3. wps快速繪制數學交集圖
- 7-MongoDB
- Win10安裝配置MongoDB
- Navicat 15.x for MongoDB安裝破解教程
- Apache
- apache層的賬戶權限控制,以及apache黑名單白名單過濾功能
- HTTP / HTTPS協議
- HTTP協議詳解
- 代理
- 狀態碼詳解
- HTTPS詳解
- Selenium3+python3
- (A) selenium
- selenium自動化環境搭建(Windows10)
- 火狐firebug和firepath插件安裝方法(最新)
- 元素定位工具和方法
- Selenium3+python3自動化
- 新手學習selenium路線圖---學前篇
- 1-操作瀏覽器基本方法
- 2-八種元素定位方法
- 3-CSS定位語法
- 4-登錄案例
- 5-定位一組元素find_elements
- 6-操作元素(鍵盤和鼠標事件)
- 7-多窗口、句柄(handle)
- 8-iframe
- 9-select下拉框
- 10-alert\confirm\prompt
- 11-JS處理滾動條
- 12-單選框和復選框(radiobox、checkbox)
- 13-js處理日歷控件(修改readonly屬性)
- 14-js處理內嵌div滾動條
- 15-table定位
- 16-js處理多窗口
- 17-文件上傳(send_keys)
- 18-獲取百度輸入聯想詞
- 19-處理瀏覽器彈窗
- 20-獲取元素屬性
- 21-判斷元素存在
- 22-爬頁面源碼(page_source)
- 23-顯式等待(WebDriverWait)
- 24-關于面試的題
- 25-cookie相關操作
- 26-判斷元素(expected_conditions)
- 27-判斷title(title_is)
- 28-元素定位參數化(find_element)
- 29-18種定位方法(find_elements)
- 30- js解決click失效問題
- 31- 判斷彈出框存在(alert_is_present)
- 32- 登錄方法(參數化)
- 33- 判斷文本(text_to_be_present_in_element)
- 34- unittest簡介
- 35- unittest執行順序
- 36- unittest之裝飾器(@classmethod)
- 37- unittest之斷言(assert)
- 38- 捕獲異常(NoSuchElementException)
- 39- 讀取Excel數據(xlrd)
- 40- 數據驅動(ddt)
- 41- 異常后截圖(screenshot)
- 42- jenkins持續集成環境搭建
- 43- Pycharm上python和unittest兩種運行方式
- 44- 定位的坑:class屬性有空格
- 45- 只截某個元素的圖
- 46- unittest多線程執行用例
- 47- unittest多線程生成報告(BeautifulReport)
- 48- 多線程啟動多個不同瀏覽器
- (B) python3+selenium3實現web UI功能自動化測試框架
- (C) selenium3常見報錯處理
- 書籍
- (D)Selenium3自動化測試實戰--基于Python語
- 第4章 WebDriver API
- 4.1 從定位元素開始
- 4.2 控制瀏覽器
- 4.3 WebDriver 中的常用方法
- 4.4 鼠標操作
- 4.5 鍵盤操作
- 4.6 獲得驗證信息
- 4.7 設置元素等待
- 4.8 定位一組元素
- 4.9 多表單切換
- 4.10 多窗口切換
- 4.11 警告框處理
- 4.12 下拉框處理
- 4.13 上傳文件
- 4.14 下載文件
- 4.15 操作cookie
- 4.16 調用JavaScript
- 4.17 處理HTML5視頻播放
- 4.18 滑動解鎖
- 4.19 窗口截圖
- 第5章 自動化測試模型
- 5.3 模塊化與參數化
- 5.4 讀取數據文件
- 第6章 unittest單元測試框架
- 6.1 認識unittest