对Python的Django框架中的项目进行单元测试的方法
Python中的单元测试
我们先来回顾一下Python中的单元测试方法。
下面是一个Python的单元测试简单的例子:
假如我们开发一个除法的功能,有的同学可能觉得很简单,代码是这样的:
defdivision_funtion(x,y): returnx/y
但是这样写究竟对还是不对呢,有些同学可以在代码下面这样测试:
defdivision_funtion(x,y): returnx/y if__name__=='__main__': printdivision_funtion(2,1) printdivision_funtion(2,4) printdivision_funtion(8,3)
但是这样运行后得到的结果,自己每次都得算一下去核对一遍,很不方便,Python中有unittest模块,可以很方便地进行测试,详情可以文章最后的链接,看官网文档的详细介绍。
下面是一个简单的示例:
importunittest defdivision_funtion(x,y): returnx/y classTestDivision(unittest.TestCase): deftest_int(self): self.assertEqual(division_funtion(9,3),3) deftest_int2(self): self.assertEqual(division_funtion(9,4),2.25) deftest_float(self): self.assertEqual(division_funtion(4.2,3),1.4) if__name__=='__main__': unittest.main()
我简单地写了三个测试示例(不一定全面,只是示范,比如没有考虑除数是0的情况),运行后发现:
F.F ====================================================================== FAIL:test_float(__main__.TestDivision) ---------------------------------------------------------------------- Traceback(mostrecentcalllast): File"/Users/tu/YunPan/mydivision.py",line16,intest_float self.assertEqual(division_funtion(4.2,3),1.4) AssertionError:1.4000000000000001!=1.4 ====================================================================== FAIL:test_int2(__main__.TestDivision) ---------------------------------------------------------------------- Traceback(mostrecentcalllast): File"/Users/tu/YunPan/1.py",line13,intest_int2 self.assertEqual(division_funtion(9,4),2.25) AssertionError:2!=2.25 ---------------------------------------------------------------------- Ran3testsin0.001s FAILED(failures=2)
汗!发现了没,竟然两个都失败了,测试发现:
4.2除以3等于1.4000000000000001不等于期望值1.4
9除以4等于2,不等于期望的2.25
下面我们就是要修复这些问题,再次运行测试,直到运行不报错为止。
譬如根据实际情况,假设我们只需要保留到小数点后6位,可以这样改:
defdivision_funtion(x,y): returnround(float(x)/y,6)
再次运行就不报错了:
... ---------------------------------------------------------------------- Ran3testsin0.000s
OK
Django中的单元测试
尽早进行单元测试(UnitTest)是比较好的做法,极端的情况甚至强调“测试先行”。现在我们已经有了第一个model类和Form类,是时候开始写测试代码了。
Django支持python的单元测试(unittest)和文本测试(doctest),我们这里主要讨论单元测试的方式。这里不对单元测试的理论做过多的阐述,假设你已经熟悉了下列概念:testsuite,testcase,test/testaction, testdata,assert等等。
在单元测试方面,Django继承python的unittest.TestCase实现了自己的django.test.TestCase,编写测试用例通常从这里开始。测试代码通常位于app的tests.py文件中(也可以在models.py中编写,但是我不建议这样做)。在Django生成的depotapp中,已经包含了这个文件,并且其中包含了一个测试用例的样例:
depot/depotapp/tests.py
fromdjango.testimportTestCase classSimpleTest(TestCase): deftest_basic_addition(self): """ Teststhat1+1alwaysequals2. """ self.assertEqual(1+1,2)
你可以有几种方式运行单元测试:
- pythonmanage.pytest:执行所有的测试用例
- pythonmanage.pytestapp_name,执行该app的所有测试用例
- pythonmanage.pytestapp_name.case_name:执行指定的测试用例
用第三种方式执行上面提供的样例,结果如下:
$pythonmanage.pytestdepotapp.SimpleTest
Creatingtestdatabaseforalias'default'... . ---------------------------------------------------------------------- Ran1testin0.012s OK Destroyingtestdatabaseforalias'default'...
你可能会主要到,输出信息中包括了创建和删除数据库的操作。为了避免测试数据造成的影响,测试过程会使用一个单独的数据库,关于如何指定测试数据库的细节,请查阅Django文档。在我们的例子中,由于使用sqlite数据库,Django将默认采用内存数据库来进行测试。
下面就让我们来编写测试用例。在《AgileWebDevelopmentwithRails4th》中,7.2节,最终实现的ProductTest代码如下:
classProductTest<ActiveSupport::TestCase test"productattributesmustnotbeempty"do product=Product.new assertproduct.invalid? assertproduct.errors[:title].any? assertproduct.errors[:description].any? assertproduct.errors[:price].any? assertproduct.errors[:image_url].any? end test"productpricemustbepositive"do product=Product.new(:title=>"MyBookTitle", :description=>"yyy", :image_url=>"zzz.jpg") product.price=-1 assertproduct.invalid? assert_equal"mustbegreaterthanorequalto0.01", product.errors[:price].join(';') product.price=0 assertproduct.invalid? assert_equal"mustbegreaterthanorequalto0.01", product.errors[:price].join(';') product.price=1 assertproduct.valid? end defnew_product(image_url) Product.new(:title=>"MyBookTitle", :description=>"yyy", :price=>1, :image_url=>image_url) end test"imageurl"do ok=%w{fred.giffred.jpgfred.pngFRED.JPGFRED.Jpg http://a.b.c/x/y/z/fred.gif} bad=%w{fred.docfred.gif/morefred.gif.more} ok.eachdo|name| assertnew_product(name).valid?,"#{name}shouldn'tbeinvalid" end bad.eachdo|name| assertnew_product(name).invalid?,"#{name}shouldn'tbevalid" end end test"productisnotvalidwithoutauniquetitle"do product=Product.new(:title=>products(:ruby).title, :description=>"yyy", :price=>1, :image_url=>"fred.gif") assert!product.save assert_equal"hasalreadybeentaken",product.errors[:title].join(';') end test"productisnotvalidwithoutauniquetitle-i18n"do product=Product.new(:title=>products(:ruby).title, :description=>"yyy", :price=>1, :image_url=>"fred.gif") assert!product.save assert_equalI18n.translate('activerecord.errors.messages.taken'), product.errors[:title].join(';') end end
对Product测试的内容包括:
1.title,description,price,image_url不能为空;
2.price必须大于零;
3.image_url必须以jpg,png,jpg结尾,并且对大小写不敏感;
4.titile必须唯一;
让我们在Django中进行这些测试。由于ProductForm包含了模型校验和表单校验规则,使用ProductForm可以很容易的实现上述测试:
depot/depotapp/tests.py
#/usr/bin/python #coding:utf8 """ Thisfiledemonstrateswritingtestsusingtheunittestmodule.Thesewillpass whenyourun"manage.pytest". Replacethiswithmoreappropriatetestsforyourapplication. """ fromdjango.testimportTestCase fromformsimportProductForm classSimpleTest(TestCase): deftest_basic_addition(self): """ Teststhat1+1alwaysequals2. """ self.assertEqual(1+1,2) classProductTest(TestCase): defsetUp(self): self.product={ 'title':'MyBookTitle', 'description':'yyy', 'image_url':'http://google.com/logo.png', 'price':1 } f=ProductForm(self.product) f.save() self.product['title']='MyAnotherBookTitle' ####title,description,price,image_url不能为空 deftest_attrs_cannot_empty(self): f=ProductForm({}) self.assertFalse(f.is_valid()) self.assertTrue(f['title'].errors) self.assertTrue(f['description'].errors) self.assertTrue(f['price'].errors) self.assertTrue(f['image_url'].errors) ####price必须大于零 deftest_price_positive(self): f=ProductForm(self.product) self.assertTrue(f.is_valid()) self.product['price']=0 f=ProductForm(self.product) self.assertFalse(f.is_valid()) self.product['price']=-1 f=ProductForm(self.product) self.assertFalse(f.is_valid()) self.product['price']=1 ####image_url必须以jpg,png,jpg结尾,并且对大小写不敏感; deftest_imgae_url_endwiths(self): url_base='http://google.com/' oks=('fred.gif','fred.jpg','fred.png','FRED.JPG','FRED.Jpg') bads=('fred.doc','fred.gif/more','fred.gif.more') forendwithinoks: self.product['image_url']=url_base+endwith f=ProductForm(self.product) self.assertTrue(f.is_valid(),msg='errorwhenimage_urlendwith'+endwith) forendwithinbads: self.product['image_url']=url_base+endwith f=ProductForm(self.product) self.assertFalse(f.is_valid(),msg='errorwhenimage_urlendwith'+endwith) self.product['image_url']='http://google.com/logo.png' ###titile必须唯一 deftest_title_unique(self): self.product['title']='MyBookTitle' f=ProductForm(self.product) self.assertFalse(f.is_valid()) self.product['title']='MyAnotherBookTitle'
然后运行pythonmanage.pytestdepotapp.ProductTest。如同预想的那样,测试没有通过:
Creatingtestdatabaseforalias'default'... .F.. ====================================================================== FAIL:test_imgae_url_endwiths(depot.depotapp.tests.ProductTest) ---------------------------------------------------------------------- Traceback(mostrecentcalllast): File"/Users/holbrook/Documents/Dropbox/depot/../depot/depotapp/tests.py",line65,intest_imgae_url_endwiths self.assertTrue(f.is_valid(),msg='errorwhenimage_urlendwith'+endwith) AssertionError:FalseisnotTrue:errorwhenimage_urlendwithFRED.JPG ---------------------------------------------------------------------- Ran4testsin0.055s FAILED(failures=1) Destroyingtestdatabaseforalias'default'...
因为我们之前并没有考虑到image_url的图片扩展名可能会大写。修改ProductForm的相关部分如下:
defclean_image_url(self): url=self.cleaned_data['image_url'] ifnotendsWith(url.lower(),'.jpg','.png','.gif'): raiseforms.ValidationError('图片格式必须为jpg、png或gif') returnurl
然后再运行测试:
$pythonmanage.pytestdepotapp.ProductTest
Creatingtestdatabaseforalias'default'... .... ---------------------------------------------------------------------- Ran4testsin0.060s OK Destroyingtestdatabaseforalias'default'...
测试通过,并且通过单元测试,我们发现并解决了一个bug。