对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。