ビジネスロジックを確実に自動テストするには、「外部依存(時刻・メール・HTTP・シーケンス・ファイル・権限)を分離して差し替え可能にする」ことが要点です。PL/SQLでもポート/アダプタ発想・薄いファサード・依存の抽象化(インターフェース)・シノニム差し替え・アプリケーションコンテキスト・条件付きコンパイルを組み合わせると、テスト容易性を大幅に高められます。この記事では、実運用を想定したプロシージャ設計とモック化の実装パターンを、まとまったコード例で解説します。
- 基本方針:テスタブルなPL/SQLの設計原則
- ポート/アダプタ設計:仕様(Port)を固定し実装(Adapter)を差し替える
- ドメインサービスはPort越しに外部へ依存する
- テスト用“時刻/UUID”の差し替え(Fake Clock/Id)
- アプリケーションコンテキストで「テストモード」を切り替える
- 条件付きコンパイルでテスト分岐をビルド時に切り替える
- 副作用の観測:モックテーブルとGTT(グローバル一時表)
- トランザクションとロールバック戦略(テストフレンドリー化)
- サンプル:utPLSQLでドメインとモックを検証する
- HTTP/外部APIのモック:薄いHTTPポートで吸収する
- 依存注入のもう一手:エイリアス/エイリアステーブルでの切り替え
- アンチパターンと回避
- まとめ
基本方針:テスタブルなPL/SQLの設計原則
テスタビリティを上げる最短ルートは、(1) 副作用の局所化、(2) トランザクション境界の上位集約、(3) 依存の抽象化、(4) 冪等な設計、の4点です。具体的には「DBMS_UTL*やUTL_HTTP等の外部I/Oは直接呼ばずに“ポート”パッケージに包む」「COMMITはなるべくファサード(呼び出し元)に集約」「現在時刻やUUIDはラッパー越しに取得」「同じ入力を繰り返しても結果が変わらないよう自然キーや一意制約で収束」を徹底します。
ポート/アダプタ設計:仕様(Port)を固定し実装(Adapter)を差し替える
以下は「メール送信」を外部依存として切り出し、実装を本番用とモックで差し替える最小構成です。呼び出し側は常にmail_port
の仕様だけを参照し、実体はシノニムで選択します。
-- 1) 仕様(Port):公開インターフェースだけ定義
CREATE OR REPLACE PACKAGE mail_port AUTHID DEFINER AS
PROCEDURE send(p_to VARCHAR2, p_subj VARCHAR2, p_body CLOB);
END;
/
-- 2) 本番アダプタ(Adapter):実際の送信(例:UTL_SMTP/ORDSなど)
CREATE OR REPLACE PACKAGE mail_impl_smtp AUTHID DEFINER AS
PROCEDURE send(p_to VARCHAR2, p_subj VARCHAR2, p_body CLOB);
END;
/
CREATE OR REPLACE PACKAGE BODY mail_impl_smtp AS
PROCEDURE send(p_to VARCHAR2, p_subj VARCHAR2, p_body CLOB) IS
BEGIN
-- 実際の送信処理(簡略化)
NULL; -- UTL_SMTP/UTL_MAIL 等を使用
END;
END;
/
-- 3) モックアダプタ:送信内容を記録するだけ(テスト観測点)
CREATE TABLE mail_mock_log(
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
rcpt VARCHAR2(320), subj VARCHAR2(4000), body CLOB, at TIMESTAMP DEFAULT SYSTIMESTAMP
);
CREATE OR REPLACE PACKAGE mail_impl_mock AUTHID DEFINER AS
PROCEDURE send(p_to VARCHAR2, p_subj VARCHAR2, p_body CLOB);
END;
/
CREATE OR REPLACE PACKAGE BODY mail_impl_mock AS
PROCEDURE send(p_to VARCHAR2, p_subj VARCHAR2, p_body CLOB) IS
BEGIN
INSERT INTO mail_mock_log(rcpt, subj, body) VALUES(p_to, p_subj, p_body);
END;
END;
/
-- 4) シノニムで実体を差し替え(本番:SMTP / テスト:MOCK)
CREATE OR REPLACE SYNONYM mail_impl FOR mail_impl_smtp; -- 本番既定
-- テスト環境では ↓ を発行
-- CREATE OR REPLACE SYNONYM mail_impl FOR mail_impl_mock;
-- 5) Portの実装は常にシノニム越しに実体へ委譲
CREATE OR REPLACE PACKAGE BODY mail_port AS
PROCEDURE send(p_to VARCHAR2, p_subj VARCHAR2, p_body CLOB) IS
BEGIN
mail_impl.send(p_to, p_subj, p_body); -- 実体は同名手続きで互換
END;
END;
/
ドメインサービスはPort越しに外部へ依存する
業務ロジックは外部依存を一切知らない構造にします。下例は「注文確定」でメールを通知するユースケースです。COMMIT
は上位レイヤー(APIファサードやバッチ)で行う前提にし、プロシージャ自身は純粋に“状態変換”だけを担当させるとテストが簡単になります。
CREATE TABLE orders(
req_id VARCHAR2(64) PRIMARY KEY,
cust_id NUMBER NOT NULL,
amount NUMBER NOT NULL,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP
);
CREATE OR REPLACE PACKAGE order_service AS
PROCEDURE place(p_req_id VARCHAR2, p_cust NUMBER, p_amt NUMBER);
END;
/
CREATE OR REPLACE PACKAGE BODY order_service AS
PROCEDURE place(p_req_id VARCHAR2, p_cust NUMBER, p_amt NUMBER) IS
BEGIN
MERGE INTO orders d
USING (SELECT p_req_id req_id, p_cust cust, p_amt amt FROM dual) s
ON (d.req_id = s.req_id)
WHEN MATCHED THEN UPDATE SET d.amount=s.amt
WHEN NOT MATCHED THEN INSERT(req_id,cust_id,amount) VALUES(s.req_id,s.cust,s.amt);
mail_port.send('notify@example.com', 'order placed',
JSON_OBJECT('reqId' VALUE p_req_id, 'amt' VALUE p_amt RETURNING CLOB));
END;
END;
/
テスト用“時刻/UUID”の差し替え(Fake Clock/Id)
テストが壊れやすい代表が「現在時刻」と「GUID」です。これもPort化して固定値に差し替えられるようにします。
-- 仕様
CREATE OR REPLACE PACKAGE clk_port AS
FUNCTION now RETURN TIMESTAMP;
FUNCTION guid RETURN VARCHAR2;
END;
/
-- 実体(本番)
CREATE OR REPLACE PACKAGE clk_impl_real AS
FUNCTION now RETURN TIMESTAMP; FUNCTION guid RETURN VARCHAR2;
END;
/
CREATE OR REPLACE PACKAGE BODY clk_impl_real AS
FUNCTION now RETURN TIMESTAMP IS BEGIN RETURN SYSTIMESTAMP; END;
FUNCTION guid RETURN VARCHAR2 IS BEGIN RETURN RAWTOHEX(SYS_GUID()); END;
END;
/
-- モック(テストで上書き)
CREATE OR REPLACE PACKAGE clk_impl_mock AS
g_now TIMESTAMP := TIMESTAMP '2025-10-07 12:00:00';
g_guid VARCHAR2(64) := 'TEST-GUID-0001';
FUNCTION now RETURN TIMESTAMP; FUNCTION guid RETURN VARCHAR2;
END;
/
CREATE OR REPLACE PACKAGE BODY clk_impl_mock AS
FUNCTION now RETURN TIMESTAMP IS BEGIN RETURN g_now; END;
FUNCTION guid RETURN VARCHAR2 IS BEGIN RETURN g_guid; END;
END;
/
CREATE OR REPLACE SYNONYM clk_impl FOR clk_impl_real; -- 本番
-- テストで:CREATE OR REPLACE SYNONYM clk_impl FOR clk_impl_mock;
-- Port本体
CREATE OR REPLACE PACKAGE BODY clk_port AS
FUNCTION now RETURN TIMESTAMP IS BEGIN RETURN clk_impl.now; END;
FUNCTION guid RETURN VARCHAR2 IS BEGIN RETURN clk_impl.guid; END;
END;
/
アプリケーションコンテキストで「テストモード」を切り替える
環境に応じた分岐は、コンテキストに寄せるとテストしやすくなります。以下はメール送信の内側でモックログへフォールバックする例です。
-- コンテキスト作成
CREATE CONTEXT app_ctx USING app_ctx_pkg;
CREATE OR REPLACE PACKAGE app_ctx_pkg AS
PROCEDURE set_mode(p_test BOOLEAN);
FUNCTION is_test RETURN BOOLEAN;
END;
/
CREATE OR REPLACE PACKAGE BODY app_ctx_pkg AS
PROCEDURE set_mode(p_test BOOLEAN) IS
BEGIN
DBMS_SESSION.SET_CONTEXT('APP_CTX','TEST_MODE', CASE WHEN p_test THEN '1' ELSE '0' END);
END;
FUNCTION is_test RETURN BOOLEAN IS
BEGIN
RETURN SYS_CONTEXT('APP_CTX','TEST_MODE') = '1';
END;
END;
/
-- mail_portの中で参照(必要な場合のみ)
CREATE OR REPLACE PACKAGE BODY mail_port AS
PROCEDURE send(p_to VARCHAR2, p_subj VARCHAR2, p_body CLOB) IS
BEGIN
IF app_ctx_pkg.is_test THEN
mail_impl_mock.send(p_to, p_subj, p_body); -- 強制モック
ELSE
mail_impl.send(p_to, p_subj, p_body);
END IF;
END;
END;
/
条件付きコンパイルでテスト分岐をビルド時に切り替える
CCFLAGSを使えば、ビルド時に振る舞いを丸ごと切り替えられます。CIで「本番ビルド」と「テストビルド」を分けたい場合に有効です。
ALTER SESSION SET plsql_ccflags = 'TESTING:TRUE';
-- パッケージ内
$IF $$TESTING $THEN
-- テスト専用の軽量ロジック
$ELSE
-- 本番ロジック
$END
副作用の観測:モックテーブルとGTT(グローバル一時表)
メール・HTTP・外部通知などは、モック実装で「呼び出し履歴」をテーブルに書き出して検証します。テスト間の独立性が必要なら、グローバル一時表を使うとクリーンです。
CREATE GLOBAL TEMPORARY TABLE http_mock_log(
id NUMBER, url VARCHAR2(2000), method VARCHAR2(10), body CLOB
) ON COMMIT DELETE ROWS;
-- テスト中の確認例
SELECT COUNT(*) FROM http_mock_log WHERE url LIKE '%/orders%' AND method='POST';
トランザクションとロールバック戦略(テストフレンドリー化)
プロシージャ内部でのCOMMIT
/ROLLBACK
は極力避け、上位で制御します。テストでは1テスト=1トランザクションとして最後にロールバックすれば、データ掃除が不要になります。どうしても内部で確定すべき監査は、最小限の自治トランザクションへ切り出します。
-- テストスイート冒頭
SAVEPOINT sp_test;
-- ... 複数ケース実行 ...
ROLLBACK TO sp_test; -- 初期状態に戻す
サンプル:utPLSQLでドメインとモックを検証する
軽量に1ケースだけ示します。実装は任意のテストFW(utPLSQL等)を想定してください。
-- 1) テストセットアップ:モード切替&シノニム差し替え
BEGIN
app_ctx_pkg.set_mode(TRUE);
EXECUTE IMMEDIATE 'CREATE OR REPLACE SYNONYM mail_impl FOR mail_impl_mock';
EXECUTE IMMEDIATE 'CREATE OR REPLACE SYNONYM clk_impl FOR clk_impl_mock';
clk_impl_mock.g_now := TIMESTAMP '2025-10-07 09:00:00';
clk_impl_mock.g_guid := 'GUID-TEST-1234';
END;
/
-- 2) 実行
BEGIN
order_service.place(p_req_id => 'REQ-1', p_cust => 100, p_amt => 5000);
END;
/
-- 3) 検証(観測点はDB)
DECLARE
v_mail_cnt NUMBER;
BEGIN
SELECT COUNT(*) INTO v_mail_cnt FROM mail_mock_log WHERE rcpt='notify@example.com';
IF v_mail_cnt < 1 THEN
RAISE_APPLICATION_ERROR(-20000, 'メール送信が記録されていない');
END IF;
END;
/
HTTP/外部APIのモック:薄いHTTPポートで吸収する
UTL_HTTPを直接使わず、薄いポートを経由します。本番はUTL_HTTP、テストはモックテーブルへ記録する実装に差し替えます。
CREATE OR REPLACE PACKAGE http_port AS
FUNCTION post_json(p_url VARCHAR2, p_body CLOB) RETURN NUMBER; -- 返り値=HTTPステータス
END;
/
CREATE OR REPLACE PACKAGE http_impl_real AS
FUNCTION post_json(p_url VARCHAR2, p_body CLOB) RETURN NUMBER;
END;
/
CREATE OR REPLACE PACKAGE BODY http_impl_real AS
FUNCTION post_json(p_url VARCHAR2, p_body CLOB) RETURN NUMBER IS
BEGIN
-- UTL_HTTPでPOSTし、ステータス返却(省略)
RETURN 200;
END;
END;
/
CREATE TABLE http_calls_mock(url VARCHAR2(2000), body CLOB, at TIMESTAMP);
CREATE OR REPLACE PACKAGE http_impl_mock AS
FUNCTION post_json(p_url VARCHAR2, p_body CLOB) RETURN NUMBER;
END;
/
CREATE OR REPLACE PACKAGE BODY http_impl_mock AS
FUNCTION post_json(p_url VARCHAR2, p_body CLOB) RETURN NUMBER IS
BEGIN
INSERT INTO http_calls_mock(url, body, at) VALUES(p_url, p_body, SYSTIMESTAMP);
RETURN 202; -- 疑似応答
END;
END;
/
CREATE OR REPLACE SYNONYM http_impl FOR http_impl_real; -- 本番
-- テストで:CREATE OR REPLACE SYNONYM http_impl FOR http_impl_mock;
CREATE OR REPLACE PACKAGE BODY http_port AS
FUNCTION post_json(p_url VARCHAR2, p_body CLOB) RETURN NUMBER IS
BEGIN
RETURN http_impl.post_json(p_url, p_body);
END;
END;
/
依存注入のもう一手:エイリアス/エイリアステーブルでの切り替え
「参照先表を環境ごとに差し替えたい」場合は、Editioning Viewや同名シノニム(v_orders
)を経由し、本番は実表、テストはGTT/薄いコピーに向けると、クエリ本文を一切変えずに検証できます。
-- 本番
CREATE OR REPLACE VIEW v_orders AS SELECT * FROM orders;
-- テスト
-- CREATE GLOBAL TEMPORARY TABLE orders_test ON COMMIT DELETE ROWS AS SELECT * FROM orders WHERE 1=0;
-- CREATE OR REPLACE VIEW v_orders AS SELECT * FROM orders_test;
アンチパターンと回避
プロシージャ内部での無差別COMMIT、UTL_HTTP/UTL_SMTP直呼び、SYSTIMESTAMP/ SYS_GUIDのべた書き、例外握りつぶし、巨大な暗黙依存(パッケージ変数)――これらはテスト不能・再現不能の主要因です。必ずポート化して差し替え可能にし、状態は引数から受け取り、終了時に返すスタイルを貫きます。
まとめ
PL/SQLでも「ポート/アダプタ」「シノニム差し替え」「コンテキスト/CCFLAGS」「GTTモック」「上位でのトランザクション制御」を組み合わせれば、外部依存を自在に切り離してテストできます。ドメインは純粋化し、副作用はポートへ追放、観測点はDBテーブルに残す――この基本を守るだけで、ユニット/結合/回帰テストが安定し、変更に強いプロシージャ設計が実現します。