Python專案維護不了?可能是測試沒到位。Django的單元測試和整合測試初探

程序设计实验室發表於2024-03-06

前言

好久沒搞 Django 了,最近維護一個我之前用 Django 開發的專案竟然有親切的感覺😂

測試,在以前確實是經常被忽略的話題,特別是對於 Python Web 這種快速開發框架,怎麼敏捷怎麼來,快速開發快速上線,而不是慢工出細活做得很規範,往往也是因為這種粗狂的開發風格,導致專案後續難以維護,這時候再給 Python 冠上一個開發容易維護難的名字。

Python: 我不背這鍋😒

說回正題,這次的測試包括兩部分,在 Django 專案內部寫單元測試和整合測試,保證專案功能正常,然後我還開發了一個獨立的自動測試工具,可以根據 OpenAPI 的文件來測試,並且在測試完成後輸出測試報告,報告內容包括每個介面是否測試透過和響應時間等。

這個工具我使用了 go 語言開發,主要是考慮到了 go 語言可以傻瓜式的實現交叉編譯,生成的可執行檔案直接上傳到伺服器就可以執行,非常方便。

這個自動測試工具我會在下一篇檔案介紹。

Django 的測試

不得不說 Django 的文件寫得真不錯👍

我看了一會文件就開始寫測試,Django 全家桶真的舒服,開發體驗太絲滑了

使用 startapp 建立 app 的時候,每個 app 目錄下都有個 tests.py 檔案,我們的測試程式碼就寫在這個檔案裡面好了。

如果測試程式碼很多的話,還可以拆分,如何拆分參考 views.py 的拆分,把 tests.py 改成 package ,即建立個 tests 目錄,下面放各個測試檔案,然後在 __init__.py 裡引入。

測試分為單元測試和整合測試,在 Django 裡寫單元測試比 AspNetCore 舒服多了,不用考慮依賴注入的問題,Django 全給你處理好了。

在測試的時候,Django 會自動建立測試資料庫,因此也不用自己去折騰環境隔離啥的。

關於這倆種測試的區別,我之前的文章裡有介紹,就不復制貼上了。

Asp-Net-Core學習筆記:單元測試和整合測試

例子

這次以兩個 app 為例

  • config - 單元測試
  • dashboard - 整合測試

在 Django 裡這倆種測試都沒啥心智負擔,也不用啥額外的操作,直接在 tests.py 裡寫就完事了。

單元測試

以 config app 作為單元測試的例子

我封裝了一個 ConfigService 程式碼如下

from datetime import date
from .models import CommonConfig


class ConfigService(object):
    def __init__(self):
        ...

    @staticmethod
    def get_config(key: str) -> str:
        queryset = CommonConfig.objects.filter(key=key)
        if queryset.exists():
            return queryset.first().value
        return ''

    @property
    def today(self):
        return date.today()

    @property
    def start_year(self):
        value = self.get_config('start_year')
        return str(self.today.year) if len(value) == 0 else value

    @property
    def start_month(self):
        value = self.get_config('start_month')
        return str(self.today.month) if len(value) == 0 else value

    @property
    def end_year(self):
        value = self.get_config('end_year')
        return str(self.today.year) if len(value) == 0 else value

    @property
    def end_month(self):
        value = self.get_config('end_month')
        return str(self.today.month) if len(value) == 0 else value

編輯 apps/config/tests.py

from django.test import TestCase
from apps.config.models import CommonConfig
from apps.config.services import ConfigService


class CommonConfigTestCase(TestCase):
    def setUp(self):
      CommonConfig.objects.create(key='start_year', value='2023')
      CommonConfig.objects.create(key='end_year', value='2024')
      CommonConfig.objects.create(key='start_month', value='1')
      CommonConfig.objects.create(key='end_month', value='10')

    def test_common_config(self):
      cfg = ConfigService()
      self.assertEqual(cfg.start_year, '2023')
      self.assertEqual(cfg.end_year, '2024')
      self.assertEqual(cfg.start_month, '1')
      self.assertEqual(cfg.end_month, '10')

整合測試

整合測試是模擬 HTTP 請求去訪問介面,看看介面正不正常。

Django 的 TestCase 類裡自帶了 client 屬性,可以很方便的請求介面。

一般介面都要登入才能用,然後 client 裡也很貼心的整合了 Django 的認證授權體系,直接 login 就完事了。

注意測試的時候是自動建立了臨時資料庫,所以得先新增使用者。

接著呼叫 self.client.get, self.client.post 之類的方法去測試介面就好了。

from django.test import TestCase
from django.shortcuts import reverse
from django.contrib.auth.models import User
from rest_framework import status


class DashboardTests(TestCase):
    def setUp(self):
        User.objects.create_user(username='user', password='pwd')
        self.client.login(username="user", password="pwd")

    def test_overview(self):
        resp = self.client.get(reverse('dashboard:overview'), {'grant_year': 2023, 'grant_month': 2})
        self.assertEqual(resp.status_code, status.HTTP_200_OK)
        self.assertEqual(resp.json()['code'], status.HTTP_200_OK)

    def test_monthly_data(self):
        resp = self.client.get(reverse('dashboard:monthly_data'), {'grant_year': 2023, 'grant_month': 2})
        self.assertEqual(resp.status_code, status.HTTP_200_OK)
        self.assertEqual(resp.json()['code'], status.HTTP_200_OK)

    def test_county_data(self):
        resp = self.client.get(reverse('dashboard:county_data'), {'grant_year': 2023, 'grant_month': 2})
        self.assertEqual(resp.status_code, status.HTTP_200_OK)
        self.assertEqual(resp.json()['code'], status.HTTP_200_OK)

執行測試

測試寫完之後

使用命令執行測試

python manage.py test

還有其他引數請參考官方文件

不過執行測試的時候就沒有 AspNetCore 爽了,沒有那種一個個介面相繼亮起綠燈的快感;

Django 的測試只能看到測試結果,有多少個測試透過了,如果有報錯會看到 Traceback 資訊。

$ python .\manage.py test 
Found 5 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.831s

OK
Destroying test database for alias 'default'...

小結

Django 還是熟悉的味道,好用就對了。

參考資料

沒想到不知不覺中 Django 刷版本號到 5.0 了…

  • https://docs.djangoproject.com/zh-hans/5.0/topics/testing/

相關文章