Python中的單元測試
我們先來回顧一下Python中的單元測試方法。
下面是一個 Python的單元測試簡單的例子:
假如我們開發一個除法的功能,有的同學可能覺得很簡單,代碼是這樣的:
1
2
|
def division_funtion(x, y): return x / y |
但是這樣寫究竟對還是不對呢,有些同學可以在代碼下面這樣測試:
1
2
3
4
5
6
7
8
|
def division_funtion(x, y): return x / y if __name__ = = '__main__' : print division_funtion( 2 , 1 ) print division_funtion( 2 , 4 ) print division_funtion( 8 , 3 ) |
但是這樣運行后得到的結果,自己每次都得算一下去核對一遍,很不方便,Python中有 unittest 模塊,可以很方便地進行測試,詳情可以文章最后的鏈接,看官網文檔的詳細介紹。
下面是一個簡單的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import unittest def division_funtion(x, y): return x / y class TestDivision(unittest.TestCase): def test_int( self ): self .assertEqual(division_funtion( 9 , 3 ), 3 ) def test_int2( self ): self .assertEqual(division_funtion( 9 , 4 ), 2.25 ) def test_float( self ): self .assertEqual(division_funtion( 4.2 , 3 ), 1.4 ) if __name__ = = '__main__' : unittest.main() |
我簡單地寫了三個測試示例(不一定全面,只是示范,比如沒有考慮除數是0的情況),運行后發現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
F.F ====================================================================== FAIL: test_float (__main__.TestDivision) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/tu/YunPan/mydivision.py", line 16, in test_float self.assertEqual(division_funtion(4.2, 3), 1.4) AssertionError: 1.4000000000000001 != 1.4 ====================================================================== FAIL: test_int2 (__main__.TestDivision) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/tu/YunPan/1.py", line 13, in test_int2 self.assertEqual(division_funtion(9, 4), 2.25) AssertionError: 2 != 2.25 ---------------------------------------------------------------------- Ran 3 tests in 0.001s FAILED (failures=2) |
汗!發現了沒,竟然兩個都失敗了,測試發現:
4.2除以3 等于 1.4000000000000001 不等于期望值 1.4
9除以4等于2,不等于期望的 2.25
下面我們就是要修復這些問題,再次運行測試,直到運行不報錯為止。
譬如根據實際情況,假設我們只需要保留到小數點后6位,可以這樣改:
1
2
|
def division_funtion(x, y): return round ( float (x) / y, 6 ) |
再次運行就不報錯了:
1
2
3
|
... ---------------------------------------------------------------------- Ran 3 tests in 0.000s |
OK
Django中的單元測試
盡早進行單元測試(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
1
2
3
4
5
6
7
|
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: 執行指定的測試用例
用第三種方式執行上面提供的樣例,結果如下:
1
|
$ python manage.py test depotapp.SimpleTest |
1
2
3
4
5
6
7
|
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代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
#/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。如同預想的那樣,測試沒有通過:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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的相關部分如下:
1
2
3
4
5
|
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 |
然后再運行測試:
1
|
$ python manage.py test depotapp.ProductTest |
1
2
3
4
5
6
7
|
Creating test database for alias 'default'... .... ---------------------------------------------------------------------- Ran 4 tests in 0.060s OK Destroying test database for alias 'default'... |
測試通過,并且通過單元測試,我們發現并解決了一個bug。