Skip to content

解決 Django 功能測試中資料會被清除的問題

Published: 4 分鐘

不久前,山姆鍋開始學習 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,原始碼如下:

# -*- 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

郭信義 (Sam Kuo)

奔騰網路科技技術長,專長分散式系統、Web 應用與雲端服務架構、設計、開發、部署與維運。工作之餘,喜歡關注自由軟體的發展與應用,偶爾寫一下部落格文章。

你可能會有興趣的文章