前言

不久前 , 山姆鍋開始學習 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='[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 這篇文章 。 必須老實講 , 山姆鍋不知道這個問題的確切原因 , 也不知道有沒有其他解法 , 所以感覺真是不踏實 。 不管如何 , 希望對跟我遇到相同問題的人有所幫助 。


知識不會因為傳播而減少,喜歡這篇文章請幫忙分享。


本篇文章由 Sampot (山姆鍋) 發表,下面是有關他的連結:

評論

您的反饋是我寫作的最大動力,歡迎參與討論。P.S. 我會優先回答張貼在這裡的問題。

comments powered by Disqus