2026年5月12日火曜日

utPLSQLを使ってPL/SQLのプロシージャの単体テストを実施する

PL/SQLで記述したプロシージャやファンクションの単体テストを実施するフレームワークとして、utPLSQLがあります。大きく書き換えられたv3から数えても10年近い歴史があるフレームワークですが、本ブログの著者は積極的に使おうとしたことはありません。

自分でコードを書いている範囲ではテスト用のコードを書く気は起こらなかったのですが、生成AIに書いてもらったコードとなると、単体テストは必要です。

そのような事情で、utPLSQLを使ってみました。

以下より、utPLSQLを使用した作業について記録します。

DocumentationのutPLSQL-frameworkに、おおむね参照すべき内容が記載されています。

User Guide
Reporting

以下の項目について紹介します。

Installation - utPLSQLのインストール手順
Annotations - パッケージ定義に付与するアノテーションについて
Expectations - 結果を検証するファンクションについて
Running unit tests - 単体テストの実行手順
Using reporters - テスト結果の出力方法


utPLSQLのインストール手順



インストール手順の説明です。

utPLSQLのコードをGitHubのリポジトリutPLSQLからダウンロードします。セクションScripted download of latest utPLSQL versionに、Unix/LinuxとWindowsの手順がそれぞれ記載されています。

utPLSQL.zipをダウンロードしたのちに、それを回答します。

unzip -q utPLSQL.zip
cd utPLSQL
ls

% unzip -q utPLSQL.zip 

% cd utPLSQL

utPLSQL % ls

CHANGELOG.md CONTRIBUTING.md examples readme.md VERSION

CODE_OF_CONDUCT.md docs LICENSE source

utPLSQL % 


解凍されたディレクトリに含まれるsourceに移動します。

cd source
ls

utPLSQL % cd source

source % ls

api create_utplsql_owner.sql install.sql

check_object_grants.sql define_ut3_owner_param.sql reporters

check_sys_grants.sql dummy.sql set_install_params.sql

core expectations uninstall_all.sql

create_grants.sql install_above_12_1.sql uninstall_coverage_tables.sql

create_synonyms_and_grants_for_public.sql install_component.sql uninstall_objects.sql

create_synonyms.sql install_ddl_trigger.sql uninstall_synonyms.sql

create_user_grants.sql install_headless_with_trigger.sql uninstall.sql

create_user_synonyms.sql install_headless.sql

source % 


いくつかのインストール方法があります。
  1. DDLトリガーを作成せずに、一度にすべてインストールする
  2. DDLトリガーを作成して、一度にすべてインストールする
  3. ユーザーの作成など、ステップを分けてインストールする
今回はDDLトリガーを作成した上で、一度にインストールします。スクリプトinstall_headless_with_trigger.sqlを実行します。

SYSDBA権限を持つユーザーでデータベースに接続し、上記のスクリプトを実行します。

第1引数はutPLSQLが作成するオブジェクト(表やパッケージ)を保持するスキーマです。新規作成となるため、存在しないスキーマ名を指定します。
第2引数は作成するスキーマのパスワードです。ここで作成するユーザーでデータベースに接続することはないため、どのようなパスワードでもかまいません。
第3引数は作成するユーザーのデフォルト表領域を指定します。

以下は、スキーマUT3をデフォルト表領域USERSとして作成し、utPLSQLをインストールしています。

@install_headless_with_trigger.sql ut3 [パスワード] users

SQL> @install_headless_with_trigger.sql ut3 3qgeqf4tq5r3t2 users


Creating utPLSQL user ut3

-------------------------------------------------------------

Installing utPLSQL v3 framework into ut3 schema

-------------------------------------------------------------

Switching current schema to ut3

-------------------------------------------------------------

Installing component UT_DBMS_OUTPUT_CACHE


エラーはありません。

--------------------------------------------------------------


Installing component UT_VARCHAR2_LIST


[中略]



Synonym DBMSPCC_RUNSは作成されました。



Synonym DBMSPCC_UNITSは作成されました。


Installing component UT_TRIGGER_ANNOTATION_PARSING


エラーはありません。

--------------------------------------------------------------


Oracle AI Database 26ai Free Release 23.26.1.0.0 - Develop, Learn, and Run for Free

Version 23.26.1.0.0から切断されました

source % 


スクリプトの実行が完了するとutPLSQLのインストールは完了です。

インストールの選択肢にDDLトリガーの有無がありました。utPLSQLはパッケージ定義にアノテーションを記述し、そのアノテーションにそって単体テストを実施します。

例えばパッケージ定義test_run_sqlを以下のように記述します。アノテーションは、このパッケージ定義に記述されている--%で始まるコメントです。
create or replace package test_run_sql as
    --%suite(Test function run_sql)

    --%beforeall
    procedure prepare_data;

    --%afterall
    procedure cleanup_data;

    --%test(apple)
    procedure test_select_apple;

    --%test(banana)
    procedure test_select_banana;
end test_run_sql;
/
パッケージ定義はデータベースにテキストとして保存されます。Oracle Databaseでは、ビューDBA_SOURCEより、パッケージ定義のテキストを参照できます。

select text from dba_source where owner = 'APEXDEV' and name = 'TEST_RUN_SQL' and type = 'PACKAGE' order by line asc;

SQL> select text from dba_source where owner = 'APEXDEV' and name = 'TEST_RUN_SQL' and type = 'PACKAGE' order by line asc;


TEXT                                   

______________________________________ 

package test_run_sql as                

    --%suite(Test function run_sql)    

    --%beforeall                       

    procedure prepare_data;            

    --%afterall                        

    procedure cleanup_data;            

    --%test(apple)                     

    procedure test_select_apple;       

    --%test(banana)                    

    procedure test_select_banana;      

end test_run_sql;                      


15行が選択されました。 


SQL> 


このソースよりアノテーションを取り出し、utPLSQLが持つ表UT_ANNOTATION_CACHEなどに転記します。DDLトリガーを作成すると、転記するパッケージ定義の特定にトリガーを使用します。データベース・オブジェクトが少ないときは問題がありませんが、数が多い場合はトリガーを使用した方がアノテーションの転記が早く終了します。

utPLSQLを削除する場合、uninstall_all.sqlを実行します。作成したデータベース・ユーザーは残るため、uninstall_all.sql実行後にユーザーをドロップします。単にユーザーをドロップするだけでは、パブリック・シノニムが残るため、必ずuninstall_all.sqlを実行したのちに、ユーザーをドロップします。

テスト・パッケージの削除後などに、アノテーション・キャッシュを初期化する手順については、以下に記載されています。

ut_runner.rebuild_annotation_cacheやut_runner.purge_cacheを呼び出します。


パッケージ定義に付与するアノテーションについて



指定できるアノテーションの説明です。

パッケージ定義test_run_sqlが、以下のように記述されていたとします。
create or replace package test_run_sql as
    --%suite(Test function run_sql)

    --%beforeall
    procedure prepare_data;

    --%afterall
    procedure cleanup_data;

    --%test(apple)
    procedure test_select_apple;

    --%test(banana)
    procedure test_select_banana;
end test_run_sql;
/
実際のテストはパッケージtest_run_sqlの本体に記述されています。

このパッケージtest_run_sqlを、単体テストとして実行するにはut.runを呼び出します。

exec ut.run('test_run_sql');

以下は実行例です。

SQL> set serveroutput on

SQL> exec ut.run('test_run_sql');

Test function run_sql

  apple [.001 sec]

  banana [.002 sec] (FAILED - 1)

 

Failures:

 

  1) test_select_banana

      Actual: json was expected to equal: json

      Diff: 1 differences found

      1 unequal values

        Actual value: "apple" was expected to be: "banana" on path: $[0]."VALUE"

      at "APEXDEV.TEST_RUN_SQL.TEST_SELECT_BANANA", line 41 ut.expect(l_actual).to_( equal(l_expect) );

       

Finished in .004732 seconds

2 tests, 1 failed, 0 errored, 0 disabled, 0 warning(s)

 



PL/SQLプロシージャが正常に完了しました。


SQL> 


アノテーションsuiteで、テストの名称を設定しています。

--%suite(Test function run_sql)

プロシージャの直前のコメントとして、アノテーションbeforeallafteralltestをそれぞれ指定しています。

テストの実行順序は、以下になります。
  1. アノテーションbeforewallが指定されたプロシージャprepare_dataが最初に実行されます。
  2. アノテーションtest(apple)が指定されたプロシージャtest_select_appleが単体テストとして実行されます。テスト名はappleです。
  3. アノテーションtest(banana)が指定されたプロシージャtest_select_bananaが単体テストとして実行されます。テスト名はbananaです。
  4. アノテーションafterallが指定されたプロシージャcleanup_dataが最後に実行されます。
アノテーションtestで指定される単体テストについては、パッケージ内の出現順序に従って実行されます。テストの実行順序などを制御するアノテーションとしては、この他にbeforeeachaftereachbeforetestaftertestdisabledがあります。

アノーテーションを設定することにより実行順序の制御や、実行単位の設定、名前付けなどを行います。


結果を検証するファンクションについて



単体テストの結果の検証に、Expectationsを使用します。

プロシージャut.expectを呼び出して単体テストが期待通りに動作しているか、そうでないのかを検証し、それをテスト結果として報告します。

ut_expectの呼び出し形式は以下です。
  ut.expect( a_actual {data-type} [, a_message {varchar2}] ).to_( {matcher} );
  ut.expect( a_actual {data-type} [, a_message {varchar2}] ).not_to( {matcher} );
第1引数に実際の値を渡します。ファンクションexpectは型ごとにオーバーロードされているため、数値、文字、日付型、CLOB、JSON(json_element_tおよび派生型を含む)、カーソル、コレクションなどを渡せます。ファンクションut.expectは、検証処理を呼び出せるオブジェクトを返します。
第2引数は、想定している値が実際の値と異なる際に表示されるメッセージです。デフォルトのメッセージに追加します。

ut_expectが戻すオブジェクトのメンバー・ファンクション(総称的なものがto_およびnot_to)に、matcherを引数として渡します。matcherは比較条件となるファンクションです。matcherには、equal(期待している値と実際の値が一致した時に成功)、be_null(実際の値がnullのときに成功)など、いろいろな比較条件があります。期待値(equalなど、期待値がある場合)は、matcherの引数として指定します。

以下はl_actualとl_expectが一致している場合に成功となります。

ut.expect(l_actual).to_( equal(l_expect) );

これは以下のように短縮できます。

ut.expect(l_actual).to_equal(l_expect);

単体テストを記述するパッケージ本体は、プロシージャのコードにut.expectの呼び出しを含めます。

例えば、先ほどのパッケージ定義test_run_sqlのパッケージ本体は、以下のように記述します。
create or replace package body test_run_sql as

    procedure prepare_data
    as
    begin
        insert into my_test(value) values('apple');
    end prepare_data;

    procedure cleanup_data
    as
    begin
        delete from my_test where value = 'apple';
    end cleanup_data;

    procedure test_select_apple
    as
        l_sql    clob;
        l_expect json_array_t;
        l_result clob;
        l_actual json_array_t;
    begin
        l_sql := q'[select value from my_test where value = 'apple']';
        l_expect := json_array_t.parse('[{"VALUE":"apple"}]');
        l_result := my_run_sql(l_sql);
        l_actual := json_array_t.parse(l_result);
        ut.expect(l_actual).to_equal(l_expect);
    end test_select_apple;

    procedure test_select_banana
    as
        l_sql    clob;
        l_expect json_array_t;
        l_result clob;
        l_actual json_array_t;
    begin
        l_sql := q'[select value from my_test where value = 'apple']';
        l_expect := json_array_t.parse('[{"VALUE":"banana"}]');
        l_result := my_run_sql(l_sql);
        l_actual := json_array_t.parse(l_result);
        -- ut.expect(l_actual).to_equal(l_expect);
        ut.expect(l_actual).to_( equal(l_expect) );
    end test_select_banana;

end test_run_sql;
/
matcherには、be_between、be_empty、be_false、be_greater_or_equal、be_greater_than、be_less_or_equal、be_less_than、be_like、be_not_null、be_null、be_true、have_count、match、equal、contain、to_be_within of、to_be_within_pct ofなどがあります。

matcherの引数にはsys_refcursorを指定できるため、データベースへのデータの挿入や削除の結果の比較も容易です。


単体テストの実行手順



それぞれのテストをパッケージとして実装して、単体テストを実行します。テストの実行には、ut.runまたはut_runner.runを呼び出します。

アノテーションsuitepathで明示的にパスが設定されていなければ、所有者:パッケージ名:プロシージャ名が、単体テストのパスになります。

無指定の場合、そのスキーマに登録されているすべての単体テストが実行されます。実行されるテストを制限する場合は、引数にパスを指定します。アスタリスク(*)を、パス名のワイルドカードとして含めることができます。

例えばパッケージtest_run_sqlのテスト・プロシージャtest_select_appleのみを実行する場合は、以下のように指定します。

exec ut.run('test_run_sql.test_select_apple');

SQL> exec ut.run('test_run_sql.test_select_apple');

Test function run_sql

  apple [.002 sec]

 

Finished in .003497 seconds

1 tests, 0 failed, 0 errored, 0 disabled, 0 warning(s)

 



PL/SQLプロシージャが正常に完了しました。


SQL> 


test_run_sql.test_select_*を指定すると、指定されたパスに一致するテスト・プロシージャtest_select_appleおよびtest_select_bananaが実行されます。

exec ut.run('test_run_sql.test_select_*');

SQL> exec ut.run('test_run_sql.test_select_*');

Test function run_sql

  apple [.001 sec]

  banana [.002 sec] (FAILED - 1)

 

Failures:

 

  1) test_select_banana

      Actual: json was expected to equal: json

      Diff: 1 differences found

      1 unequal values

        Actual value: "apple" was expected to be: "banana" on path: $[0]."VALUE"

      at "APEXDEV.TEST_RUN_SQL.TEST_SELECT_BANANA", line 41 ut.expect(l_actual).to_( equal(l_expect) );

       

Finished in .004282 seconds

2 tests, 1 failed, 0 errored, 0 disabled, 0 warning(s)

 



PL/SQLプロシージャが正常に完了しました。


SQL> 


テスト・コードおよびテストの対象となるプロシージャやファンクションは、commitやrollbackなどのトランザクションの制御文は含まないことが想定されています。
utPLSQLはデフォルトで、テスト終了後に自動的にロールバックを行います。アノテーションrollbackの指定により、トランザクションの自動制御を無効にできます。


テスト結果の出力方法



単体テストの実行にはut.runを呼び出します。

レポーターを指定していない場合、デフォルトのDocumentation Reporterが呼び出されます。
https://www.utplsql.org/utPLSQL/latest/userguide/reporters.html#documentation-reporter

Documentation Reporterを明示的に指定すると、以下の呼び出しになります。

exec ut.run('test_run_sql.test_select_*', ut_documentation_reporter());

SQL> exec ut.run('test_run_sql.test_select_*', ut_documentation_reporter());

Test function run_sql

  apple [.001 sec]

  banana [.002 sec] (FAILED - 1)

 

Failures:

 

  1) test_select_banana

      Actual: json was expected to equal: json

      Diff: 1 differences found

      1 unequal values

        Actual value: "apple" was expected to be: "banana" on path: $[0]."VALUE"

      at "APEXDEV.TEST_RUN_SQL.TEST_SELECT_BANANA", line 41 ut.expect(l_actual).to_( equal(l_expect) );

       

Finished in .004351 seconds

2 tests, 1 failed, 0 errored, 0 disabled, 0 warning(s)

 



PL/SQLプロシージャが正常に完了しました。


SQL> 


Reporterのひとつに、JUnit Reporterがあります。

exec ut.run('test_run_sql.test_select_*', ut_junit_reporter());

JUnit Reporterを使うと、テスト結果は以下のように出力されます。

SQL> exec ut.run('test_run_sql.test_select_*', ut_junit_reporter());

<?xml version="1.0"?>

<testsuites tests="2" disabled="0" errors="0" failures="1" name="" time=".004432" >

<testsuite tests="2" id="1" package="test_run_sql"  disabled="0" errors="0" failures="1" name="Test function run_sql" time=".004356" >

<testcase classname="test_run_sql" assertions="1" name="apple" time=".001344" >

<system-out/>

<system-err/>

</testcase>

<testcase classname="test_run_sql" assertions="1" name="banana" time=".002119"  status="Failure">

<failure>

<![CDATA[

Actual: json was expected to equal: json

Diff: 1 differences found

1 unequal values

  Actual value: "apple" was expected to be: "banana" on path: $[0]."VALUE"

at "APEXDEV.TEST_RUN_SQL.TEST_SELECT_BANANA", line 41 ut.expect(l_actual).to_( equal(l_expect) );

]]>

</failure>

<system-out/>

<system-err/>

</testcase>

<system-out/>

<system-err/>

</testsuite>

</testsuites>



PL/SQLプロシージャが正常に完了しました。


SQL> 


ut.runは表関数として呼び出すことができます。プロシージャとして呼び出した場合は、テスト結果がターミナルに出力されますが、表関数として呼び出すとSELECT文の結果としてテスト結果を取得できます。

呼び出し方法の例です。
begin
    for r in (
        select column_value from ut.run('test_run_sql.test_select_*', ut_junit_reporter())
    )
    loop
        dbms_output.put_line(r.column_value);
    end loop;
end;
/

SQL> begin

  2      for r in (

  3          select column_value from ut.run('test_run_sql.test_select_*', ut_junit_reporter())

  4      )

  5      loop

  6          dbms_output.put_line(r.column_value);

  7      end loop;

  8  end;

  9* /

<?xml version="1.0"?>

<testsuites tests="2" disabled="0" errors="0" failures="1" name="" time=".004181" >

<testsuite tests="2" id="1" package="test_run_sql"  disabled="0" errors="0" failures="1" name="Test function run_sql" time=".004112" >

<testcase classname="test_run_sql" assertions="1" name="apple" time=".001175" >

<system-out/>

<system-err/>

</testcase>

<testcase classname="test_run_sql" assertions="1" name="banana" time=".002171"  status="Failure">

<failure>

<![CDATA[

Actual: json was expected to equal: json

Diff: 1 differences found

1 unequal values

  Actual value: "apple" was expected to be: "banana" on path: $[0]."VALUE"

at "APEXDEV.TEST_RUN_SQL.TEST_SELECT_BANANA", line 41 ut.expect(l_actual).to_( equal(l_expect) );

]]>

</failure>

<system-out/>

<system-err/>

</testcase>

<system-out/>

<system-err/>

</testsuite>

</testsuites>



PL/SQLプロシージャが正常に完了しました。


SQL> 


Oracle Databaseに接続してSQLを実行できる環境であれば、どこでもテスト結果をストリームとして受け取ることができます。

大まかにutPLSQLの使い方をまとめてみました。

確認作業に使用したコードをGitHubのutPLSQL-studiesにまとめています。
https://github.com/ujnak/utPLSQL-studies

今回の記事は以上になります。