
本記事は3回に渡って、ユニットテストの作成について解説します。
はじめに
ユニットテストとは?
プログラムを構成する最小単位である「関数」や「メソッド」単位で、その正しさを検証するためのテスト手法になります。
ObjectScriptでは、個々のクラスメソッドやインスタンスメソッドの入出力が、仕様通りに動作しているかどうかを確認します。
ユニットテストを導入する最大の目的は、「バグを早期に発見しやすくする事」です。
また、関数ごとにテストを定義しておくことで、将来的なコード変更によって意図しない動作を素早く検知できます。
このように、ユニットテストはコードの品質を高め、保守性を向上させるために欠かせない仕組みだといえます。
本記事は、ユニットテストについて解説を行います。
ユニットテストについて
ObjectScriptでの各役割は下記になります。
| 項目 | 説明 | ObjectScriptの役割 |
|---|---|---|
| テストスイート | 機能的に共通する複数のテストケースをまとめる | フォルダ |
| テストケース | 1つの機能やクラス等において、 一連の振る舞いを検証するテストメソッドの集合体 | クラス |
| テストメソッド | 1つの機能・条件を検証するための最小単位のテスト | メソッド |
ユニットテストを実行すると、結果を専用の画面で確認する事が可能です。
■ユニットテストの結果確認画面

では、テストケースを作成していきましょう!
テストケース(テストクラス)の作成
テストケース(テストクラス)の作成を行います。
では、一つずつ確認していきます。
%UnitTest.TestCaseを継承したクラスを用意する
新規クラスを作成し、Extendsに「%UnitTest.TestCase」を設定します。

これにより、下記が使用可能になります。
- %outUnitTest.incのマクロ
- %UnitTest.TestCase.clsの関数
これらに関しては、後程詳細に説明します。
関数名「Test〇〇〇〇」のテスト用関数を用意する
テスト用のクラスを作成したら、次は動的関数を作成します。
その際、関数名は必ず「Test〇〇〇〇」と命名します。
先ずは、サンプルとして「足し算結果を確認するためのテスト」を作成してみましょう。
名称は「TestAddition」にします。

■現在のテスト用クラスの状態
空の関数が追加されました。
Class developer.export.UnitTest Extends %UnitTest.TestCase
{
/// ユニットテストの一つ。足し算機能のテスト用関数
Method TestAddition()
{
}
}
次は検証用のロジックを記述していきます。
検証用のロジックを作成する
検証用のロジックを記述する前に、検証に使用できるマクロを解説します。
■ユニットテスト用マクロ一覧
| マクロ名 | 説明 |
|---|---|
| $$$AssertEquals(A, B, コメント) | AとBが一致している事を確認 |
| $$$AssertNotEquals(A, B, コメント) | AとBが不一致である事を確認 |
| $$$AssertStatusOK(status, コメント) | ステータスが正常である事を確認 |
| $$$AssertStatusNotOK(status, コメント) | ステータスがエラーである事を確認 |
| $$$AssertTrue([式], コメント) | 式がTrueである事を確認 |
| $$$AssertNotTrue([式], コメント) | 式がFalseである事を確認 |
| $$$AssertFilesSame(fileA, fileB, コメント) | ファイルの内容の一致を確認 |
| $$$AssertFilesSQLUnorderedSame(fileA, fileB, コメント) | SQL クエリ結果を含む 2 つのファイルに、順序付けられていない同じ結果が含まれる事を確認 |
| $$$LogMessage(コメント) | メッセージの登録 |
| $$$AssertSuccess(コメント) | メッセージの登録(成功時) |
| $$$AssertFailure(コメント) | メッセージの登録(失敗時) |
| $$$AssertSkipped(コメント) | メッセージの登録(テストがスキップされた時) |
これらマクロを利用して、検証ロジックを作成していきます。
$$$AssertEquals、$$$AssertNotEquals
$$$AssertEqualsは、2つの値が一致しているかを確認するマクロです。
「関数の戻り値」と「期待値」を比較します。
例えば、下記は2つの引数を足して返す関数になります。
■サンプル用の足し算関数
Class developer.test.Sample
{
ClassMethod Addition(arg1 As %Integer, arg2 As %Integer) As %Integer
{
q arg1 + arg2
}
}上記関数の検証用のテストメソッドを作成します。
→サンプルなので、ログ関連がふんだんに盛り込まれています。
Class developer.export.UnitTest Extends %UnitTest.TestCase
{
/// ユニットテストの一つ。足し算機能のテスト用関数
Method TestAddition()
{
d $$$LogMessage("足し算関数の検証")
// 一致することを検証する
s ans = ##class(developer.test.Sample).Addition(3,4)
, chk = 7
d $$$AssertEquals(ans, chk, $$$FormatText("足し算関数検証(一致) %1 = %2", ans, chk))
// 不一致することを検証する
s ans = ##class(developer.test.Sample).Addition(8,20)
d $$$AssertNotEquals(ans, chk, $$$FormatText("足し算関数検証(不一致) %1 = %2", ans, chk))
d $$$AssertSuccess("テスト成功")
}
}「関数の戻り値(ans)」と「期待値(chk)」を比較しています。
折角のテストサンプルなので、ついでに失敗ケースも作成しておきましょう。
/// 検証エラー
Method TestAdditionError()
{
d $$$LogMessage("足し算関数の検証(エラーケース)")
// 検証エラーを確認する
s ans = ##class(developer.test.Sample).Addition(5,1)
, chk = 7
d $$$AssertEquals(ans, chk, $$$FormatText("足し算関数検証(エラー) %1 = %2", ans, chk))
d $$$AssertFailure("テストエラー")
}これで、テスト実行時に検証エラーを確認する事ができます。
$$$AssertStatusOK、$$$AssertStatusNotOK
このマクロは、ステータスの状態を検証するマクロになります。
サンプル関数は、データの「登録(%Save)」と「削除(%DeleteId)」を行います。
→ 両関数は共に戻り値が「%Status」です。
下記に新しいテストクラスを用意しました。
■サンプル用のデータクラス
データの登録と削除用に「save」「delete」の関数を用意しています。
Class developer.test.TestData Extends %Persistent
{
Index idx On test [ PrimaryKey ];
Property test As %String;
ClassMethod save(name As %String) As %Status
{
s o = ..%New()
, o.test = name
q o.%Save()
}
ClassMethod delete(name As %String) As %Integer
{
&sql(select id into :id from developer_test.TestData where test=:name)
q:(SQLCODE=100) $$$ERROR($$$CacheError, "データがありません name = "_name)
q ..%DeleteId(id)
}
}■ステータスを検証するテスト関数
Class developer.export.UnitTestObject Extends %UnitTest.TestCase
{
Parameter NAME = "サンプルテスト";
/// ユニットテスト:オブジェクトの登録
Method TestSave()
{
// データ登録テスト
s sts = ##class(developer.test.TestData).save(..#NAME)
d $$$AssertStatusOK(sts, $$$FormatText("データの登録 name = %1", ..#NAME))
// ユニークエラー
s sts = ##class(developer.test.TestData).save(..#NAME)
d $$$AssertStatusNotOK(sts, $$$FormatText("登録時エラー %1 = ", $system.Status.GetErrorText(sts)))
}
/// ユニットテスト:オブジェクトの削除
Method TestDelete()
{
// データ削除テスト
s sts = ##class(developer.test.TestData).delete(..#NAME)
d $$$AssertStatusOK(sts, $$$FormatText("データの削除 name = %1", ..#NAME))
}
}
「save」「delete」の戻り値「%Status」を、マクロで検証しています。
ただし、このテストには現状、下記2点の「問題」が存在しています。
次は、これらを解決したいと思います。
テスト環境を整備する関数を使用する
テスト実施前に、テスト環境を整備する必要があれば、下記メソッドを利用して環境を整えます。
| 関数名 | 説明 |
|---|---|
| OnBeforeOneTest | テスト・クラスの各メソッドの直前に実行 |
| OnBeforeAllTests | テスト・クラス実行前に1回実行 |
| OnAfterOneTest | テスト・クラスの各メソッドの直後に実行 |
| OnAfterAllTests | テスト・クラス実行後に1回実行 |
今回のテストケースで言えば、レコードの削除や登録になります。
■環境整備用の関数
Class developer.export.UnitTestObject Extends %UnitTest.TestCase
{
Parameter NAME = "サンプルテスト";
/// テスト前に一度だけ実行する
Method OnBeforeAllTests() As %Status
{
// 全レコードの削除を実施
q ##class(developer.test.TestData).%KillExtent()
}
/// テスト クラス内の各テスト メソッドが実行される直前に <B>RunTest</B> によって実行されます。<br>
/// <dl>
/// <dt><i>testname</i>
/// <dd>実行するテストの名前。必須。
/// </dl>
Method OnBeforeOneTest(testname As %String) As %Status
{
i (testname="TestSave") {
// 特になし
}elseif(testname="TestDelete"){
// レコードを作成して削除を実施可能に
d ##class(developer.test.TestData).save(..#NAME)
}
q $$$OK
}
/// ユニットテスト:オブジェクトの登録
Method TestSave()
{
// データ登録テスト
s sts = ##class(developer.test.TestData).save(..#NAME)
d $$$AssertStatusOK(sts, $$$FormatText("データの登録 name = %1", ..#NAME))
// ユニークエラー
s sts = ##class(developer.test.TestData).save(..#NAME)
d $$$AssertStatusNotOK(sts, $$$FormatText("登録時エラー %1 = ", $system.Status.GetErrorText(sts)))
}
/// ユニットテスト:オブジェクトの削除
Method TestDelete()
{
// データ削除テスト
s sts = ##class(developer.test.TestData).delete(..#NAME)
d $$$AssertStatusOK(sts, $$$FormatText("データの削除 name = %1", ..#NAME))
}
/// テスト クラス内の各テスト メソッドが実行された直後に <B>RunTest</B> によって実行されます。<br>
/// <dl>
/// <dt><i>testname</i>
/// <dd>Name of the test to be run. Required.
/// </dl>
Method OnAfterOneTest(testname As %String) As %Status
{
// テスト終了毎に、全レコードの削除を実施
q ##class(developer.test.TestData).%KillExtent()
}
}これで、どのテストメソッドを単独で実行しても、エラーにならないでしょう。
その他のマクロも試してみる
検証用のマクロは他にもあるので、ついでに動作を検証してみましょう。
$$$AssertTrue、$$$AssertNotTrue
値がTrueかFalse(%Boolean値)を検証するマクロになります。
True or Falseなので、%Fileの操作が検証しやすそうです。
ファイル操作は、%Booleanの戻り値を持つ関数が多いですよね。
■検証用のファイル操作関数
/// ファイル削除を検証する関数
ClassMethod deleteFile(filePath As %String) As %Boolean
{
// ファイルの存在を確認
s flg = ##class(%File).Exists(filePath)
q:('flg) flg
// ファイルの削除
q ##class(%File).Delete(filePath)
}
ClassMethod makeFile(filePath As %String) As %Boolean
{
// ファイルの作成
s flg = ##class(%File).Exists(filePath)
q:(flg) 0
// ファイルの作成
s file=##class(%File).%New(filePath)
d file.Open("WN")
, file.WriteLine("サンプルテキスト出力")
, file.%Save()
, file.Close()
k file
q 1
}上記関数の検証用の新規テストクラスとメソッドを作成します。
Class developer.export.UnitTestFile Extends %UnitTest.TestCase
{
Parameter FILEPATH = "D:\Temp\txt\UnitTest.txt";
/// テスト前にファイルを生成する
Method OnBeforeAllTests() As %Status
{
d ##class(developer.test.Sample).makeFile(..#FILEPATH)
q $$$OK
}
/// ユニットテスト:ファイルの削除
Method TestFileDelete()
{
// ファイルの削除
s flg = ##class(developer.test.Sample).deleteFile(..#FILEPATH)
d $$$AssertTrue(flg, $$$FormatText("ファイルの削除 ファイル名 = %1", ..#FILEPATH))
// Falseが返る
s flg = ##class(developer.test.Sample).deleteFile(..#FILEPATH)
d $$$AssertNotTrue(flg, $$$FormatText("ファイルの削除(エラー)", ..#FILEPATH))
}
}環境整備として、ファイルの生成を行い、テストメソッドとしてファイルの削除を検証したいと思います。
$$$AssertFilesSame、$$$AssertFilesSQLUnorderedSame
ファイルの内容が一致しているか検証します。
ファイルの比較を行うテストメソッドを作成しました。
Class developer.export.UnitTestFileCheck Extends %UnitTest.TestCase
{
Parameter DIR = "D:\Temp\txt\";
Parameter FILENM = "UnitCopyTest.txt";
Method OnBeforeAllTests() As %Status
{
d ##class(developer.test.Sample).makeFile(..#DIR_..#FILENM)
q $$$OK
}
/// テスト クラス内の各テスト メソッドが実行される直前に <B>RunTest</B> によって実行されます。<br>
/// <dl>
/// <dt><i>testname</i>
/// <dd>実行するテストの名前。必須。
/// </dl>
Method OnBeforeOneTest(testname As %String) As %Status
{
i (testname="TestFileCheck"){
k %zFilePath
}elseif(testname="TestFileNoCheck"){
k %zFilePath
}
q $$$OK
}
Method TestFileCheck()
{
// ファイルのコピー
s fileNm = $p(..#FILENM, ".", 1)
, ext = $p(..#FILENM, ".", 2)
, %zFilePath = ..#DIR_fileNm_"copy."_ext
d ##class(%File).CopyFile(..#DIR_..#FILENM, %zFilePath)
// ファイルのチェック
d $$$AssertFilesSame(..#DIR_..#FILENM, %zFilePath)
}
Method TestFileNoCheck()
{
// ファイルのコピー
s fileNm = $p(..#FILENM, ".", 1)
, ext = $p(..#FILENM, ".", 2)
, %zFilePath = ..#DIR_fileNm_"copy."_ext
s file=##class(%File).%New(%zFilePath)
d file.Open("WN")
, file.WriteLine("内容の不一致")
, file.%Save()
, file.Close()
k file
// ファイルのチェック(内容の不一致)
d $$$AssertFilesSame(..#DIR_..#FILENM, %zFilePath)
}
Method TestQueryPlan()
{
s fileNm1 = "queryPlan1.txt"
, fileNm2 = "queryPlan2.txt"
d $$$AssertFilesSQLUnorderedSame(..#DIR_fileNm1, ..#DIR_fileNm2)
}
/// Run by <B>RunTest</B> immediately after each test method in the test class is run.<br>
/// <dl>
/// <dt><i>testname</i>
/// <dd>Name of the test to be run. Required.
/// </dl>
Method OnAfterOneTest(testname As %String) As %Status
{
i (testname="TestFileCheck"){
s flg = ##class(developer.test.Sample).deleteFile(%zFilePath)
k %zFilePath
}elseif(testname="TestFileNoCheck"){
s flg = ##class(developer.test.Sample).deleteFile(%zFilePath)
k %zFilePath
}
q $$$OK
}
/// Run by <B>RunTest</B> once after all test methods in the test class are run. Can be used to tear down a test environment that was set up by <B>OnBeforeAllTests</B> See example in <b>OnBeforeAllTests</b>.
Method OnAfterAllTests() As %Status
{
d ##class(developer.test.Sample).deleteFile(..#DIR_..#FILENM)
k %zFilePath
q $$$OK
}
}$$$AssertFilesSQLUnorderedSame
ファイルの内容が一致しない場合、記載内容が「クエリプラン」か「SQLクエリ」の場合、挙動が変わります。
マクロ内部で下記関数「parseSQLFile」が実行され、クエリの内容を分解しています。
s file1="D:\Temp\txt\queryPlan1.txt"
Open file1:"r":0 Else Quit 0
d ##class(%UnitTest.TestCase).parseSQLFile(file1,.parsed,0,0)
Close file1
zw parsed後は、両ファイルの要素を比較しているようです。
クエリプランは「<plan>~</plan>」で囲まれていて、クエリは「SQL>」で始まる必要があります。
今回は、下記2点を用意しました。
<plans>
<plan>
<sql>
SELECT ID,ABO血液型,RH血液型,dele,patientId,カナ氏名,コメント,性別,漢字氏名,生年月日 FROM developer_data.Patient2 where 漢字氏名 like '九重山%' & ABO血液型 = 'O' & RH血液型 = '+' & 性別 = 1 /*#OPTIONS {"DynamicSQL":1} */
</sql>
<cost value="2273924"/>
Call module B, which populates bitmap temp-file A.
Read bitmap temp-file A, looping on ID.
For each row:
Read master map developer_data.Patient2.IDKEY, using the given idkey value.
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(性別).
Output the row.
<module name="B" top="1">
Read index map developer_data.Patient2.i2, looping on %SQLUPPER(漢字氏名) (with a %STARTSWITH range condition), %SQLUPPER(カナ氏名), 生年月日, and ID.
For each row:
Add ID bit to bitmap temp-file A.
</module>
</plan>
</plans>SQL>
SELECT
ID,
ABO血液型,
RH血液型,
dele,
patientId,
カナ氏名,
コメント,
ローマ字氏名,
性別,
新患登録日,
死亡日時,
漢字氏名,
生年月日
FROM
developer_data.Patient2
where
漢字氏名 like '九重山%' &
ABO血液型 = 'O' &
RH血液型 = '+' &
性別 = 1クエリプランの出力は下記を参考にして下さい。
おわりに
いかがだったでしょうか。
本記事では、ユニットテストの作成を中心に解説致しました。
システムに組み込むには、かなりの開発コストがかかるとは思いますが、長期運用を行うのであれば作成して損はしないと思います。
さて、次回はユニットテストの実行編になります。
今回準備した3つのクラスを使って、ユニットテストを実施しましょう!


