【IRIS/Cache】ユニットテスト(実行編)

本記事は、ユニットテストの実行について解説します。

※この記事は下記の方向けになります。
  • 長期間運用が想定されるシステムを構築する方
  • 不具合が多くて悩んでいる方
  • コードの品質向上を目的とした、ユニットテストの導入検討を行っている方

ユニットテストを実行する為には、テスト専用のクラスが必用になります。
もし、前回を確認していない場合は、下記から参照してください。

はじめに

本記事は、ユニットテストの実行編となります。

ユニットテストの実行には、下記手順が必用になります。

ユニットテストの実行手順
  1. テスト・クラスのエクスポート
  2. メインディレクトリの設定
  3. テストコマンドの実行

では、一つずつ確認していきましょう。

① テスト・クラスのエクスポート

テスト・クラスは、エクスポートして特定のディレクトリに配置します。

作成編で作成したテスト・クラスは下記4クラスになり、これらが「テスト・ケース」となります。

前回の記事で作成したテスト・クラス
  • developer.export.UnitTest.cls
  • developer.export.UnitTestObject.cls
  • developer.export.UnitTestFile.cls
  • developer.export.UnitTestFileCheck.cls

テストケースは、テストの目的や対象、関連性などに基づいてグループ化します。
このグループを、「テストスイート」と呼びます。

ObjectScriptでは、テストケース(テストクラス)を配置する「フォルダ」をテストスイートとしています。

今回は、サンプルなので複数のテストスイートを用意したいと思います。

<メインディレクトリ>
  ├ sampleTest
  │ └ developer.export.UnitTest.cls
  │
  ├ sampleObj
  │ └ developer.export.UnitTestObject.cls
  │
  └ sampleFile
    ├ developer.export.UnitTestFile.cls
    └ developer.export.UnitTestFileCheck.cls

この配置に合わせて、クラスをエクスポートします。

エクスポート実施

今回は、下記コマンドを利用してエクスポートしました。

自システムで運用する場合は、「clsNm, path」を適宜読み替えてください。

k err
s clsNm = "developer.export.UnitTest.cls"
s path = "D:\Temp\test\sampleTest\"_clsNm_".xml"
d $system.OBJ.Export(clsNm, path, "-d", .err) zw err

k err
s clsNm = "developer.export.UnitTestObject.cls"
s path = "D:\Temp\test\sampleObj\"_clsNm_".xml"
d $system.OBJ.Export(clsNm, path, "-d", .err) zw err

k err
s clsNm = "developer.export.UnitTestFile.cls"
s path = "D:\Temp\test\sampleFile\"_clsNm_".xml"
d $system.OBJ.Export(clsNm, path, "-d", .err) zw err

k err
s clsNm = "developer.export.UnitTestFileCheck.cls"
s path = "D:\Temp\test\sampleFile\"_clsNm_".xml"
d $system.OBJ.Export(clsNm, path, "-d", .err) zw err

コマンド実行後、ディレクトリを確認してみます。
今回は、コマンドプロンプトから確認してみました。

D:\Temp\test>dir /s
 ドライブ D のボリューム ラベルは ボリューム です
 ボリューム シリアル番号は 5C07-9571 です

 D:\Temp\test のディレクトリ

2025/05/17  10:56    <DIR>          .
2025/05/17  10:56    <DIR>          ..
2025/05/17  16:00    <DIR>          sampleFile
2025/05/16  21:19    <DIR>          sampleObj
2025/05/15  08:03    <DIR>          sampleTest
               0 個のファイル                   0 バイト

 D:\Temp\test\sampleFile のディレクトリ

2025/05/17  16:00    <DIR>          .
2025/05/17  16:00    <DIR>          ..
2025/05/18  09:44             1,253 developer.export.UnitTestFile.cls.xml
2025/05/18  09:44             3,946 developer.export.UnitTestFileCheck.cls.xml
               2 個のファイル               5,199 バイト

 D:\Temp\test\sampleObj のディレクトリ

2025/05/16  21:19    <DIR>          .
2025/05/16  21:19    <DIR>          ..
2025/05/18  09:44             2,546 developer.export.UnitTestObject.cls.xml
               1 個のファイル               2,546 バイト

 D:\Temp\test\sampleTest のディレクトリ

2025/05/15  08:03    <DIR>          .
2025/05/15  08:03    <DIR>          ..
2025/05/18  09:43             1,508 developer.export.UnitTest.cls.xml
               1 個のファイル               1,508 バイト

     ファイルの総数:
               4 個のファイル               9,253 バイト
              11 個のディレクトリ  478,038,736,896 バイトの空き領域

エクスポートされているのが確認できました。

② メインディレクトリの設定

テストスイートを配置したルートパスを設定します。
先ほどの例であれば、「D:\Temp\test」が該当します。

ターミナルより下記コマンドを実行して、ルートパスを設定してください。
 ※ テストを実施するネームスペースに変更します。

zn [テスト実行ネームスペース]
s ^UnitTestRoot = "D:\Temp\test"

③ テストコマンドの実行

ここまでの準備が終わったら、いよいよテストを実行します。

テストの実行コマンドは「RunTest」と「DebugRunTestCase」の2つあります。

// テスト後、ロードしたテスト・クラスを削除する
d ##class(%UnitTest.Manager).RunTest([testspec], [qualifiers], [userparam])

// テスト・クラスのロード・削除を行わない
d ##class(%UnitTest.Manager).DebugRunTestCase([testspec], [qualifiers], [userparam])

両コマンドとも、引数が結構ややこしいので、要注意です。

RunTestの方は、エクスポートしたテスト・クラスをロードしてテストを実行した後、実施したテスト・クラスを削除します

テスト・クラスの開発中で動作を確認したい場合は、「qualifiers」に「/noload/nodelete」を設定すると、テスト・クラスが削除されないので便利です。

DebugRunTestCaseは、メインディレクトリ直下にテスト・クラスを配置して実行します。
 ※ 実行リスト作成用のため

引数

testspec

実行するテスト項目を指定できます。

「テストスイート」「テストケース」「テストメソッド」は、「:」区切りで連結する事でテスト対象を絞り込むことが可能です。

例)特定のテストメソッドのみテストを実施したい場合
  > テストスイート:テストケース:テストメソッド

また、複数のテストスイートやテストケースを実行したい場合は、「;」区切りで連結する事でテストを実施する事が可能です。

例)テストスイート配下の「テストケースA」「テストケースB」…を実行したい場合
「テストスイート:テストケース:テストメソッドA;テストケース:テストメソッドB;…」

指定内容説明
null全テストスイートを実行する
テストスイート※省略可
指定すると、直下の全テストケースを実行する
テストケース※省略可
指定すると、クラス内の全テストメソッドを実行する
テストメソッド※省略可
testspecの各項目説明

サンプル実行例

// 全テストスイート実行
d ##class(%UnitTest.Manager).RunTest()

// テストスイート指定
d ##class(%UnitTest.Manager).RunTest("sampleTest","/noload/nodelete/nodebug")

// developer.export.UnitTestFileCheck.clsのみを実行する
s testspec = "sampleFile:developer.export.UnitTestFileCheck"
d ##class(%UnitTest.Manager).RunTest(testspec,"/noload/nodelete")

qualifiers

テスト実行時のオプションを指定します。

修飾子説明
/load規定値
テストをロードする
/unloadテストをロードしないで実行する
/run規定値
テストを実行する
/norunテストをロードするが、実行しない
/delete規定値
テスト実行後、Iris or Cacheより該当クラスを削除する
/nodeleteテスト実行後に、該当クラスを削除しない
/recursive規定値
指定したディレクトリのサブディレクトリ内でテストを検索する
/norecursiveサブディレクトリは無視する
/debug最初のテストが失敗すると後続のテストを実行しない
/nodebug規定値
/autoload/autoload=[dir]を使用すると、テストは ^UnitTestRoot ディレクトリのサブディレクトリ[dir]からロードする
/display=all規定値
/display=allを使用すると、メソッドの実行時に拡張情報が表示される。
/display=none限定された情報が表示される。

開発時のおすすめは「/noload/nodelete」です。

これを付け忘れて、何度か開発中のテストクラスが削除された事が何度かあります。
マジで辛いです。

皆様もお気をつけて!

userparam

「/log」を指定します。

指定するとターミナルへの出力が最小になり、下記ファイルへの出力を

<インストールディレクトリ>mgr\UNITTEST.LOG

これはこれで・・・ありかも?

テスト実行

では、いよいよテストを実行したいと思います。

今回は下記コマンドを使用しましょう。

d ##class(%UnitTest.Manager).RunTest(,"/noload/nodelete/nodebug")

コマンドを実行すると、画面に大量の情報が表示されます。


===============================================================================
Directory: D:\Temp\test\sampleFile\
===============================================================================
  sampleFile begins ...
ディレクトリにあるアイテムのリスト作成を開始 on 05/18/2025 16:49:57 '*.xml;*.XML;*.cls;*.mac;*.int;*.inc;*.CLS;*.MAC;*.INT;*.INC'

ファイル D:\Temp\test\sampleFile\developer.export.UnitTestFile.cls.xml を xml としてリストしています
ファイル D:\Temp\test\sampleFile\developer.export.UnitTestFileCheck.cls.xml を xml としてリストしています
リスト作成が正常に完了しました。

    developer.export.UnitTestFile begins ...
      TestFileDelete() begins ...
        AssertTrue:ファイルの削除 ファイル名 = D:\Temp\txt\UnitTest.txt (passed)
        AssertNotTrue:ファイルの削除(エラー) (passed)
        LogMessage:Duration of execution: .000634 sec.
      TestFileDelete passed
    developer.export.UnitTestFile passed
    developer.export.UnitTestFileCheck begins ...
      TestFileCheck() begins ...
        AssertFilesSame:'D:\Temp\txt\UnitCopyTest.txt'=='D:\Temp\txt\UnitCopyTestcopy.txt' (passed)
        LogMessage:Duration of execution: .006184 sec.
      TestFileCheck passed
      TestFileNoCheck() begins ...
AssertFilesSame:'D:\Temp\txt\UnitCopyTest.txt'=='D:\Temp\txt\UnitCopyTestcopy.txt' (failed)  <<==== **FAILED**   sampleFile:developer.export.UnitTestFileCheck:TestFileNoCheck
File1 D:\Temp\txt\UnitCopyTest.txt
0:
>1:サンプルテキスト出力
2:
File2 D:\Temp\txt\UnitCopyTestcopy.txt
0:
>1:内容の不一致
2:

        LogMessage:Duration of execution: .004963 sec.
      TestFileNoCheck failed
      TestQueryPlan() begins ...
AssertFilesSQLUnorderSame:'D:\Temp\txt\queryPlan1.txt'=='D:\Temp\txt\queryPlan2.txt' (failed)  <<==== **FAILED**   sampleFile:developer.export.UnitTestFileCheck:TestQueryPlan

plan 1  SELECT ID,ABO血液型,RH血液型,dele,patientId,カナ氏名,コメント,性別,漢字氏名,生年月日 FROM developer_data.Patient2 where 漢字氏名 like '九重山%' & ABO血液型 = 'O' & RH血液型 = '+' & 性別 = 1 /*#OPTIONS {"DynamicSQL":1} */ =  SELECT ID,ABO血液型,RH血液型,dele,patientId,カナ氏名,コメント,性別,漢字氏名,生年月日 FROM developer_data.Patient2 where 漢字氏名 like '九重山%' & patientId between 11730001000 and 11730002000 /*#OPTIONS {"DynamicSQL":1} */
plan 1      Test the "=" condition on %SQLUPPER(ABO血液型), the "=" condition on %SQLUPPER(性別), the "=" condition on %SQLUPPER(RH血液型), the "NOT NULL" condition on %SQLUPPER(ABO血液型), the "NOT NULL" condition on %SQLUPPER(RH血液型), and the "NOT NULL" condition on %SQLUPPER(性別). =      Test the ">=" condition on %SQLUPPER(patientId) and the "<=" condition on %SQLUPPER(patientId).
        LogMessage:Duration of execution: .002952 sec.
      TestQueryPlan failed
    developer.export.UnitTestFileCheck failed
  Skipping deleting classes 
  sampleFile failed

===============================================================================
Directory: D:\Temp\test\sampleObj\
===============================================================================
  sampleObj begins ...
ディレクトリにあるアイテムのリスト作成を開始 on 05/18/2025 16:49:58 '*.xml;*.XML;*.cls;*.mac;*.int;*.inc;*.CLS;*.MAC;*.INT;*.INC'

ファイル D:\Temp\test\sampleObj\developer.export.UnitTestObject.cls.xml を xml としてリストしています
リスト作成が正常に完了しました。

    developer.export.UnitTestObject begins ...
      TestDelete() begins ...
        AssertStatusOK:データの削除 name = サンプルテスト (passed)
        LogMessage:Duration of execution: .000297 sec.
      TestDelete passed
      TestSave() begins ...
        AssertStatusOK:データの登録 name = サンプルテスト (passed)
        AssertStatusNotOK:登録時エラー エラー #5808: キーが一意ではありません: developer.test.TestData:idx:^developer.test.TestDataI("idx"," サンプルテスト") =  (passed)
        LogMessage:Duration of execution: .029102 sec.
      TestSave passed
    developer.export.UnitTestObject passed
  Skipping deleting classes 
  sampleObj passed

===============================================================================
Directory: D:\Temp\test\sampleTest\
===============================================================================
  sampleTest begins ...
ディレクトリにあるアイテムのリスト作成を開始 on 05/18/2025 16:49:58 '*.xml;*.XML;*.cls;*.mac;*.int;*.inc;*.CLS;*.MAC;*.INT;*.INC'

ファイル D:\Temp\test\sampleTest\developer.export.UnitTest.cls.xml を xml としてリストしています
リスト作成が正常に完了しました。

    developer.export.UnitTest begins ...
      TestAddition() begins ...
        LogMessage:足し算関数の検証
        AssertEquals:足し算関数検証(一致) 7 = 7 (passed)
        AssertNotEquals:足し算関数検証(不一致) 28 = 7 (passed)
        AssertSuccess:テスト成功 (passed)
        LogMessage:Duration of execution: .00025 sec.
      TestAddition passed
      TestAdditionError() begins ...
        LogMessage:足し算関数の検証(エラーケース)
AssertEquals:足し算関数検証(エラー) 6 = 7 (failed)  <<==== **FAILED**   sampleTest:developer.export.UnitTest:TestAdditionError
AssertFailure:テストエラー (failed)  <<==== **FAILED**   sampleTest:developer.export.UnitTest:TestAdditionError
        LogMessage:Duration of execution: .000658 sec.
      TestAdditionError failed
    developer.export.UnitTest failed
  Skipping deleting classes 
  sampleTest failed

Use the following URL to view the result:

http://XXX.XXX.XXX.XXX:52773/csp/sys/%25UnitTest.Portal.Indices.cls?Index=59&$NAMESPACE=SAMPLE
Some tests FAILED in suites: sampleFile,sampleTest

これらの情報は、次回確認しましょう。

情報の最後に表示されている下記URLをブラウザのURLに張り付けると、直接確認画面が表示されます。「http://XXX.XXX.XXX.XXX:52773/csp/sys/%25UnitTest.Portal.Indices.cls?Index=59&$NAMESPACE=SAMPLE」

※IPはマスクを掛けています。

おわりに

いかがだったでしょうか。

今回はユニットテストの実行について解説しました。

ユニットテストはただ書いて終わりではなく、「いつ、どのタイミングで実行するか」が非常に重要だと思います。

これについては現場や開発スタイルによって意見が分かれる部分もあり、一定の議論があります。

状況期待できる効果
開発中開発者が修正/追加する度に、開発環境でユニットテストを実行する。
不具合の早期発見が期待できる。
コミット/プッシュ前コミット/プッシュ直前に、自動でユニットテストを実行する。
不具合を含んだソースがリポジトリに入るのを防ぐことができる。
CIでの自動テストプルリクエスト等で自動でユニットテストを実行する仕組みを構築する。
アプリ全体の品質を保全できる。
定期実行定期的に全ユニットテストを実行する。
アプリ全体の品質を保全できる。

ユニットテストの実行は、複数の状況で実行できるように設計すると良いかもしれません。

ObjectScriptは、タスクスケジュールやInteroperabilityがあるので、それらを活用してもよいと思います。

さて、次回はユニットテストの結果を確認しましょう。
ObjectScriptが用意している専用の画面は、かなり癖があるので注意が必要です。