浩的博客

python工业互联网应用实战10—数据校验model层的clean()

  前面的单元测试章节主要介绍了如何用单元测试模块来进行业务逻辑测试。只是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)

  现在把工程运行起来,试着提交一条数据库已有任务号的任务数据,我们会得到如下提示,重复任务号的任务数据不能提交到数据库层,这个模式达到了设计预期。但是操作不是非常友好,界面从修改界面跳转到了列表界面,如果我们需要修改任务号重新录入任务,我得重新录入所有任务数据!

 

   这个操作模式是不符合用户习惯的,djangomodel层提供了一个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等其它接口时,不会再有大量的重复性开发工作。同时,也演示了编写单元测试对代码改进方面的好处。通过单元测试我们能发现很多传统通过功能测试或者集成测试才会发现的问题,从而在开发过程中就能优化我们的代码结构和设计,所以用好单元测试对于企业应用开发来说是“事半功倍”的效果。

posted on 2021-03-26 08:43  wuch  阅读(315)  评论(0编辑  收藏  举报

导航