Django实战(10):单元测试
尽早进行单元测试(UnitTest)是比较好的做法,极端的情况甚至强调“测试先行”。现在我们已经有了第一个model类和Form类,是时候开始写测试代码了。
Django支持python的单元测试(unit test)和文本测试(doc test),我们这里主要讨论单元测试的方式。这里不对单元测试的理论做过多的阐述,假设你已经熟悉了下列概念:test suite, test case, test/test action, test data, assert等等。
在单元测试方面,Django继承python的unittest.TestCase实现了自己的django.test.TestCase,编写测试用 例通常从这里开始。测试代码通常位于app的tests.py文件中(也可以在models.py中编写,但是我不建议这样做)。在Django生成的 depotapp中,已经包含了这个文件,并且其中包含了一个测试用例的样例:
depot/depotapp/tests.py
- from django.test import TestCase
- class SimpleTest(TestCase):
- def test_basic_addition(self):
- """
- Tests that 1 + 1 always equals 2.
- """
- self.assertEqual(1 + 1, 2)
你可以有几种方式运行单元测试:
python manage.py test:执行所有的测试用例
python manage.py test app_name, 执行该app的所有测试用例
python manage.py test app_name.case_name: 执行指定的测试用例
用第三中方式执行上面提供的样例,结果如下:
$ python manage.py test depotapp.SimpleTest
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.012s
OK
Destroying test database for alias 'default'...
你可能会主要到,输出信息中包括了创建和删除数据库的操作。为了避免测试数据造成的影响,测试过程会使用一个单独的数据库,关于如何指定测试数据库 的细节,请查阅Django文档。在我们的例子中,由于使用sqlite数据库,Django将默认采用内存数据库来进行测试。
下面就让我们来编写测试用例。在《Agile Web Development with Rails 4th》中,7.2节,最终实现的ProductTest代码如下:
- class ProductTest < ActiveSupport::TestCase
- test "product attributes must not be empty"do
- product = Product.new
- assert product.invalid?
- assert product.errors[:title].any?
- assert product.errors[:description].any?
- assert product.errors[:price].any?
- assert product.errors[:image_url].any?
- end
- test "product price must be positive"do
- product = Product.new(:title => "My Book Title",
- :description => "yyy",
- :image_url => "zzz.jpg")
- product.price = -1
- assert product.invalid?
- assert_equal "must be greater than or equal to 0.01",
- product.errors[:price].join('; ')
- product.price = 0
- assert product.invalid?
- assert_equal "must be greater than or equal to 0.01",
- product.errors[:price].join('; ')
- product.price = 1
- assert product.valid?
- end
- def new_product(image_url)
- Product.new(:title => "My Book Title",
- :description => "yyy",
- :price => 1,
- :image_url => image_url)
- end
- test "image url"do
- ok = %w{ fred.gif fred.jpg fred.png FRED.JPG FRED.Jpg
- http://a.b.c/x/y/z/fred.gif }
- bad = %w{ fred.doc fred.gif/more fred.gif.more }
- ok.eachdo |name|
- assert new_product(name).valid?, "#{name} shouldn't be invalid"
- end
- bad.eachdo |name|
- assert new_product(name).invalid?, "#{name} shouldn't be valid"
- end
- end
- test "product is not valid without a unique title"do
- product = Product.new(:title => products(:ruby).title,
- :description => "yyy",
- :price => 1,
- :image_url => "fred.gif")
- assert !product.save
- assert_equal "has already been taken", product.errors[:title].join('; ')
- end
- test "product is not valid without a unique title - i18n"do
- product = Product.new(:title => products(:ruby).title,
- :description => "yyy",
- :price => 1,
- :image_url => "fred.gif")
- assert !product.save
- assert_equal I18n.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
- """
- This file demonstrates writing tests using the unittest module. These will pass
- when you run "manage.py test".
- Replace this with more appropriate tests for your application.
- """
- from django.test import TestCase
- from forms import ProductForm
- class SimpleTest(TestCase):
- def test_basic_addition(self):
- """
- Tests that 1 + 1 always equals 2.
- """
- self.assertEqual(1 + 1, 2)
- class ProductTest(TestCase):
- def setUp(self):
- self.product = {
- 'title':'My Book Title',
- 'description':'yyy',
- 'image_url':'http://google.com/logo.png',
- 'price':1
- }
- f = ProductForm(self.product)
- f.save()
- self.product['title'] = 'My Another Book Title'
- #### title,description,price,image_url不能为空
- def test_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必须大于零
- def test_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结尾,并且对大小写不敏感;
- def test_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')
- for endwith in oks:
- self.product['image_url'] = url_base+endwith
- f = ProductForm(self.product)
- self.assertTrue(f.is_valid(),msg='error when image_url endwith '+endwith)
- for endwith in bads:
- self.product['image_url'] = url_base+endwith
- f = ProductForm(self.product)
- self.assertFalse(f.is_valid(),msg='error when image_url endwith '+endwith)
- self.product['image_url'] = 'http://google.com/logo.png'
- ### titile必须唯一
- def test_title_unique(self):
- self.product['title'] = 'My Book Title'
- f = ProductForm(self.product)
- self.assertFalse(f.is_valid())
- self.product['title'] = 'My Another Book Title'
然后运行 python manage.py test depotapp.ProductTest。如同预想的那样,测试没有通过:
Creating test database for alias 'default'...
.F..
======================================================================
FAIL: test_imgae_url_endwiths (depot.depotapp.tests.ProductTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/holbrook/Documents/Dropbox/depot/../depot/depotapp/tests.py", line 65, in test_imgae_url_endwiths
self.assertTrue(f.is_valid(),msg='error when image_url endwith '+endwith)
AssertionError: False is not True : error when image_url endwith FRED.JPG
----------------------------------------------------------------------
Ran 4 tests in 0.055s
FAILED (failures=1)
Destroying test database for alias 'default'...
因为我们之前并没有考虑到image_url的图片扩展名可能会大写。修改ProductForm的相关部分如下:
- def clean_image_url(self):
- url = self.cleaned_data['image_url']
- ifnot endsWith(url.lower(), '.jpg', '.png', '.gif'):
- raise forms.ValidationError('图片格式必须为jpg、png或gif')
- return url
然后再运行测试:
$ python manage.py test depotapp.ProductTest
Creating test database for alias 'default'...
....
----------------------------------------------------------------------
Ran 4 tests in 0.060s
OK
Destroying test database for alias 'default'...
测试通过,并且通过单元测试,我们发现并解决了一个bug。