不久前,山姆鍋開始學習 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 來測試過。

解決方法

山姆鍋採用的解決方法是直接繼承 TestCaseLiveServerTestCase 這兩個類別,繼承 [TestCase` 使用資料庫交易來還原資料庫狀態, 使用 LiveServerTestCase 來設定測試用的 WSGI 服務器。這個新的類別很沒有創意地叫做 FunctionalTest,原始碼如下:

django_testcase.pyview raw
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
# -*- 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='[email protected]'
)
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