第十五章 測試(一)

編寫單元測試主要有2個目的:

  1. 實現新功能時能夠確保新添加的代碼按預期方式運行;
  2. 每次修改應用後,運行單元測試能確保現有代碼的功能沒有迴歸,即新改動沒有影響代碼的正常運行;

       從一開始我們就爲Flasky應用編寫了單元測試,檢查數據庫模型類有沒有實現功能。由於模型類很容易在運行中的應用上下文之外進行測試,故應用中我們將絕大多數業務邏輯都放在了模型類中實現,視圖函數中的代碼只起到粘合劑的作用。爲模型類編寫單元測試,可以覆蓋絕大多數代碼。本章我們將再介紹2中單元測試的類別:

  1. 使用Flask客戶端測試視圖函數
  2. 使用selenium進行端到端的測試

       我們使用包來組織測試,tests包中,各模塊都以test_*開頭。unittest支持自動發現測試,執行python -m unittest discover -v運行測試時,unittest默認會從當前目錄開始尋找以test_*.py模式命名的模塊,然後運行其中的測試。

       視圖函數只在在請求上下文和運行的應用裏運行。Flask內建了一個測試客戶端用於解決(至少部分解決)這一問題。測試客戶端能復現應用運行在Web服務器中的環境,讓測試充當客戶端來發送請求。

tests/test_client.py:使用Flask測試客戶端編寫的測試框架

import re
import unittest
from app import create_app, db
from app.models import User, Role


class FlaskClientTestCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app('testing')
        self.app_context = self.app.app_context()
        self.app_context.push()
        # 應用上下文激活以後,纔可以使用db
        db.create_all()
        Role.insert_rows()
        self.client = self.app.test_client(use_cookies=True)

    def tearDown(self):
        db.drop_all()
        self.app_context.pop()

    def test_home_page(self):
        response = self.client.get('/')
        self.assertTrue(response.status_code, 200)
        self.assertIn(b'Stranger', response.data)

       在執行測試時是沒有Flask上下文存在的。但有些行爲又依賴於程序上下文或請求上下文才能正確進行。比如,Flask-SQLAlchemy中用來清除數據庫會話的db.session.remove()調用通過teardown_appcontext裝飾器註冊,而這個函數只會在程序上下文銷燬時纔會觸發。

       另外,當時用工廠函數創建程序時,我們使用current_app來操作程序實例。事實上,除了我們程序中使用的代碼,擴展的代碼中也會使用current_app。比如,Flask-SQLAlchemy需要從程序實例獲取配置信息。直接創建程序實例,並在實例化SQLAlchemy類時傳入程序實例時,Flask-SQLAlchemy會直接從這個程序實例app對象獲取配置信息。當使用工廠函數創建程序實例並使用init_app()初始化程序後,Flask-SQLAlchemy則會從current_app對象獲取程序配置信息。

       使用app_context()和test_request_context()方法可以手動激活程序上下文和請求上下文。

       新增的self.client實例變量即Flask測試客戶端對象,在這個對象上調用請求方法嚮應用發起請求。如果創建測試客戶端時啓用了use_cookies選項,這個客戶端就能像瀏覽器一樣接收和發送cookie。

       test_home_page()演示了測試客戶端的使用,向根路徑發送GET請求後,我們先檢查響應的狀態碼,然後使用response.data獲取響應體,類型爲字節串,所以使用b'Stranger'。使用response.get_data(as_text=True)可以獲取字符串形式。

       測試客戶端還可以使用POST方法發送包含表單數據的POST請求。但需要注意,Flask-WTF生成的表單中包含一個隱藏字段,其內容是CSRF令牌。爲了發送CSRF令牌,測試必須請求表單所在的頁面,然後解析響應返回的HTML代碼,提取CSRF令牌。簡單起見,最好在測試環境中禁用CSRF保護機制。

config.py 在測試配置中禁用CSRF保護機制

class TestingConfig(Config):
    TESTING = True
    # 未指定環境變量時,測試換將將使用內存數據庫sqlite
    SQLALCHEMy_DATABASE_URI = os.environ.get('TEST_DATABASE_URI') or 'sqlite://'
    WTF_CSRF_ENABLED = False

tests/test_client.py:使用Flask測試客戶端模擬新用戶註冊的整個流程

class FlaskClientTestCase(unittest.TestCase):
    ... ...
        def test_register_and_login(self):
        # register a new account
        response = self.client.post('/auth/register', data={
            'email': '[email protected]',
            'username': 'Bob',
            'password': '123',
            'password2': '123',
        })
        self.assertEqual(response.status_code, 302)

        # login with the new account
        response = self.client.post('/auth/login', data={
            'email': '[email protected]',
            'password': '123'
        }, follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        self.assertTrue(re.search(b'Hello,\s+Bob!', response.data))
        self.assertTrue(b'You have not confirmed your account yet.' in response.data)

        # send a confirmation token
        user = User.query.filter_by(email='[email protected]').first()
        token = user.generate_confirmation_token()
        response = self.client.get('/auth/confirm/{}'.format(token), follow_redirects=True)
        self.assertTrue(response.status_code, 200)
        self.assertTrue(b'You have confirmed your account. Thanks!' in response.data)

        # logout
        response = self.client.get('/auth/logout', follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        self.assertTrue(b'You have been logged out.' in response.data)

       這個測試先向註冊路由提交一個表單。post方法的data參數是一個字典,包含表單中的各字段,字段的名稱和表單字段的name屬性保持一致。由於CSRF保護機制已經在配置中禁用了,因此無需和表單數據一起發送。爲了確認註冊成功,測試檢測響應的狀態碼是否爲302,表示重定向。

       接下來使用剛剛註冊的email登錄應用,調用post方法時指定了參數follow_redirects=True,讓客戶端像瀏覽器那樣,自動向重定向的URL發起GET請求。指定這個參數後,返回的不是302狀態碼,而是請求重定向的URL返回的響應。

       成功登錄後的響應應該是一個頁面,顯示一個包含用戶名的歡迎消息,並提醒用戶去確認賬戶。值得注意的是在檢查“Hello Bob”時,由於字符串由靜態部分和動態部分組成,Jinjia2模板生成最終的HTML會在中間加上額外的空格。因此我們使用正則進行匹配。

       在進行確認賬戶操作時,我們忽略了註冊時生成的令牌,直接調用User模型相關方法產生新令牌,構建URL進行確認。向這個包含令牌的URL發起GET請求,這個請求的響應是重定向到首頁,故這裏再次指定了參數follow_redirects=True。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章