mcpField
F
frappe-testing-unit

frappe-testing-unit

github.com/impertio-studioテスト・品質198115 回インストール30 週間前に更新
リポジトリ

frappe-testing-unit について

Frappe開発者向けのユニットテストと統合テストのリファレンススキルです。バージョン別のテストクラス、フィクスチャ、テストパターン、状態管理に加え、実行やレポート生成のコマンドを扱います。

主な機能

  1. 01ユニットテストと統合テストのクラス
  2. 02テスト実行コマンドとオプション
  3. 03フィクスチャの設定パターン
  4. 04ドキュメントと権限のテスト
  5. 05テスト状態用コンテキストマネージャー
  6. 06データベース分離のガイド

活用シーン

  1. 01関数やメソッドの単体テスト
  2. 02ドキュメントのライフサイクル動作の検証
  3. 03権限とユーザーコンテキストの確認
  4. 04アプリテストの実行とレポート生成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/impertio-studio/frappe_claude_skill_package --skill frappe-testing-unit

インストール後、Claude Code で/frappe-testing-unitを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
.gitkeep
anti-patterns.md
api-reference.md
examples.md
fixtures.md

詳細

単体テストと統合テスト

クイックリファレンス

タスクコマンド/クラス
すべてのテストを実行bench --site test_site run-tests
アプリのテストを実行bench --site test_site run-tests --app myapp
DocType のテストを実行bench --site test_site run-tests --doctype "Sales Order"
単一のテストメソッドを実行bench --site test_site run-tests --doctype "Sales Order" --test test_submit
モジュールのテストを実行bench --site test_site run-tests --module "myapp.mymodule.doctype.mydt.test_mydt"
プロファイラーを使って実行bench --site test_site run-tests --doctype "Task" --profile
failfast を使って実行bench --site test_site run-tests --failfast
JUnit XML を生成bench --site test_site run-tests --junit-xml-output /path/report.xml
フィクスチャの読み込みをスキップbench --site test_site run-tests --skip-test-records --skip-before-tests
基底クラス(v14)from frappe.tests.utils import FrappeTestCase
単体テストクラス(v15 以降)from frappe.tests.classes import UnitTestCase
統合テストクラス(v15 以降)from frappe.tests.classes import IntegrationTestCase

判断フロー:どのテスト基底クラスを使うか?

Need to test a function or method in isolation?
├─ YES → Does it require database access?
│   ├─ NO → UnitTestCase (v15+) or FrappeTestCase (v14)
│   └─ YES → IntegrationTestCase (v15+) or FrappeTestCase (v14)
└─ NO → Need to test document lifecycle (create/submit/cancel)?
    ├─ YES → IntegrationTestCase (v15+) or FrappeTestCase (v14)
    └─ NO → Need to test permissions or user context?
        ├─ YES → IntegrationTestCase (v15+) or FrappeTestCase (v14)
        └─ NO → UnitTestCase (v15+) or FrappeTestCase (v14)

バージョンに関する注意:v14 では、FrappeTestCase のみが基底クラスです。v15 以降でも引き続き使用できます(非推奨のラッパー)が、新しいコードでは必ず UnitTestCase または IntegrationTestCase を優先してください。

テスト基底クラス

FrappeTestCase(v14:互換性ラッパーとして v15 以降でも引き続き使用可能)
from frappe.tests.utils import FrappeTestCase

class TestMyDoctype(FrappeTestCase):
    def test_something(self):
        doc = frappe.get_doc({"doctype": "My Doctype", "field": "value"})
        doc.insert()
        self.assertEqual(doc.field, "value")

動作:各テストの後に frappe.local.flags をリセットします。データベーストランザクションは各テストの前に開始され、その後ロールバックされます。setUpClass をオーバーライドする場合は、必ず super().setUpClass() を呼び出してください。

UnitTestCase(v15 以降):データベースアクセスなし
from frappe.tests.classes import UnitTestCase

class TestMyUtils(UnitTestCase):
    def test_calculation(self):
        result = my_calculation(10, 20)
        self.assertEqual(result, 30)

    def test_html_output(self):
        html = generate_html()
        self.assertEqual(self.normalize_html(html), self.normalize_html(expected))

動作:setUpClass で frappe.set_user("Administrator") を設定します。モジュールパスから DocType を自動検出します。normalize_html()、normalize_sql()、assertDocumentEqual()、assertQueryEqual()、assertSequenceSubset() を提供します。

IntegrationTestCase(v15 以降):データベースへのフルアクセス
from frappe.tests.classes import IntegrationTestCase

class TestSalesOrder(IntegrationTestCase):
    def test_submit_order(self):
        so = frappe.get_doc({
            "doctype": "Sales Order",
            "customer": "_Test Customer",
            "items": [{"item_code": "_Test Item", "qty": 1, "rate": 100}]
        }).insert()
        so.submit()
        self.assertEqual(so.docstatus, 1)

動作:UnitTestCase を拡張しています。frappe.init() を呼び出し、サイト接続をセットアップします。make_test_records() を通じてテストレコードの依存関係を読み込みます。primary_connection() と secondary_connection() のコンテキストマネージャーを提供します。maxDiff = 10_000 です。

テストファイルの構成

必ず、以下の命名規則に従って、テストファイルを DocType のディレクトリに配置してください。

myapp/
└── mymodule/
    └── doctype/
        └── my_doctype/
            ├── my_doctype.py          # DocType controller
            ├── my_doctype.json        # DocType definition
            ├── test_my_doctype.py     # Test file (MUST start with test_)
            └── test_records.json      # Optional: test fixtures

ルール:

  • テストランナーは test_ 接頭辞のないファイルを無視するため、テストファイルには必ず test_ を付けてください
  • DocType のテストには、必ず test_{doctype_in_snake_case}.py を使用してください
  • DocType 固有のテストファイルを DocType ディレクトリの外に配置しないでください
  • DocType 以外のテストは任意のモジュールに配置できますが、必ず test_*.py という命名規則に従ってください

テストフィクスチャ

方法 1:test_records.json(静的フィクスチャ)

DocType のディレクトリに test_records.json ファイルを作成します。

[
    {
        "doctype": "My Doctype",
        "field1": "_Test Value 1",
        "field2": 100
    },
    {
        "doctype": "My Doctype",
        "field1": "_Test Value 2",
        "field2": 200
    }
]

ルール:

  • 本番データと区別するため、テストデータの値には必ず _Test を付けてください
  • テストランナーは DocType のテスト実行前に、これらを自動で読み込みます
  • Link フィールドの依存関係は自動的に解決されます。テストランナーは、リンク先の DocType のレコードを先に作成します
方法 2:_test_records リスト(モジュール内フィクスチャ)
_test_records = [
    {"doctype": "My Doctype", "field1": "_Test Value 1"},
    {"doctype": "My Doctype", "field1": "_Test Value 2"},
]
方法 3:プログラムによるフィクスチャ(複雑なデータに推奨)
def create_test_data():
    if frappe.flags.test_data_created:
        return
    frappe.set_user("Administrator")
    frappe.get_doc({
        "doctype": "My Doctype",
        "field1": "_Test Value",
    }).insert()
    frappe.flags.test_data_created = True

class TestMyDoctype(IntegrationTestCase):
    def setUp(self):
        create_test_data()

テストメソッド間でフィクスチャが重複して作成されないよう、必ず frappe.flags を使用してください。

テストパターン

ドキュメントのライフサイクルをテストする
class TestInvoice(IntegrationTestCase):
    def test_full_lifecycle(self):
        # Create
        doc = frappe.get_doc({"doctype": "Sales Invoice", ...}).insert()
        self.assertEqual(doc.docstatus, 0)  # Draft

        # Submit
        doc.submit()
        self.assertEqual(doc.docstatus, 1)  # Submitted

        # Cancel
        doc.cancel()
        self.assertEqual(doc.docstatus, 2)  # Cancelled
権限をテストする
class TestPermissions(IntegrationTestCase):
    def test_user_cannot_read_private(self):
        frappe.set_user("[email protected]")
        doc = frappe.get_doc("Event", {"subject": "_Test Private Event"})
        self.assertFalse(frappe.has_permission("Event", doc=doc))

    def tearDown(self):
        # ALWAYS reset user in tearDown
        frappe.set_user("Administrator")
ユーザーコンテキストを使ったテスト(v15 以降のコンテキストマネージャー)
class TestAccess(IntegrationTestCase):
    def test_restricted_access(self):
        with self.set_user("[email protected]"):
            self.assertRaises(
                frappe.PermissionError,
                frappe.get_doc, "Salary Slip", "SAL-001"
            )
        # User automatically restored after context manager exits
ホワイトリスト登録済みメソッドをテストする
class TestAPI(IntegrationTestCase):
    def test_whitelisted_method(self):
        frappe.set_user("[email protected]")
        result = frappe.call("myapp.api.get_dashboard_data", filters={})
        self.assertIsInstance(result, dict)
        self.assertIn("total", result)
外部サービスをモックする
from unittest.mock import patch, MagicMock

class TestIntegration(IntegrationTestCase):
    @patch("myapp.integrations.stripe.requests.post")
    def test_payment_gateway(self, mock_post):
        mock_post.return_value = MagicMock(
            status_code=200,
            json=lambda: {"status": "success", "id": "ch_123"}
        )
        result = process_payment(amount=1000, currency="USD")
        self.assertEqual(result["status"], "success")
        mock_post.assert_called_once()
設定変更を伴うテスト
class TestFeature(IntegrationTestCase):
    def test_with_modified_settings(self):
        with self.change_settings("Selling Settings", {"so_required": 1}):
            # Settings temporarily changed
            self.assertRaises(frappe.ValidationError, create_delivery_note)
        # Settings automatically reverted
フックのオーバーライドを使ったテスト
class TestHooks(IntegrationTestCase):
    def test_custom_hook(self):
        with self.patch_hooks({"on_submit": ["myapp.hooks.custom_on_submit"]}):
            doc = create_and_submit_doc()
            # Verify hook was executed

コンテキストマネージャーのリファレンス

コンテキストマネージャー使用可能なクラス目的
set_user(user)UnitTestCase, IntegrationTestCaseユーザーコンテキストを一時的に切り替えます
change_settings(dt, **kw)UnitTestCase, IntegrationTestCase設定を一時的に変更します
patch_hooks(overrides)UnitTestCase, IntegrationTestCaseフックを一時的にオーバーライドします
freeze_time(time)UnitTestCase, IntegrationTestCase決定論的なテストのために時刻を固定します
debug_on(*exceptions)UnitTestCase, IntegrationTestCase例外発生時にデバッガーを起動します
timeout(seconds)デコレーター制限時間を超えたテストを失敗させます
enable_safe_exec()IntegrationTestCaseサーバースクリプトを一時的に有効にします
switch_site(site)IntegrationTestCase別のサイトに切り替えます
assertQueryCount(n)IntegrationTestCaseSQL クエリ数が指定値と一致することを検証します
assertRedisCallCounts(**kw)IntegrationTestCaseRedis コマンド数を検証します
assertRowsRead(n)IntegrationTestCase行レベルの DB アクセス制限を検証します

データベース状態の管理

  • IntegrationTestCase:各テストの後に必ずデータベースをロールバックします。クリーンアップは不要です
  • UnitTestCase:データベース接続がありません。frappe.db の呼び出しは絶対に使用しないでください
  • 各テストはクリーンな状態で実行されます。トランザクションは setUp で開始され、tearDown でロールバックされます
  • テストで frappe.db.commit() を呼び出さないでください。テストの分離性が損なわれます
  • コードがテストランナーで実行されているかどうかを確認するには、frappe.flags.in_test を使用してください

テストモードの検出

if frappe.flags.in_test:
    # Skip external API calls, emails, etc.
    return mock_response()

検証ロジックをスキップするために frappe.flags.in_test を使用しないでください。テストでは本番環境と同じコードパスを必ず実行してください。

よくある落とし穴

  1. super().setUpClass() を絶対に忘れないでください — 省略すると、フィクスチャの読み込みとユーザー設定が機能しなくなります
  2. テストで frappe.db.commit() を呼び出さないでください — データがテスト間で保持され、分離性が損なわれます
  3. frappe.set_user() を直接呼び出した場合は、tearDown で必ずユーザーをリセットしてください(v14 のパターン)
  4. テストデータには必ず _Test をプレフィックスとして付けてください — クリーンアップと識別が簡単になります
  5. テストの実行順序に依存しないでください — 各テストは必ず独立させてください
  6. フィクスチャの作成を制御するには、必ず frappe.flags を使用してください — 重複した挿入を防げます

関連項目

コメント