不久前,山姆鍋開始學習 Django 這套 Web 應用框架,在不跳脫它既有有框架的情況下,運用它可以快速建構應用程式原型。差不多同一時間,也找到 Test-driven Development with Python 這本好書,書名雖然好像跟 Django 無關,但書中的範例程式是以 Django 為基礎。 可惜,本文不是要示範如何使用 Django 做功能測試,這個主題網路有許多文章可以參考。山姆鍋在使用 Django 的 LiveServerTestCase 時 發現資料庫的資料在第一個測試案例(test case)之後就會被清除,導致後續的測試無法正常運作。
Django 的功能性測試(或者稱用戶驗收測試)最常使用 Selenium
來驅動瀏覽器,並利用 django.test.LiveServerTestCase
在背景建立測試資料庫以及 WSGI 服務器。
如此,完成一個端到端(end-to-end)的測試環境,基本上可以模擬大多數用戶跟應用之間的互動情境。
這麼完整的支援,原本是該多麽理想啊! 本文的解決方法應該只適用 SQLite
3 。
執行環境
在說明山姆鍋遇到的問題前,需要先列出我的測試環境:
- Python 2.7.x
- Django 1.8.3
- SQLite 3
- OS: Mac OS X
注意使用的是 SQLite 3 資料庫。
問題描述
除了 LiveServerTestCase
支援功能測試外, Django
也對單元測試(unit test)提供有 django.test.TestCase
來協助撰寫。 TestCase
利用資料庫對於交易(transaction)的支援,在每次測試案例之後,還原資料庫到案例開始前的狀態,這樣來達到案例之間的分隔(isolation)目的。
基於一個山姆鍋還不了解的原因,畢竟山姆鍋對於 Django
還沒有那麼多經驗,LiveServerTestCase
不是使用資料庫的交易來還原狀態,
而是對資料庫進行清除動作,就是這個神秘的清除動作,導致在功能測試時只有第一個執行的案例能夠正常,
後續用到資料庫的案例都會發生讀不到資料的情況。
有人建議功能測試不要使用 SQLite 3,使用 MySQL 或者 PostgreSQL, 原因不外乎 SQLite 3 在功能測試不穩定等等。 可是山姆鍋打算開發的應用就需要使用 SQLite 3 啊!所以,並沒有針對 MySQL 或者 PostgreSQL 來測試過。
解決方法
山姆鍋採用的解決方法是直接繼承 TestCase
跟 LiveServerTestCase
這兩個類別,繼承
[TestCase`使用資料庫交易來還原資料庫狀態, 使用 LiveServerTestCase
來設定測試用的 WSGI
服務器。這個新的類別很沒有創意地叫做 FunctionalTest
,原始碼如下:
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'conf.settings.test')
import time
from django.core import signals
from django.test.testcases import LiveServerTestCase, TestCase
from django.contrib.auth import get_user_model
from django.contrib.staticfiles.handlers import StaticFilesHandler
class FunctionalTest(LiveServerTestCase, TestCase):
static_handler = StaticFilesHandler
@classmethod
def setUpClass(cls):
"""
Django's LiveServerTestCase setupClass but without sqlite :memory check
and with additional Ghost initialization.
"""
default_address = os.environ.get('DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8090-9000')
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = default_address
# Prevents transaction rollback errors from the server thread. Removed
# as from Django 1.8.
try:
from django.db import close_connection
signals.request_finished.disconnect(close_connection)
except ImportError:
pass
super(FunctionalTest, cls).setUpClass()
@classmethod
def tearDownClass(cls):
"""
Django's LiveServerTestCase tearDownClass, but without sqlite :memory
check and shuts down the Ghost instance.
"""
super(FunctionalTest, cls).tearDownClass()
def setUp(self):
get_user_model().objects.create_superuser(
username='demo',
password='demo',
email='demo@localhost'
)
super(FunctionalTest, self).setUp()
def tearDown(self):
super(FunctionalTest, self).tearDown()
def sleep(self, secs):
time.sleep(secs)
結語
Django LiveServerTestCase 的這個問題困擾山姆鍋一陣子,網路上也沒有太多有用的資料。 本文的解法是參考 Fast functional testing with Django and Ghostrunner 這篇文章。 必須老實講,山姆鍋不知道這個問題的確切原因,也不知道有沒有其他解法,所以感覺真是不踏實。不管如何,希望對跟我遇到相同問題的人有所幫助。
參考資料
_`Django`: https://www.djangoproject.com/ ⎘
_`Test-driven Development with Python`: http://shop.oreilly.com/product/0636920029533.do ⎘
_`Selenium`: https://selenium-python.readthedocs.org/ ⎘
_`SQLite 3`: https://docs.python.org/2/library/sqlite3.html ⎘
_`Fast functional testing with Django and Ghostrunner`: https://wearespindle.com/articles/fast-functional-testing-with-django-and-ghostrunner/ ⎘
_`原始的 LiveServerTestCase 實作`: https://github.com/wearespindle/ghostrunner/blob/master/ghost/test/testcases.py ⎘