python工業網際網路應用實戰10—資料校驗model層的clean() (2021-03-07 18:15)
前面的單元測試章節主要介紹瞭如何用單元測試模組來進行業務邏輯測試。只是Django的單元測試還能給我們更多......它每次執行單元測試都會建立新的資料庫用來執行單元測試,這樣單元測試除了驗證業務邏輯之外,也能直接測試model層到資料庫是否符合整個的設計預期,這樣就可以把業務邏輯和資料層邏輯都納入到單元測試中來進行統一的業務驗證。
1.1. 資料校驗
目前為止我們未對錄入的任務做任何資料校驗,比如我們的任務號可以是空的也可以錄入重複的,任務號作為任務的可讀標識,必須是唯一的和不能為空,才能確保資料庫儲存的資料符合我們設計的業務邏輯。當前任務錄入是通過admin來實現的,我們先把任務號的唯一保護先放到admin 的save_model來進行處理,看看效果如何?
... def save_model(self, request, obj, form, change): #新增任務預設狀態設定為 未處理 if obj.pk==None: obj.State=1 obj.User=request.user if Task.objects.filter(TaskNum=obj.TaskNum).exists(): messages.set_level(request, messages.ERROR) messages.error(request, '任務號重複!') else: return super().save_model(request, obj, form, change)
現在把工程執行起來,試著提交一條資料庫已有任務號的任務資料,我們會得到如下提示,重複任務號的任務資料不能提交到資料庫層,這個模式達到了設計預期。但是操作不是非常友好,介面從修改介面跳轉到了列表介面,如果我們需要修改任務號重新錄入任務,我得重新錄入所有任務資料!
這個操作模式是不符合使用者習慣的,django的model層提供了一個clean函式來進行儲存前的資料校驗可以滿足校驗需求,同時可以raise錯誤出來,現在我們把校驗邏輯移到model層的clean函式中執行(記得還原admin.py save_model程式碼)。
... def clean(self): querySet = Task.objects.filter(TaskNum=self.TaskNum) if(self.pk!=None and self.pk>0): querySet=querySet.exclude(pk=self.pk) if querySet.exists(): raise ValidationError({'TaskNum': '任務號重複!'})
檔案:Task\models.py
重新執行一下提交重複任務號資料測試,效果如下圖:
錯誤提示非常清晰“任務號重複!”,重新修改後提交儲存即可。
如果未來我們的專案除了通過admin錄入還會存在web API介面錄入等其它方式新增任務資料的話,model層的clean校驗提供了最終校驗方案,比納入到save_model寫校驗只能校驗admin UI錄入的資料不能校驗其它渠道錄入的資料要好很多!否則實際專案中就遇到正常錄入的資料校驗是符合要求的,但是API介面推送過的資料就有問題。
1.2. 單元測試model層
從現在起我們養成好的習慣吧,每個邏輯都編寫單元測試、編寫單元測試、編寫單元測試,後面我們會發現這個習慣是一個多麼的好的好習慣!編寫測試重複提交的單元測試函式。
... def test_model_task_duplicate(self): data={'TaskNum':'100','Source':'101','Target':'05-01-01','Barcode':'101001001008','State':1,'Priority':1,} task = Task.objects.create(**data) self.assertEqual(task.Source,'101') self.assertEqual(task.Target,'05-01-01') self.assertEqual(task.Barcode,'101001001008') data={'TaskNum':'100','Source':'101','Target':'05-01-01','Barcode':'101001001008','State':1,'Priority':1,} model=Task(**data) model.full_clean()
執行單元測試會得到如下結果:
D:\my tfs\IndDemo>python manage.py test Task Creating test database for alias 'default'... System check identified no issues (0 silenced). .E. ====================================================================== ERROR: test_model_task_duplicate (Task.tests.TaskTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "D:\my tfs\IndDemo\Task\tests.py", line 69, in test_model_task_duplicate model.full_clean() File "C:\Python\Python36-32\lib\site-packages\django\db\models\base.py", line 1203, in full_clean raise ValidationError(errors) django.core.exceptions.ValidationError: {'BeginDate': ['此欄位不能為空。'], 'EndDate': ['此欄位不能為空。'], 'User': [' 此欄位不能為空。'], 'TaskNum': ['任務號重複!']} ---------------------------------------------------------------------- Ran 3 tests in 0.008s FAILED (errors=1) Destroying test database for alias 'default'... D:\my tfs\IndDemo>
執行單元測試我們會發現model的一些欄位得調整null=True,需要增加,否則就會報上述錯誤。
... class Task(models.Model): STATE_NEW =1 STATE_PROCESSED=4 STATE_RUNNING=5 STATE_COMPLETED=99 STATE_CANCEL=-1 TASK_STATE=((STATE_NEW,u'未處理'),(STATE_PROCESSED,u'處理成功'),(STATE_RUNNING,u'執行中'),(STATE_COMPLETED,u'完成'),(STATE_CANCEL,u'已取消')) TaskId = models.AutoField(u'ID',primary_key=True, db_column='task_id') TaskNum = models.IntegerField(u'任務號', null=False, db_column='task_num') Source = models.CharField(u'源地址', null=False, max_length=50, db_column='source') Target = models.CharField(u'目標地址', null=False, max_length=50, db_column='target') Barcode = models.CharField(u'容器條碼', null=False, max_length=50, db_column='barcode') State = models.IntegerField(u'狀態', choices=TASK_STATE, null=False, db_column='state') Priority = models.IntegerField(u'優先順序', choices=PRIORITY, null=False,blank=True, db_column='priority') BeginDate = models.DateTimeField(u'開始時間',null=True,blank=True, db_column='begin_date') EndDate = models.DateTimeField(u'結束時間',null=True,blank=True, db_column='end_date') SystemDate = models.DateTimeField(u'系統時間', null=False, auto_now_add=True, db_column='system_date') User = models.ForeignKey(User, verbose_name="操作員",null=True,blank=True, on_delete=models.CASCADE,db_column='user_id')
再執行一次單元測試現在就只提示:“任務號重複!”
D:\my tfs\IndDemo>python manage.py test Task Creating test database for alias 'default'... System check identified no issues (0 silenced). .E. ====================================================================== ERROR: test_model_task_duplicate (Task.tests.TaskTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "D:\my tfs\IndDemo\Task\tests.py", line 69, in test_model_task_duplicate model.full_clean() File "C:\Python\Python36-32\lib\site-packages\django\db\models\base.py", line 1203, in full_clean raise ValidationError(errors) django.core.exceptions.ValidationError: {'TaskNum': ['任務號重複!']} ---------------------------------------------------------------------- Ran 3 tests in 0.009s FAILED (errors=1) Destroying test database for alias 'default'... D:\my tfs\IndDemo>
最後單元測試程式碼調整如下:
... def test_model_task_duplicate(self): data={'TaskNum':100,'Source':'101','Target':'05-01-01','Barcode':'101001001008','State':1,'Priority':1,} task = Task.objects.create(**data) self.assertEqual(task.TaskNum,100) self.assertEqual(task.Source,'101') self.assertEqual(task.Target,'05-01-01') self.assertEqual(task.Barcode,'101001001008') with self.assertRaises(ValidationError): #① data={'TaskNum':100,'Source':'101','Target':'05-01-01','Barcode':'101001001008','State':1,'Priority':1,} model=Task(**data) model.full_clean()
標註①:採用assertRaises斷言來確保是否raise ValidationError錯誤!
通過單元測試我們確定model層提供了任務號是否重複的資料驗證!大家也會發現最後的測試程式碼 data={'TaskNum':100,}任務編碼被改成了整型,就是新增的測試斷言 self.assertEqual(task.TaskNum,100) 讓筆者發現例項的資料型別錯誤,我們設計的TaskNum的欄位型別是整型的,字串提交到model層後被強制改成整型了,如果我們斷言是字串型的'100' 會得到一個錯誤的斷言。
1.3. 小結
本小節通過講述如何資料校驗以及為了提高資料驗證程式碼的重用性,我們把驗證儘量放到model層進行,好處就是為了再對外提供webAPI等其它介面時,不會再有大量的重複性開發工作。同時,也演示了編寫單元測試對程式碼改進方面的好處。通過單元測試我們能發現很多傳統通過功能測試或者整合測試才會發現的問題,從而在開發過程中就能優化我們的程式碼結構和設計,所以用好單元測試對於企業應用開發來說是“事半功倍”的效