標籤:
PYTHON開發入門與實戰11-單元測試1. 單元測試
本章節我們來講講django工程中如何?單元測試,單元測試如何編寫以及在可持續項目中單元測試的重要性。
下面是單元測試的定義:
單元測試是開發人員編寫的一小段代碼,用於檢驗被測代碼的一個很小的、很明確的功能是否正確。
1. 它是一種驗證行為
程式中的每一項功能都是測試來驗證它的正確性。它為以後的開發提供支援。就算是開發後期,我們也可以輕鬆的增加功能或更改程式結構,而不用擔心這個過程中會破壞重要的東西,它為代碼的重構提供了保障。這樣,我們就可以更自由的對程式進行改進。
2. 它是一種設計行為
編寫單元測試將使我們從調用者觀察、思考。特別是先寫測試(test-first),迫使我們把程式設計成易於調用和可測試的,即迫使我們解除軟體中的耦合。什麼時候測試?單元測試越早越好,早到什麼程度?極限編程(Extreme Programming,或簡稱XP)講究TDD,即測試驅動開發,先編寫測試代碼,再進行開發。
不過在實際的編碼過程中,我們不必過分強調先幹什麼後寫什麼,重要的是高效和個人感覺舒適。從筆者的經驗來看,根據設計或需求先編寫某個功能函數的架構,然後就著手編寫測試函數,針對產品的功能編寫測試案例,最後編寫函數的實現代碼,每完成一個功能點都運行單元測試,隨時補充完善測試案例。這種測試同行代碼編寫入模式,會對函數的構思有很大的協助,如何去編寫可單元測試的函數慢慢的就會變成書寫和思考習慣。
所謂先編寫產品功能的函數架構,是指先編寫函數空的實現,考慮參數的有哪些參數和可驗證的傳回值,同時預設直接返回一個合適值(假定值),編譯通過後即可編寫測試代碼,這時,函數名、參數表、傳回型別都應該確定下來了,所編寫的測試代碼以後需修改的可能性比較小,當然由於個人編碼成熟度等級的不同,實際開發過程中調整也在所難免,好在單元測試可以迅速跟蹤調準導致的影響。
3. 它是一種編寫文檔的行為
單元測試是一種無價的文檔,它是展示函數或類如何使用的最佳文檔。這份文檔是可編譯、可啟動並執行,並且它保持最新,永遠與代碼同步。
4. 它具有迴歸性。
自動化的單元測試避免了代碼出現迴歸,編寫完成之後,可以隨時隨地的快速運行測試。筆者的經驗表明一個盡責的單元測試方法將會在軟體開發的早期階段就可以發現很多的Bug,並且修改它們的成本也很低。在軟體開發的後期階段,Bug的發現和修改將會變得更加困難,尤其修改BUG可能導致引入新的BUG,並要消耗大量的時間和開發費用。
筆者的經曆的項目就遇到這樣的問題,項目上線的前一天的某個BUG修改,導致當晚一直加班深夜解決新引入的BUG問題,所以後來單元測試的迴歸性在筆者的項目經驗裡最喜歡的特性。無論什麼時候作出修改都要進行完整的迴歸測試,可以避免修改可能引入的BUG。在生命週期中儘早地對軟體產品進行測試將使效率和品質得到最好的保證。在提供了經過測試的單元的情況下,系統整合過程將會極大地簡化。開發人員可以將精力集中在單元之間的互動作用和全域的功能實現上,而不是陷入充滿很多Bug的單元之中不能自拔。
如果考慮做一個可以持續改進和維護的項目,尤其有大量的商務規則和邏輯的系統,單元測試就顯得非常重要,單元測試主要這對商務邏輯編寫測試代碼,確定編碼是否滿足測試要求。下面我們就進入Django的單元測試實踐吧。
1.1. 運行單元測試
我們建立好Django app 每個app 都會建立一個單元測試tests.py的單元測試檔案,代碼如下:
"""This file demonstrates writing tests using the unittest module. These will passwhen you run "manage.py test".Replace this with more appropriate tests for your application."""from django.test import TestCaseclass SimpleTest(TestCase): def test_basic_addition(self): """ Tests that 1 + 1 always equals 2. """ self.assertEqual(1 + 1, 2)
我們現在可以在IDE環境中,TEST->All Tests運行這個測試例子看看單元測試啟動並執行效果。
1.2. 開始我們的第一個單元測試
我們來看看就提交入庫單這個業務來說,目前的views.py函數AddInStockBill是沒辦法進行單元測試的,應為期參數涉及到web請求內容參數request,如何編寫可具備單元測試的功能代碼也是早期編寫單元測試,可以讓我們逐步掌握的代碼解耦的思維模式。
入庫單業務的關鍵點,就是入庫單提交後我們需要更新該入庫單對應物料的庫存資料,虛擬碼如下:
1. 根據當前入庫單的物料,在庫存表中尋找當前物料的庫存記錄;
2. 如果有當前庫存記錄返回當前庫存對象,如果沒有就建立一個新的對象;
3. 更新入庫單對應物料的當前庫存資料;
我們來看看如何嘗試測試先行的開發模式去考慮一個入庫單model提交將導致庫存的更新情境,用代碼說話吧:
from django.test import TestCasefrom inventory.models import *from inventory import views class InventoryTest(TestCase):def test_updating_inventory_in(self): #1.建立一個Item執行個體; item = Item() item.ItemId = 1 item.ItemCode = ‘1001‘ item.ItemName = ‘普通螺母‘ #2.床建一個新入庫單對象 inStockBill = InStockBill() inStockBill.InStockBillCode=‘201501010001‘ inStockBill.InStockDate = ‘2015-01-01‘ inStockBill.Operator = ‘張三‘ inStockBill.Amount = 10 inStockBill.Item = item #3.建立當前該物料的庫存對象 inventory = Inventory() inventory.InventoryId = 1
inventory.Item = item inventory.Amount = 10 #當前庫存數量 #如何構建更新庫存的函數,讓其可具備測試調用 views.UpdatingInventoryIn(inStockBill,inventory) #校正測試是否滿足當前情境 self.assertEqual(inventory.Amount ,20)
當前我們當前運行單元測試肯定會出錯,因為我們還沒有編寫views.UpdatingInventoryIn函數:
def UpdatingInventoryIn(inStockBill,inventory): inventory.Amount = inventory.Amount + inStockBill.Amount
1.3. 執行單元測試
在IDE環境中執行改成我們寫好的單元測試,我們看到結果如,測試通過。
這裡我們的單元測試主要針對核心業務來構建,不考慮相關對象的擷取方式,就是說測試案例是我們根據測試情境來構建的,不考慮對象是否在資料庫中,也就是與持久層沒有關係。早年筆者在這裡也是大費周折,測試資料與持久層資料緊密耦合,結果更換資料庫或者認為刪除資料後,單元測試的迴歸測試就無法執行,單元測試的優勢大打折扣。單元測試的迴歸性在後續代碼重構,業務變更中有著巨大的優勢,不能迴歸的單元測試價值就少了很多,所以我們在考慮單元測試時,一定要盡量與持久層資料解耦,測試案例資料在測試代碼中構建。
1.4. 代碼的持續改進
前面的代碼中,我們的測試案例情境是假定該物料是已經有庫存資料的,那如果該物料以前沒有庫存資料,我們的代碼怎麼來寫呢,我們還是從測試案例開始吧,增加測試案例代碼。
…
#校正測試是否滿足當前情境 self.assertEqual(inventory.Amount ,20) inventory = Inventory() #當前沒有庫存資料,我們建立對象屬性都沒有賦值 views.UpdatingInventoryIn(inStockBill,inventory) self.assertEqual(inventory.Amount ,10) self.assertEqual(inventory.Item.ItemId ,inStockBill.Item.ItemId)
執行測試錯誤,因為views.UpdatingInventoryIn(inStockBill,inventory)沒有對inventory為空白的情況處理,我改進代碼來滿足這樣的需求。於是函數代碼就變成了下面這樣:
def UpdatingInventoryIn(inStockBill,inventory):
if (inventory.InventoryId == None): inventory.Item = inStockBill.Item inventory.Amount = 0inventory.Amount = inventory.Amount + inStockBill.Amount
執行測試通過,剛才的測試案例是寫在一個測試函數裡還是分開寫,主要是看我們的測試案例複雜度了,複雜度高的就分開來寫,簡單就寫在一個測試函數裡。函數粒度的選擇由程式員來考慮了,核心就是關注可讀性,函數太長我們就把函數拆小,提高可讀性。(這裡筆者強烈推薦《代碼重構》這本很多年以前的經典書)。
最後們views裡的增加入庫單函數重構成如下這樣代碼:
@transaction.commit_on_successdef AddInStockBill(request): if request.method == ‘POST‘: form = InStockBillForm(request.POST) if form.is_valid(): cd = form.cleaned_data inStockBill = InStockBill() inStockBill.InStockBillCode = cd[‘InStockBillCode‘] inStockBill.InStockDate = cd[‘InStockDate‘] inStockBill.Amount = cd[‘Amount‘] inStockBill.Operator = cd[‘Operator‘] inStockBill.Item = cd[‘Item‘] inventorys = inStockBill.Item.inventory_set.all() if (inventorys.count()==0): currentInventory = Inventory() else: currentInventory = inventorys[0] #注意的函數調用,你會發現更新庫存與如何擷取庫存對象完全解耦 UpdatingInventoryIn(inStockBill,currentInventory) currentInventory.save() #更新庫存 inStockBill.save() #儲存入庫單資料 return HttpResponseRedirect(‘/success/‘) else: form = InStockBillForm() return render_to_response(‘InStockAdd.html‘,{‘form‘: form} ,context_instance = RequestContext(request))
1.5. 小結
如何編寫單元測試代碼,測試先行的模式會讓編碼人員去思考如何把商務邏輯抽象出來變成一個可以用單元測試來跟蹤的函數單元很有協助,如果我們在編寫一個與資料庫打交道的應用系統,把商務邏輯與如何擷取資料解耦合對系統的可擴充性和可維護性相當的重要,尤其當我們打算構建一個可以持續改進的系統時尤為如此。
標籤: python, django, unit test
PYTHON單元測試