設計向けとテスト設計向けの適切な仕様表現の差異(デシジョンテーブルを例として)

設計向けとテスト設計向けでは、適切な仕様表現について認識に差異が出ることがあります。

題材として、以下の仕様をデシジョンテーブルで表現する場合を考えます:

題材とする仕様

  • 組み込み機器の単軸制御モード機能が対象

  • この機能は次の3つの入力を持つ
    • 省電力化モード(とり得る値:ON、OFF)
    • 制御方針(とり得る値:精密重視、速度重視)
    • 目標との距離(とり得る値:整数値[cm])

  • この機能は次の出力を持つ
    • 速度モード(とり得る値:高速モード、中速モード、低速モード)

  • この機能は出力の速度モードを次のように決定する
    • 省電力化モードがONの場合、低速モードで固定する
    • 省電力化モードがOFFの場合、以下に従う
      • 目標との距離が10cm未満の場合、低速モードを選択する
      • 目標との距離が10cm以上かつ、制御方針が精密重視なら、中速モードを選択する
      • 目標との距離が10cm以上かつ、制御方針が速度重視なら、高速モードを選択する

 

設計向けのデシジョンテーブルの例

設計では、実装の方針がわかる前提下ならば、デシジョンテーブルを簡単化しても支障はありません。むしろ必要十分な範囲内で情報を簡単化した方が、都合が良い場合が多いです。

今回の仕様では、以下のようにデシジョンテーブルを表記できます。

f:id:goyoki:20200628225659p:plain

 

テスト設計向けのデシジョンテーブルの例

一方、テスト設計向けに、デシジョンテーブル法に基づいてテーブルを作る場合、上記のようにデシジョンテーブルを簡単化できるとは限りません。どこまで簡単化できるかが、処理順序や品質リスクに依存するためです。

まず以下のような実装が行われている場合について考えます。

# 速度モード設定処理()にはモードに応じた障害のリスクがあるとする

def 単軸制御モード機能(省電力化, 制御方針, 目標との距離):
    if 省電力化 == ON:
        速度モード設定処理(低速モード) #1
        return

    if 目標との距離 < 10:
        速度モード設定処理(低速モード) #2
        return
    
    if 制御方針 == 精密重視:
        速度モード設定処理(中速モード) #3
        return
    else:
        速度モード設定処理(高速モード) #4
        return

この場合、シンプルな分岐構造で仕様が実装されているため、前述の簡単化デシジョンテーブルを使っても問題ありません。テスト技法に則り、デシジョンテーブルの各列をテストケースに展開すれば、すべての速度モード設定処理()を網羅するテストケースを用意できます。

一方で、次のような実装が行われている場合について考えます。

# 速度モード設定処理()にはモードに応じた障害のリスクがあるとする

def 単軸制御モード機能2(省電力化, 制御方針, 目標との距離):
    if 制御方針 == 精密重視:
        if 省電力化 == ON:
            速度モード設定処理(低速モード) #1
        elif 目標との距離 < 10:
            速度モード設定処理(低速モード) #2
        else:
            速度モード設定処理(中速モード) #3
        return
    else:
        if 省電力化 == ON:
            速度モード設定処理(低速モード) #4
        elif 目標との距離 < 10:
            速度モード設定処理(低速モード) #5
        else:
            速度モード設定処理(高速モード) #6
        return

この実装の場合、前述の簡単化したデシジョンテーブルに従ってテストケースを導出すると、一部の速度モード設定処理を網羅できず品質リスクが残ります。

この実装に基づいて適切にデシジョンテーブルを簡単化するならば、一例として以下のようなものになるでしょう。

f:id:goyoki:20200628225724p:plain

また機能の処理が複雑なコードで分散実装されていたり、見えにくい品質リスクが散在していたりする場合だと、実装から順序性や品質リスクが読み取れなくなる場合があります。そうなると、デシジョンテーブルは全く簡単化できない場面もあり得ます。

すなわち、設計と同じように簡単化できない場合があるということです。

なおこの話の参考文献として、 ソフトウェアテスト技法練習帳 に解説があり、お勧めできます。

 

設計向けとテスト設計向けの適切な仕様表現の差異

テストの導出元にするデシジョンテーブルをどこまで簡単化できるかは、順序性、広く言えば品質リスクに依存します。そしてテスト設計ではより包括的に品質リスクに対応する必要があることから、設計のための仕様より、簡単化できない場面が多いです。言い換えると、テスト設計では、設計と比べ、より広い視点で、情報が補強された仕様表現が求められる場面が多いです。

なお「より広い視点で、情報が補強された仕様表現が求められる」といっても、現実でそれを十分に実現できる場面は多くありません。テスト用に仕様を作り直すコストはないのが普通ですし、そもそも品質リスクは広く・見えにくく潜在していて、仕様化ですべて対応できないためです。その現実に立ち向かうため、以下のような活動が重要になってくると感じます。

  • 設計や実装でのリスクコントロールの工夫。少ないテストでも大丈夫なように作る。
  • 探索的テストによる補完。仕様の情報不足を、テストエンジニアの能力や学習結果で補完する。
  • グレーボックステストの拡充。設計やコードの構造に基づいてテスト設計の穴を補強する。
  • 無則のテストの導入。ペアワイズ法などで仕様化されていない条件もテストする。
  • 品質保証の重ね合わせ。コードレビュー、ユニットテストシステムテスト、専任チームによる品質保証などを組み合わせて、品質リスクの見落としを減らす。

UTP2(UML Testing Profile 2)でテスト設計の成果物を一通り表現する

 UMLのプロファイル(特定用途向けに、ステレオタイプ、タグ付き値、モデル要素の制約・関係性の拡張をくわえたもの)の中に、ソフトウェアテストのためのプロファイルUTP2(UML Testing Profile 2)があります。

 UTP2は、テストでモデルベースドアプローチを実現して、ソフトウェアテストの各種活動でモデリングの恩恵を受けることを目的としています。恩恵とは、例えばテスト分析・設計での思考をモデルで補助する、トレーサビリティを明示化する、テスト成果物のレビューなどの協業を支える、モデル駆動などのツールの恩恵を得る、といったものです。
 UTP2の内容は、UMLによる開発のモデリングのこれまでの蓄積を活かして、テスト環境とテスト実行の仕様定義が多めとなっています。
 現状は知名度・普及度は低い状態です。今後普及するかは、使い勝手の良いモデリングツールが出てくるかに依存していると思います。

 今回はUTP2の紹介として、テスト設計の一通りの活動の成果物を、UTP2で表現していきたいと思います。

前置き

 今回は以下の2.1betaに基づいてテスト設計を行います。

UML Testing Profile 2.1beta
https://www.omg.org/spec/UTP2/About-UTP2/

 なおUTP2は仕様が大きく、使用する際はテーラリングした上での部分採用の形をとることになります(実際、UTP2仕様書中のExampleは、それぞれでかなり仕様を絞り込んでいて表現がばらばらになっています)。今回の例示もUTP2の一部の仕様のみを使用しています。

題材

 今回は医療用デジタル体温計のプロダクトシステムを対象とします。

UTP2に基づいたテストの活動

テストコンテキストの定義

 UTP2ではまずテストの活動をテストコンテキストの集まりで表現します(注:ここでいうテストコンテキストは、UTP2独特の定義づけがされた言葉です)。

 テストコンテキストは、以降のテストの活動が行いやすいように、テストレベルやテストタイプなどを基準に、テストの活動を分けたものです。テストコンテキストは、テストの成果物を含む、テストの活動にかかわる情報の集合として表現されます。

 比喩を使うと、例えばシステムを開発する際は、システムをサブシステムに分割し、サブシステムごとに分かれて開発を進めます。そこでいうシステムとサブシステムの関係が、テスト活動全体とテストコンテキストの関係に当てはまります。

 テストコンテキストはTestContextをステレオタイプにもつパッケージと定義されており、パッケージ図でモデリングします。

 図中のとおり、テストコンテキストはテストタイプとテストレベルをパラメータとして持っています。
 今回はこのうち、システムテストの機能正確性テストに属する、温度計測精度テストに絞ってテスト設計を進めます。

 なおテストコンテキストを定義するためには、その前にテストレベルやテストタイプの分析・計画が必要です。UTP2ではテストタイプとテストレベルをValueSpecificationと定義しており、分析はクラス図あるいはオブジェクト図で行うことになります。ただそれらテストコンテキスト定義以前の活動については、UTP2でほとんど扱われていないため今回は省きます。

テスト目的とテスト要求の分析

 次にテストコンテキストごとにテスト分析・設計を進めていきます。

 まず、テストコンテキストのテスト目的を、以降のテスト分析ができるようになるまで具体化します。テスト目的はTestObjectiveのステレオタイプを付与したクラスと定義されており、クラス図でモデリングしていきます。

 テスト目的を具体化したら、その目的に基づいてテスト要求を定義します。テスト要求は、テストしたいと思えるテスト対象の特質です。テスト目的を満たすように、テスト対象の要求を参照しながら定義します。テスト要求はTestReqirementのステレオタイプを付与したクラスと定義されており、クラス図でモデリングしていきます。

テスト設計

 次に、テスト要求とテスト目的からテスト設計方針を立て、その方針に基づいてテストケースを作成します。モデルにおいては、テスト設計方針にはTestDesignDirective、テストケースにはTestCaseのステレオタイプを付与して表現します。
 テスト要求・テスト設計方針・テストケースの関係性については、2.1beta時点のUTP2仕様書では仕様定義と文章による解説が中心で、一貫性を持った例示を行っていません。ただ以下のようなモデルで表現できると思います。

 なお上記ではテスト要求との関係を表現するためクラス図を用いていますが、TestDesignDirectiveはインスタンスと定義されており、それ単体でモデリングする際はオブジェクト図を用いることになります。

 別の活用例として、SysMLのようにテスト要求モデルにテストケースを関連付けて、両者のトレーサビリティをモデルで表現する方法も提示されています。またテスト設計方針や、テストケースの集まりであるテストセットは、オブジェクト図やパッケージ図で構造化しながら設計を進めることもできます。

テスト環境の設計

 一方、テストを実行するためのテスト環境をモデリングします。テスト環境はTest Configurationとして表現されます。テスト対象にTestItem、テスト環境のコンポーネントにTestComponentのステレオタイプを付与して、クラス図やコンポジット構造図でモデリングします。


テスト実装

 そしてテストケースごとにテストケースの手順を作成します。
 手順はTestCaseのステレオタイプを付与したふるまいモデルで表現します。今回は仕様書の例示に従ってシーケンス図でモデリングしています。

Prometheusでファイルサービスディスカバリを使って監視対象を動的に変える

監視対象が動的に増減するような状況においてPrometheusで監視を行う場合、サービスティスカバリ機能が便利です。今回はその例として、Prometheusのファイルサービスディスカバリで監視対象を動的に変更する方法についてまとめます。

動作環境の構築

まずdocker-composeで最低限の環境を構築します。
次のディレクトリ構成を用意します。

docker-compose.yaml 
prometheus/
  prometheus.yaml
  targets.json

docker-compose.yamlは次の通りです。prometheusとnode-exporterのdockerコンテナを用意します。

version: '3'
services:
  prometheus:
    image: prom/prometheus
    container_name: prometheus
    volumes:
      - ./prometheus:/etc/prometheus
    command: "--config.file=/etc/prometheus/prometheus.yaml"
    ports:
      - 9090:9090
    restart: always
  exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    ports:
      - 9100:9100
    restart: always

prometheus.yamlは次の通りです。
ファイルサービスディスカバリを使用する場合、scrape_configsにfile_sd_configsを指定し、その中のfilesにglob形式でターゲットファイルへのパスを記入します(今回は*.json。このyamlファイルからの相対パスで記述)

global:
  scrape_interval: 15s
scrape_configs:
  - job_name: 'node'
    file_sd_configs:
      - files:
        - '*.json'

すると、prometheusはglob形式で指定したファイルを探し出して監視対象に加えてくれるようになります。

targets.jsonは次の通りです。

[
  {
    "labels": {
      "job": "node"
    },
    "targets": [
      "node-exporter:9100"
    ]
  }
]

prometheusで最初の状態を確認する

「docker-compose up -d」でprometheusとnode-exporterを立ち上げます。
「http://【実行環境のアドレス】:9090/graph」にアクセスし、Expressionに「up{job="node"}」を入力してExecuteします。すると以下の結果が出力されます。

Element	Value
up{instance="node-exporter:9100",job="node"}	1

targets.jsonで指定した監視対象がTargetに加えられていることがわかります。

ターゲットファイルを追加して監視対象を変更する

次にprometheusを実行したまま監視対象を変えてみます。
targets.jsonと同じディレクトリに次のtargets2.jsonを追加します。

[
  {
    "labels": {
      "job": "node"
    },
    "targets": [
      "prometheus:9090"
    ]
  }
]

再度prometheusのgraphページにアクセスし「up{job="node"}」をExecuteします。すると以下の結果が出力されます。

Element	Value
up{instance="node-exporter:9100",job="node"}	1
up{instance="prometheus:9090",job="node"}	1

targets2.jsonでの追加分が、監視対象に反映されていることがわかります。


Github Actionsでテストや静的解析に失敗するプルリクエストをマージできないようにする

Github Actionsを活用するとGithub内でCIを完結できます。その活用例として、今回はプリリクエスト作成時にユニットテストと静的解析を実行し、それらが成功しないとプルリクエストのマージを拒絶する仕組みをGithub Actionsで作ります。

対象コード

対象ソースコードは以下です。

https://github.com/hiro-iseri/blog_demo

プロダクトコードをdemoディレクトリに、ユニットテストをtestsディレクトリに格納する構成をとります。

python-package.ymlの作成

/.github/workflows/ディレクトリにpython-package.ymlを作成し、Github Actionsの設定を記述します。
今回の設定内容ですが、Python3.8環境にて、プロダクトコードに対し、flake8で静的解析、標準unittestでtestsディレクトリのユニットテストを実行します。
これをプルリクエストの作成時、更新時に実行するように設定します。
以下記述例です。

name: demo

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  build:

    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.8]
    steps:
    - uses: actions/checkout@v2
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v1
      with:
        python-version: ${{ matrix.python-version }}
    - name: Lint
      run: |
        pip install flake8
        flake8 demo --show-source
    - name: Test
      run:
        python -m unittest discover tests

プルリクエスト画面で静的解析・ユニットテスト結果を表示する

上記yamlをpushすると、プルリクエストの作成・更新時に、Github Actionsが実行されるようになります。
そしてプルリクエストのマージ画面で次のように静的解析・ユニットテスト結果が付記されるようになります。

no_block

静的解析・ユニットテスト失敗時にプルリクエストのマージをブロックする

次に、masterブランチへのプルリクエストを対象に、静的解析・ユニットテスト失敗時のブロックを有効化します。
設定方法ですが、GithubプロジェクトのSettingsタブ→Branchesメニューを選択します。
Branch name patternの対象をmasterとし、「Require status checks to pass before merging」「Require branches to be up to date before merging」を有効化して、前述のyamlで定義したbuildにチェックを入れます。

この設定を有効化すると、masterに対するプルリクエストについて、静的解析・ユニットテストが失敗するものはマージがブロックされるようになります。

Nスイッチカバレッジ計測ツールを公開

状態遷移テストの補助用に、Nスイッチカバレッジを計測するツールnswitchcov_aを公開しました。

https://github.com/hiro-iseri/nswitchcov_a

使い方の例

CUIツールです。状態遷移定義ファイルと実行フローファイルの2つを入力に実行します。

具体例として、以下の状態遷移を扱います:

f:id:goyoki:20200210002948p:plain

この状態遷移を、以下のテストケースで網羅します:

  • テストケース1 :初期状態→状態1→状態4
  • テストケース2 :初期状態→状態1→状態2→状態3→初期状態
  • テストケース3 :初期状態→状態1→状態3

このケースの2スイッチカバレッジを計測します。

作成するファイル

状態遷移定義ファイル(stateflow.txt)は例えば以下のようになります

初期状態 - イベント1 > 状態1
状態1 - イベント2 > 状態2
状態1 - イベント4 > 状態3
状態1 - イベント5 > 状態4
状態2 - イベント3 > 状態3
状態3 - イベント5 > 初期状態

実行フローファイル(exepath.txt)は例えば以下のようになります

#テストケース1
初期状態 - イベント1 > 状態1 - イベント5 > 状態4
#テストケース2
初期状態 - イベント1 > 状態1 - イベント2 > 状態2 - イベント3 > 状態3 - イベント5 > 初期状態
#テストケース3
初期状態 - イベント1 > 状態1 - イベント4 > 状態3

これらを入力としてコマンドラインで実行すると、2スイッチカバレッジ25.00%の結果が得られます。

nswitchcov_a.exe --exepath=exepath.txt --stateflow=stateflow.txt --n=2
nswitchcov_a(ver:1.01)
number of execution path:3
number of n-switch path:8
n-switch coverage:25.00%(2/8)

GSNでテストタイプを分析する

テストの上流設計でのテストの全体像を分析する手段の一つに、テストタイプの抽出・整理があります。

このテストタイプの分析の助けになるモデリングツールにGSN (Goal Structuring Notation)があります。GSNは、ゴール、戦略、証跡を主な構成要素として、ゴールをどのように達成するかをツリー構造でモデリングする記法です。

Goal Structuring Notation Community Standard

一つの手段ですが、テストタイプの分析にあたっては、ゴールにテストの目的・目標を、証跡にテストタイプを描くようにカスタマイズすることで、GSNをテストタイプ分析ツールとして活用できるようになります。

なおこの活用方法は結構前から存在します。運営に関わらさせていただいているテスト設計コンテストで度々見かける方法であるほか、D-Caseを取り入れた最新のHAYST法でも似た活用方法が見受けられます。

GSNを使ったテストタイプ分析の例ですが、例えば以下のようなモデルになります。

f:id:goyoki:20210804012326p:plain

GSNによるテストタイプ分析に都合が良い点として、以下があります:

  • ツリー構造によりテストアーキテクチャレベルでの関心事の分離を実現できます。これにより大規模なテストも分析しやすくなります。
  • 「戦略」ノードにより、テストタイプ分析にあたって必要な分析や計画が明示化されます。例えば組織に基づいた戦略ノードが出た場合は、組織設計が必要なことがわかりますし、品質特性に基づいた戦略ノードが出た場合は、対象プロダクトが具体的にどのような品質特性で表現すべきかの観点分析が必要なことがわかります。

組み込み開発での品質保証、テスト、CD/CIの未来についてのイベント開催

直前ですが、「Tomorrow’s Software Testing for Embedded Systems」というイベントを開催します。

10月23日 Tomorrow’s Software Testing for Embedded Systems(東京都)

電通大にしさんの同名の講演の再演になります。組み込み開発における品質保証、CI/CD、DevOps、モデルベース開発について講演いただくほか、組み込みの品質保証について深く語る場となります。内容にご興味ある方はぜひご参加いただけると嬉しいです。