Django 筆記 – django.test.TestCase 不同 test method 產生的 DB 資料互相隔離
一個經驗紀錄,提供給不知道而撞牆的人。
知道的人則知道是因為 django.test.TestCase 有被 atomic() 包起來。
- 注意: 這與 setUpCalss、 setUpTestData 要處理的事情不同,setUpCalss、 setUpTestData 是在 class-level 執行一次的動作,可以在這邊 INSERT 一些 DB 資料,提供給 class 內所有的 method 共享使用,所以是在解決『不同 method 之間共享某些預先建立好的 DB 資料』的問題,而不是在解決『不同 method 產生的資料互相隔離』的問題。
django.test.TestCase 測試 CRUD API
在大部分的 Django Test 起手式,可能多以 Writing and running tests 為基礎,先嘗試使用 django.test.TestCase 。
可能會寫一些單元測試,也可能用 django.test.Client 或 rest_framework.APIClient 去做一些整合測試。
當我們開始寫一些 CRUD 的測試 API 測試時,可能會很自然地,想在同一個 test class 內把 CRUD 做一輪。然後,可會考慮把 CRUD 四個動作安排在四個或更多個 function 內。
然後 … 就撞牆了 …
不同 test method 之間所產生的 DB 資料,似乎是互相隔離的
可能會發現:奇怪,怎麼在第一個 method 先 call api Create 內容後,到了後面 test method 想 call api 去 Read、Update、Delete 那個內容時,卻是 404 ?
仔細追蹤,發現 Django get Query 的結果 raise 出 DoesNotExist
?所以資料不見了?
再進一步測試,發現 primary key 的 sequence 是有增加的,確定之前的 INSERT 動作曾經存在過,只是資料消失了,有一種不同 test method 之間資料互相隔離之感。
原因已在本文開頭寫了,就是 django.test.TestCase 有被 atomic() 包起來,class-level 一層, method level 一層,詳細資料直接看 django.test.TestCase 相關段落有寫,以下把最重要那句摘錄出來:『Wraps the tests within two nested atomic() blocks: one for the whole class and one for each test.』
所以如果多個 test method 之間互有依賴的(或者說是他們所需的 DB 資料互有依賴),就不能直接用 django.test.TestCase,或者,所有的 API 動作都要寫到同一個 method 內,這樣就可以運作。
解法嘗試
嘗試一 – override tearDownClass() 之類的
因為查看了一下,diango.test.TestCase.tearDownClass() 內似乎有 atomic() 相關動作。
所以,嘗試:
- 繼續使用 django.test.TestCase
- 嘗試 override tearDownClass()
- 用 super(TestCase. self).method() 的方式去執行更上一層 parent class 的相關 method也就是設法只跳過 diango.test.TestCase.tearDownClass(),但保留其 parent 的 tearDownClass()
實驗了一下,似乎資料還是被隔離,其實這也很合理,因為 tearDownClass 應該是 class-level 的動作,所以應該處理不了『不同 method 之間』。
這裡沒有進一步去實驗分析真正做 atomic() 的地方,先嘗試別的方式。
嘗試二 – override 更多 method
參考這篇 Disabling Atomic Transactions In Django Test Cases ,override 到它所提的四個 method,包含 tearDownClass() 在內
這次好像過度 override,引發了其他的錯誤 – 原本的 class 內我有設置一些變數要提供不同 method 之間共用,這些變數反而消失了。
這裡沒有進一步追蹤、實測或嘗試下去,暫時先放棄 override,畢竟,不熟悉的情況下去 override 本來就很危險,可能會誤殺重要的其他動作。
嘗試三 – 使用其他 test class
django.test.TransactionTestCase
這個 class 一樣有包裝類似的清除資料動作,只是是用 truncate,文件內有提到是在每個 test 開始時,摘錄最重要的一句:『Resetting the database to a known state at the beginning of each test to ease testing and using the ORM.』
實戰了一下,不同 test method 之間資料也是隔離的。
這裡沒有進一步嘗試下去,放棄這個 test class。
django.test.SimpleTestCase
這個 test class 預設不可做 DB query,所以我卡在產生假資料的地方,會 raise AssertionError: Database queries to ‘default’ not allowed in SimpleTestCase 之類的錯誤訊息。
雖然有依照文件把 databases 設定成 __all__ 之類的,但還是收到一樣的錯誤。
這裡沒有進一步嘗試下去,放棄這個 test class。
unittest.TestCase
實測可用,不同 method 所做的 DB 存取被保留了下來,因此可以達成原先的設想 – 在一個 test class 內寫多個 method,多個 method 之間 API 所做的 DB 動作可以互相依賴,因此 CRUD 或者更複雜的情境劇本也都可以進行。
當然,如果有個別 method 的 DB 資料不想保留,就得自己寫 tearDown() 清除了,並且要自行解決另一個問題 – tearDown 是每一個 method 結束時都會執行的動作,如果只有其中一個 method,那要怎麼做? (這裡暫時沒研究,不過猜想應該是要從某處或者 self 內,找出 method name 來,自己寫 if 做判斷)
結論
- 如果每個 method 之間的 DB 資料沒有要互相依賴,可以正常使用 django.test 內的 test class。
- 注意: 與 setUpCalss、 setUpTestData 要解決的事情不同
- 反之,可以考慮 Python 的 unittest.TestCase ,當然若此時需要清除 DB 資料的話,就得自己處理。
Leave a Reply