Garmin アプリ開発(6) テスト設計

apps.garmin.com

公式SDKにはユニットテストのサポートが入ってます。

統合環境をセットアップしただけでユニットテストが準備済みなのはすばらしいですね。時代の流れを感じます。

公式:https://developer.garmin.com/connect-iq/core-topics/unit-testing/

 

test runner

テストを実行するには、「(:test)」アノテーションをstaticな関数につけます。

VS ccode上で、 CTRL+Shift+p でコマンドパレットを開き、Monkey C : Run Testを実行すると呼び出されます。

(:test)
function HandlerIsActive_Test1(logger as Logger) as Boolean {
    print("AppBase_test1 start");
    var view  = new ViewMock();
....
}

逆にRun Testを実行しても、通常初めに実行されるAppBase派生クラスは実行されません。クラスの生成などはすべてこのテストランナー内で行う必要がありますので注意しましょう。

 

mockクラス

mock定義も同様に「(:test)」で良いみたいです(ドキュメントには書いて無さそうですが)。こちらは自動で実行されませんが、テストランナーから利用することができます。

(:test)
class ViewMock extends AdditionalTimeStopwatchView{
        function initialize(){
            AdditionalTimeStopwatchView.initialize(); //親クラスを初期化
        }
...
}

 

テスト設計

f:id:mnobori:20211228151908p:plain

test diagram

主に自作の2つのクラス「AdditionalTimeHandler」「StopWatch」をテストします。そのためにtestフォルダー内に2つのテストランナー、2つのクラスを作りました。

Hander_test, StopWatch_testはテストランナーです。シンプルなテストケースを多数並べておけば、順次呼び出されます。

 

Garmin アプリ開発(5) クラス構造 - mnob Tech blog では他にSDK派生クラスが3個(App, 入力, View)ありましたが、こいつらはテストしません
SDK派生クラスであることからSDK依存部分がテスト困難なこと、最初っからそれを見越してシンプルに作ることで自動テストを不要にする考え方です。

 

View_Mockクラス

AdditionalTimeHandlerはAdditionalTimeView(SDK派生)クラスに依存しています。AdditioanlTimeViewクラスはSDK依存部分がテストしにくいので、そのままではテストできません。

そこでViewクラスを派生して、View_Mockクラスを準備します。

View_MockはGarmin SDKの呼び出しを行わないように、AdditionalTimeHandlerから呼ばれる外部APIを全部printfに置き換えてテストが動けばOKという方針です

        public function setClock(clockTime){
            printf("view.setClock: $1$:$2$", [clock2HourMin(clockTime), clock2Sec(clockTime)]);
        }
        public function setFreerun(elapsed){
            printf("view.setFreerun: $1$", [elapsed2MinSec(elapsed)]);
        }
    ....

 

StopWatch_testSubclass クラス

StopWatchクラスは現在のシステム時刻を取得するため、SDKのSystemに依存しています。

  protected function getNow() as Number{
        return System.getTimer();
    }

System.getTimer()でシステムのミリ秒を取得して計算していますが、このままではテストのたびに違う値になりテストできません。

 

そこでt-wadaさんのこの記事を参考に対策します。現在時刻が関わるユニットテストから、テスト容易性設計を学ぶ - t-wadaのブログ

今回は「アプローチ4: 対象クラスのテスト用サブクラスをテスト内で作成 (Test-Specific Subclass)」の戦略を採用しました。

(:test)
class StopWatch_TestSubclass extends StopWatch{
    private var nowStub = 0 as Number;
  function initialize() {
        StopWatch.initialize();
  }
    //override
    public function getNow() as Number{
        return nowStub;
    }
  public function setNow(now as Number){
        nowStub = now;
    }
}

システムに依存していた getNow() メソッドをoverrideして、setNow()でセットされた値を使うサブクラスを作成しました。このサブクラスをテスト対象にすることで、この2つのメソッド以外は本体のStopWatchクラスのテストが可能になりました。

 

テストケースの例

(:test)
function StopWatch_Test4(logger as Logger) as Boolean {
    var stw = new StopWatch_TestSubclass();
    stw.setNow(1000); // 現在時刻を1秒にセット
    stw.StartStop();  // Start/Stopボタンを押す
    stw.setNow(2000); // 現在時刻を2秒にセット
    stw.StartStop();   // Start/Stopボタンを押す
    stw.setNow(3500);  // 現在時刻を3.5秒にセット
    return (stw.getElapsed() == 1000); // 経過時間が1秒かどうかテスト
}

このように、現在時刻をセットしてストップウォッチのテストができます

 

 

ソースコードhttps://github.com/masahiro-nobori-964a-d32a/Timers11Plus/tree/main/test

 

 

(続く:ネタ一覧 Vibration, Storage, 多機種展開, 49日問題, Picker, パフォーマンスチューニング, DebugPrintf)