frappe-testing-unit について
Frappe開発者向けのユニットテストと統合テストのリファレンススキルです。バージョン別のテストクラス、フィクスチャ、テストパターン、状態管理に加え、実行やレポート生成のコマンドを扱います。
主な機能
- 01ユニットテストと統合テストのクラス
- 02テスト実行コマンドとオプション
- 03フィクスチャの設定パターン
- 04ドキュメントと権限のテスト
- 05テスト状態用コンテキストマネージャー
- 06データベース分離のガイド
活用シーン
- 01関数やメソッドの単体テスト
- 02ドキュメントのライフサイクル動作の検証
- 03権限とユーザーコンテキストの確認
- 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/.gitkeepanti-patterns.mdapi-reference.mdexamples.mdfixtures.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) | IntegrationTestCase | SQL クエリ数が指定値と一致することを検証します |
assertRedisCallCounts(**kw) | IntegrationTestCase | Redis コマンド数を検証します |
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 を使用しないでください。テストでは本番環境と同じコードパスを必ず実行してください。
よくある落とし穴
super().setUpClass()を絶対に忘れないでください — 省略すると、フィクスチャの読み込みとユーザー設定が機能しなくなります- テストで
frappe.db.commit()を呼び出さないでください — データがテスト間で保持され、分離性が損なわれます frappe.set_user()を直接呼び出した場合は、tearDownで必ずユーザーをリセットしてください(v14 のパターン)- テストデータには必ず
_Testをプレフィックスとして付けてください — クリーンアップと識別が簡単になります - テストの実行順序に依存しないでください — 各テストは必ず独立させてください
- フィクスチャの作成を制御するには、必ず
frappe.flagsを使用してください — 重複した挿入を防げます
関連項目
- references/examples.md — テストの完全な例
- references/anti-patterns.md — よくある間違いと修正方法
- references/fixtures.md — フィクスチャのパターンを詳しく解説
- references/api-reference.md — テストユーティリティの API リファレンス
- frappe-testing-cicd — CI/CD パイプラインのセットアップ