投稿者

インターシステムズジャパン
記事 Toshihiko Minamoto · 9月 20, 2023 14m read

ユニットテスト: ObjectScript コードの品質

主流となっているソフトウェア開発手法では、必ずテスト専用の項目が用意されています。 これは、デリバリーの品質を持続的に達成する上では欠かせないアプローチです。

テストには、以下の 2 種類があります。

  1. ホワイトボックステスト: これらは、ソースコードとアプリケーションの機能の品質を調べるテストです。 この種のテストには、以下のようなテストがあります。
    1. 静的解析: 静的解析ソリューションは、ソースコードの解析に使用されます(テスト時に機能は実行されません)。命名パターン、インデント、宣言された未使用の変数、コンポーネント間のインデックスの結合など、定義された条件が解析ソリューション内で評価されます。 一般に、開発環境内では、品質に問題のあるソースコードの行が指摘されるため、開発者は問題の解決に取り組むことができます。
    2. ユニットテスト: テストクラス(テストユニット)が作成されます。機能トピック(顧客の維持、トランザクションの収集など)ごとに 1 つのテストクラス(テストユニット)が作成され、テストスイート(一連のテストユニット)にまとめられます。アプリケーションモジュールごと(登録モジュール、販売モジュールなど)、またはアプリケーション全体に 1 つのスイートがあります。 一般に、検出された結果を含む HTML レポートが発行されます。
  2. ブラックボックステスト: アプリケーションのソースコードにアクセスせずに、アプリケーションのバイナリーで実行されるテストです。 この種のテストには、以下のようなテストがあります。
    1. パフォーマンステスト: データの読み込みと実行スクリプトは主要な機能に対して定義され、アプリケーションと実行環境が期待されるトランザクション数とユーザー数をサポートするかどうかを評価します。
    2. 進入テスト(ペンテスト): アプリケーションの IP アドレスとポート、ソリューションをホストする製品(Web サーバー、アプリケーション、データベース)、およびアプリケーションのエンドポイント(API、Web サービス、ファイル交換ディレクトリ、およびその他の対話ポイント)には、脆弱性分析ソリューションが使用されます。 通常、検出された脆弱性を記載する PDF レポートが作成されます。
    3. 機能テスト: テストアナリストとテスターは、アプリケーションのビジュアルインターフェースから機能的な欠陥(エンドユーザーの観点でエラーのある機能)を特定することを目的とした複数のテストシナリオを作成して実行します。 現在では、これらのビジュアルインターフェースからのテストも自動化するソリューションがあります。

テスト規律の重要な目標は、テストカバレッジ(テストが対応する機能)のスイートスポットを見つけることです。 私が関わっているプロジェクトでは、少なくとも機能の 75% を達成することが目標とされています。 大きな秘密は、テストの焦点をアプリケーションのビジネスレイヤーに当てることにあります。このレイヤーに、エンドユーザーに提供されるビジネスルールと機能要件の実装が存在するためです。 様々なビジュアルインターフェースと統合ポイントには、かなわずビジネスレイヤーが必要となります。 このため、機能レイヤーのテストを自動化する場合には、テストのカバレッジに適切な割合が設定されます。

この記事では、IRIS アプリケーションのビジネスレイヤーのユニットテストを自動化する方法を説明し、エンドユーザーに提供される IRIS アプリケーションで適切なテストカバレッジの割合と品質を達成する方法を紹介します。

テストするアプリのダウンロード

https://openexchange.intersystems.com/package/global-mindmap に移動し、Github ボタンをクリックします。 以下の手順に従ってください。

  1. プロジェクトをクローンします。
$ git clone https://github.com/yurimarx/global-mindmap.git

  1. プロジェクトのルートフォルダ内で docker build を実行します。
$ docker-compose build

  1. Docker コンテナーを実行します。
$ docker-compose up -d

アプリの実際の動作を見る

テストを開始する

テストするビジネスクラス

 
テストするビジネスクラスターゲット

Class dc.globalmindmap.GlobalMindMapService
  
{
 
/// mindmap ノードを格納
ClassMethod StoreMindmapNode(data As %DynamicObject) As %Status
{
    Try {
 
        Set ^mindmap(data.id) = data.id
        Set ^mindmap(data.id, "topic") = data.topic
        Set ^mindmap(data.id, "style", "fontSize") = data.style.fontSize
        Set ^mindmap(data.id, "style", "color") = data.style.color
        Set ^mindmap(data.id, "style", "background") = data.style.background
        Set ^mindmap(data.id, "parent") = data.parent
        Set ^mindmap(data.id, "tags") = data.tags.%ToJSON()
        Set ^mindmap(data.id, "icons") = data.icons.%ToJSON()
        Set ^mindmap(data.id, "hyperLink") = data.hyperLink
 
        Return 1
 
    } Catch err {
        write !, "Error name: ", ?20, err.Name,
            !, "Error code: ", ?20, err.Code,
            !, "Error location: ", ?20, err.Location,
            !, "Additional data: ", ?20, err.Data, !
        Return
    }
}
 
ClassMethod GetMindmap(Output Nodes As %DynamicArray) As %Status
{
    Try {
     
      Set Nodes = []
 
      Set Key = $ORDER(^mindmap(""))
      Set Row =
     
      While (Key '= "") {
        Do Nodes.%Push({})
        Set Nodes.%Get(Row).style = {}
        Set Nodes.%Get(Row).id = Key
        Set Nodes.%Get(Row).hyperLink = ^mindmap(Key,"hyperLink")
        Set Nodes.%Get(Row).icons = ^mindmap(Key,"icons")
        Set Nodes.%Get(Row).parent = ^mindmap(Key,"parent")
        Set Nodes.%Get(Row).style.background = ^mindmap(Key,"style", "background")
        Set Nodes.%Get(Row).style.color = ^mindmap(Key,"style", "color")
        Set Nodes.%Get(Row).style.fontSize = ^mindmap(Key,"style", "fontSize")
        Set Nodes.%Get(Row).tags = ^mindmap(Key,"tags")
        Set Nodes.%Get(Row).topic = ^mindmap(Key,"topic")
        Set Row = Row + 1
       
        Set Key = $ORDER(^mindmap(Key))
      }
     
      Return 1
   
    } Catch err {
      Write !, "Error name: ", ?20, err.Name,
          !, "Error code: ", ?20, err.Code,
          !, "Error location: ", ?20, err.Location,
          !, "Additional data: ", ?20, err.Data, !
      Return
    }
}
 
/// mindmap ノードを削除する
ClassMethod DeleteMindmapNode(id As %String) As %Status
{
    Try {
     
      Kill ^mindmap(id)
     
      Return 1
 
    } Catch err {
      write !, "Error name: ", ?20, err.Name,
          !, "Error code: ", ?20, err.Code,
          !, "Error location: ", ?20, err.Location,
          !, "Additional data: ", ?20, err.Data, !
      Return
  }
}
 
/// Has Content(コンテンツがあるかどうか): 1 - ある、0 - ない
ClassMethod HasContent(Output Result As %String) As %Status
{
    Try {
     
        Set key = $ORDER(^mindmap(""))
     
        If key = "" {
            Set Result = "0"
        } Else {
            Set Result = "1"
        }
 
      Return 1
   
    } Catch err {
      Write !, "Error name: ", ?20, err.Name,
          !, "Error code: ", ?20, err.Code,
          !, "Error location: ", ?20, err.Location,
          !, "Additional data: ", ?20, err.Data, !
      Return
    }
}
 
}

ビジネスクラスには、テストする機能が 3 つあります。

  1. StoreMindmapNode: mindmap ノードのデータをグローバルの形態でデータベースに記録するために使用される機能。
  2. GetMindmap: グローバルに格納された mindmap のすべてのノード、つまり mindmap 全体を返すために使用される機能。
  3. DeleteMindmapNode: データベースから mindmap ノードを削除するために使用される機能(ノードを格納するグローバルノードを削除します)。

テストスイート(テストケース一式)を作成する

src ディレクトリのルートに、テストスイートの名前でディレクトリを作成します。この例では、名前を UnitTests としました。

テストスイートテストケースを作成する

%UnitTest.TestCase を継承するテストクラスを作成する必要があります。 クラス名は、テストするクラス名 + Test の形式にすることをお勧めします。 この例では、クラス名を GlobalMindMapServiceTest としています。 クラスの内容を見てみましょう。

 
テストケースクラス

Class UnitTests.GlobalMindMapServiceTest Extends %UnitTest.TestCase
  
{
 
Method TestStoreMindmapNode()
{
    Set data = {}
    Set data.id = "TestId"
    Set data.topic = "TestTopic"
    Set data.parent = ""
    Set data.tags = []
    Set data.icons = []
    Set data.style = {}
    Set data.style.background = "black"
    Set data.style.fontSize = "arial"
    Set data.style.color = "white"
    Set data.style.hyperLink = "intersystems.com"
    Do ##class(dc.globalmindmap.GlobalMindMapService).StoreMindmapNode(data)
    Set Key = ^mindmap(data.id)
    Do $$$AssertEquals(data.id, Key)
    Do ##class(dc.globalmindmap.GlobalMindMapService).DeleteMindmapNode(data.id)
}
 
Method TestDeleteMindmapNode() As %Status
{
    Set data = {}
    Set data.id = "TestDelete"
    Set data.topic = "TestDelete"
    Set data.parent = ""
    Set data.tags = []
    Set data.icons = []
    Set data.style = {}
    Set data.style.background = "black"
    Set data.style.fontSize = "arial"
    Set data.style.color = "white"
    Set data.style.hyperLink = "intersystems.com"
    Do ##class(dc.globalmindmap.GlobalMindMapService).StoreMindmapNode(data)
    Do ##class(dc.globalmindmap.GlobalMindMapService).DeleteMindmapNode(data.id)
    Set Key = $ORDER(^mindmap(data.id))
    Do $$$AssertEquals("", Key)
}
 
Method TestGetMindmap()
{
    Set data = {}
    Set data.id = "TestGet"
    Set data.topic = "TestGet"
    Set data.parent = ""
    Set data.tags = []
    Set data.icons = []
    Set data.style = {}
    Set data.style.background = "black"
    Set data.style.fontSize = "arial"
    Set data.style.color = "white"
    Set data.style.hyperLink = "intersystems.com"
    Do ##class(dc.globalmindmap.GlobalMindMapService).StoreMindmapNode(data)
    Do ##class(dc.globalmindmap.GlobalMindMapService).GetMindmap(.Result)
    Set Count = Result.%Size()
    Do $$$AssertEquals(Count, 1)
    Do ##class(dc.globalmindmap.GlobalMindMapService).DeleteMindmapNode(data.id)
}
 
}

テストされる各機能について、「Test + テストされるメソッド名」の命名規則に従ったメソッドがあることに注意してください。 それぞれのテストメソッドが、テストの実行に必要なものすべてを行うことが重要です。 例えばテストの削除の部分では、まずレコードが作成されてから、それを使用して削除し、削除が機能しているかどうかがテストされます。

各メソッドについては、$$$Assert... への呼び出しが少なくとも 1 つ必要です。 このマクロによって、テスト条件そのものの実行と記録が行われます。 この例では、すべてのメソッドに $$$AssertEquals が使用されています。 この場合、テスト条件がビジネスメソッドの実行結果に等しい場合、テストは成功としてマークされ、そうでない場合は不成功とされます。 Assert にはいくつかのオプションがあります。詳細は、https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=TUNT_AssertX をご覧ください。

OnBeforeAllTests を使用してテストに必要なデータと条件を作成し、OnAfterAllTests を使用してデータと環境を元の状態にクリーニングすることも可能です。

テストを実行する

スイートとテストケースの準備ができたら、テストを実行しましょう。 これには、ターミナルでクラスが存在するネームスペースにアクセスする必要があります。例としてこの記事の手順をご覧ください。

  1. ターミナルのネームスペース IRISAPP で、ビジネスクラスとテストクラスを含むネームスペースに移動します。
USER>zn "IRISAPP"

  1. テストスイートのフォルダが存在するフォルダをポイントする必要があります(この例では src 内の UnitTests フォルダです)。
IRISAPP>Set ^UnitTestRoot="/opt/irisbuild/src"

  1. ユニットテストを実行します。
IRISAPP>do ##class(%UnitTest.Manager).RunTest("UnitTests")

  1. http://localhost:52773/csp/sys/%25UnitTest.Portal.Indices.cls?Index=1&$NAMESPACE=IRISAPP で結果を確認します。

結果を分析する

前の手順から得るレポートは、以下のレイアウトで返されます。

何が合格(緑)し、何に失敗(赤)したかを知ることができます。 赤をクリックすると、エラーが発生した場所を確認できます。

修正しましょう。

サンプルアプリケーションを気に入った方は、グローバルコンテストで投票してください。

元の記事へ さんが書いた @Yuri Marx