盡早進行單元測試(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。