本章節我們再來說說測試,單元測試和功能測試。單元測試我們在數據驗證章節簡單提過了,本章我們進一步如何用單元測試來測試view的功能代碼;同時,也涉及一下基於selenium的功能測試做法。筆者過去的項目上常規的功能測試都是由測試人員通過人工點擊按鈕的方式來完成的,這里我們利用selenium來完成,大家體會一下當功能測試可以回歸的時候是啥趕腳。
1.1. 單元測試覆蓋task_start函數
參考前面的單元測試例子,完成test_task_start任務下達的單元測試代碼,斷言狀態是否變更為下達狀態。
... def test_task_start(self): """ 測試任務下達. """ #1構建模擬任務 data={'TaskNum':100,'Source':'101','Target':'05-01-01','Barcode':'101001001008','State':1,'Priority':1,} task = Task(**data) #task.save() #2創建業務類對象,並調用任務分解函數 taskBiz=TaskBiz() taskBiz.task_decompose(task,None)#① taskBiz.task_start(task)#② self.assertEqual(task.State,Task.STATE_RUNNING)
①:執行任務分解。
②:執行任務下達,斷言任務下達是否滿足狀態控制要求。
通過IDE的快捷菜單進入到cmd命令行運行單元測試,VS IDE 的Test Explore筆者用下來不是非常好用,有的時候單元測試函數刷新不出來。
命令行單元測試執行效果
it's ok!
1.2. selenium功能測試
通過編寫模仿用戶操作的 Selenium 測試腳本,可以從終端用戶的角度來測試應用程序,就像真實用戶所做的一樣。與通常的測試人員通過人工操作的方式,采用Selenium 確實能夠帶來效率的大幅度提升,尤其新版本發布回歸測試的時候!
1.2.1. python環境安裝Selenium
鍵入selenuim,點擊安裝鏈接
1.2.2. 安裝chromedriver驅動
如果要使用WebDriver在Chrome瀏覽器上進行測試時,需要從安裝chromedriver驅動程序。下載網址:http://chromedriver.storage.googleapis.com/index.html
筆者寫這篇的本機環境如下:
下載對應版本的chromedriver驅動文件后,下載后把文件解壓,然后放到本機chrome瀏覽器文件路徑里即可,如:C:\Program Files (x86)\Google\Chrome\Application
1.3. 添加functional_test.py功能測試代碼
先加入簡單的測試腳本代碼,打開客戶端任務列表,判斷當前瀏覽器窗口標題是否滿足斷言值——“任務清單”。
from unittest import TestCase import django from selenium import webdriver class FunctionalTest(TestCase): @classmethod def setUpClass(cls): #① super(FunctionalTest, cls).setUpClass() django.setup() cls.browser=webdriver.Chrome() cls.live_server_url = 'http://localhost:8001/task/' @classmethod def tearDownClass(cls): #② cls.browser.quit() def test_task_list(self): #③ self.browser.get(self.live_server_url ) self.browser.maximize_window() self.browser.implicitly_wait(3)#④ #假定網頁應該包含“任務列表”的標題 self.assertIn('任務列表',self.browser.title)
①:單元測試類初始化函數,執行測試時,只初始化執行一次,我們把測試需要准備的一些數據放在這里初始化。
②:單元測試類銷毀函數,執行測試時,只銷毀執行一次
③:訪問任務列表url,並斷言窗口標題是否包含“任務列表”
④:使用隱式等待3秒鍾,如果selenium 提前獲得返回,會提前結束等待。
命令行運行測試:
D:\my tfs\IndDemo>python manage.py test Task.functional_test System check identified no issues (0 silenced). DevTools listening on ws://127.0.0.1:64721/devtools/browser/0c65b5e3-1000-4233-a746-30b9142532fa F ====================================================================== FAIL: test_task_list (Task.functional_test.FunctionalTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "D:\my tfs\IndDemo\Task\functional_test.py", line 26, in test_task_list self.assertIn('任務列表',self.browser.title) AssertionError: '任務列表' not found in ''
---------------------------------------------------------------------- Ran 1 test in 6.486s FAILED (failures=1) D:\my tfs\IndDemo>
這里我們演示一下測試驅動開發里,小步的推進的原則,先添加代碼滿足這個測試條件,然后再運行測試,滿足當前測試了,添加新的測試斷言,再添加新的代碼來滿足這個測試斷言。測試驅動的開發對於開發新手來說確實會帶來很多好處,就是不用每次考慮夠多功能點,積硅步而至千里。
<!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset="utf-8" /> <title>任務列表</title> </head> <body> ...
模板文件:tasks.html
運行功能測試:
D:\my tfs\IndDemo>python manage.py test Task.functional_test System check identified no issues (0 silenced). DevTools listening on ws://127.0.0.1:65125/devtools/browser/94e6664d-cda5-41e1-adc7-b9820cb73241 . ---------------------------------------------------------------------- Ran 1 test in 5.677s OK D:\my tfs\IndDemo>
這次我們收獲了一個“ok”!
1.4. 場景功能測試
本例客戶端功能測試場景中,假定WCS客戶端不能手動增加新任務,只能夠查看任務列表和詳情,和源地址和目標地址的修改操作,以及執行分解、下達操作等。下面最后完成功能測試代碼:
from unittest import TestCase import django from django.test import LiveServerTestCase from time import sleep from selenium import webdriver from selenium.webdriver.common.keys import Keys from Task.TaskBiz import Task class FunctionalTest(LiveServerTestCase): @classmethod def setUpClass(cls): super(FunctionalTest, cls).setUpClass() django.setup() cls.browser=webdriver.Chrome() #cls.live_server_url = 'http://localhost:8001/task/' #1初始化測試任務1 data={'TaskNum':200,'Source':'101','Target':'05-01-01','Barcode':'101001001008','State':1,'Priority':1,} task = Task(**data) task.save() #2初始化測試任務2 data={'TaskNum':201,'Source':'102','Target':'05-01-02','Barcode':'101001001009','State':1,'Priority':1,} task2 = Task(**data) task2.save() @classmethod def tearDownClass(cls): #② cls.browser.quit() def test_task_list(self): #③ #print(self.live_server_url + '/task/') self.browser.get(self.live_server_url + '/task/' ) self.browser.maximize_window() self.browser.implicitly_wait(3)#④ #假定網頁應該包含“任務列表”的標題 self.assertIn("任務列表",self.browser.title) #獲取table並斷言table row 里是否包含初始化的任務數據 table = self.browser.find_element_by_id('id_task_table') #table = self.browser.find_elements_by_tag_name('table') rows = table.find_elements_by_tag_name('tr') #表標題行 self.assertIn("任務號",rows[0].text) #表第一行數據 self.assertIn('200',rows[1].text) self.assertIn('101001001008',rows[1].text) #表第二行數據 self.assertIn('201',rows[2].text) #對第一個執行任務分解操作 self.browser.find_element_by_id('1-decompose').click() self.browser.implicitly_wait(3) #sleep(3) table = self.browser.find_element_by_id('id_task_table') rows = table.find_elements_by_tag_name('tr') #表第一行數據包含子任務數 10 self.assertIn('10',rows[1].text) self.assertIn('處理成功',rows[1].text) #對第一個執行任務下達操作 self.browser.find_element_by_id('1-start').click() self.browser.implicitly_wait(3) table = self.browser.find_element_by_id('id_task_table') rows = table.find_elements_by_tag_name('tr') #表第一行數據包含子任務數 10 self.assertIn('執行中',rows[1].text) #修改第二個任務 self.browser.find_element_by_id('2-change').click() self.browser.implicitly_wait(3) self.browser.find_element_by_name('source').send_keys('111') self.browser.find_element_by_name('target').send_keys('05-01-11') self.browser.find_element_by_name('target').send_keys(Keys.ENTER) self.browser.implicitly_wait(3) table = self.browser.find_element_by_id('id_task_table') rows = table.find_elements_by_tag_name('tr') self.assertIn('111',rows[2].text) self.assertIn('05-01-11',rows[2].text)
運行功能測試:
D:\my tfs\IndDemo>python manage.py test Task.functional_test Creating test database for alias 'default'... System check identified no issues (0 silenced). DevTools listening on ws://127.0.0.1:51015/devtools/browser/82f971f5-83ba-4b55-bc19-ff700e7aedb1 E---------------------------------------- ---------------------------------------- ====================================================================== ERROR: test_task_list (Task.functional_test.FunctionalTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "D:\my tfs\IndDemo\Task\functional_test.py", line 59, in test_task_list self.browser.find_element_by_id('1-decompose').click() File "C:\Python\Python36-32\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 360, in find_element_by_id return self.find_element(by=By.ID, value=id_) File "C:\Python\Python36-32\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 978, in find_element 'value': value})['value'] File "C:\Python\Python36-32\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 321, in execute self.error_handler.check_response(response) File "C:\Python\Python36-32\lib\site-packages\selenium\webdriver\remote\errorhandler.py", line 242, in check_response raise exception_class(message, screen, stacktrace) selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"[id="1-decompose"]"} (Session info: chrome=90.0.4430.72) ---------------------------------------------------------------------- Ran 1 test in 8.513s FAILED (errors=1) Destroying test database for alias 'default'... D:\my tfs\IndDemo>
代碼“self.browser.find_element_by_id('1-decompose').click()”這句代碼找不到相應的id='1-decompose'的html element。 因此,需要改進一下模板代碼如下:
... {% for task in tasks %} <tr> <td>{{task.TaskId }}</td> <td>{{task.TaskNum}}</td> <td>{{task.Source}}</td> <td>{{task.Target}}</td> <td>{{task.Barcode}}</td> <td>{{task.get_State_display}}</td> <td>{{task.get_Priority_display}}</td> <td>-</td> <td>-</td> <td>{{task.job_set.count}}</td> <td><a id="{{task.TaskId}}-decompose" href="{{task.TaskId }}/decompose/">分解</a> <a id="{{task.TaskId}}-start" href="{{task.TaskId }}/start/">下達</a> <a id="{{task.TaskId}}-change" href="{{task.TaskId }}/change/">修改</a></td> </tr> {%endfor%} ...
相對於每行的操作鏈接賦值一個唯一的id 值,重新運行功能測試:
D:\my tfs\IndDemo>python manage.py test Task.functional_test Creating test database for alias 'default'... System check identified no issues (0 silenced). DevTools listening on ws://127.0.0.1:51636/devtools/browser/89f5a372-fbe3-4ecd-b581-07d813d56c55 . ---------------------------------------------------------------------- Ran 1 test in 6.224s OK Destroying test database for alias 'default'... D:\my tfs\IndDemo>
功能測試運行通過,接下來我們進一步完善單元測試。
1.5. 單元測試覆蓋view
Django test 也可以針對發布的url進行單元測試,從而覆蓋url和view代碼,下面我在Task/tests.py里增加 class TaskViewTest(TestCase) 類專門測試發布的url是否符合開發預期,測試代碼如下:
... class TaskViewTest(TestCase): """Tests for the application views.""" # Django requires an explicit setup() when running tests in PTVS @classmethod def setUpClass(cls): super(TaskURLTest, cls).setUpClass() django.setup() #1初始化測試任務1 data={'TaskNum':200,'Source':'101','Target':'05-01-01','Barcode':'101001001008','State':1,'Priority':1,} task = Task(**data) task.save() #2初始化測試任務2 data={'TaskNum':201,'Source':'102','Target':'05-01-02','Barcode':'101001001009','State':1,'Priority':1,} task2 = Task(**data) task2.save() def test_task_change(self): data={'source':'111','target':'05-01-11'} #更新第一個task的源和目標值 response=self.client.post('/task/1/change/',data) model = Task.objects.get(pk=1) self.assertEqual(model.Source,'111') self.assertEqual(model.Target,'05-01-11') response=self.client.get('/task/') self.assertIn('111',response.content.decode()) self.assertTemplateUsed(response,'Task/tasks.html') def test_task_decompose(self): response=self.client.get('/task/1/decompose/') model = Task.objects.get(pk=1) self.assertEqual(model.job_set.count(),10) def test_task_decompose(self): self.client.get('/task/1/decompose/') model = Task.objects.get(pk=1) self.assertEqual(model.job_set.count(),10) self.client.get('/task/1/start/') model = Task.objects.get(pk=1) self.assertEqual(model.State,Task.STATE_RUNNING)
目前,Task APP 單元測試覆蓋了所有發布的url,項目迭代推進過程中,新的改動會不會導致bug,回歸運行一把單元測試,如果“紅”了一片,趕緊回滾改動的代碼。
1.6. 小結
django的單元測試每次運行都是重新構建數據庫和銷毀數據庫,所以不用擔心測試數據重復或狀態的問題,數據每次運行都是按照測試邏輯來執行的。尤其功能測試基於LiveServerTestCase時,這個特點簡直“香”得不要不要的。傳統人工操進行的功能測試每次數據准備都夠忙一陣子的,這一點也是筆者使用django爽點之一。