テスト駆動型開発

Qmonus SDKでは、テスト駆動型開発を行うためのデバッグ機能やプラグインのユニットテストをサポートする機能群を提供しています。


デバッグソリューション

ScenarioATOMのランタイムは、Frontalが提供する様々なモニタリング機能で追跡できます。しかしながらプラグインのコードレベルで問題箇所を特定するには、不十分です。Qmonus SDKでは、より、コードレベルでの問題発見を効率化するためにプラグインのランタイムコードトレース機能とプロファイリング機能を提供しています。


ランタイムコードトレーシング

ランタイムコードトレースは、REPLが提供するtraceon traceoff組込みコマンドで簡単に有効化、無効化できます。
traceonコマンドは、プラグインの実行時に発行されるSQL文をトレースすることもできます。コマンドの詳細はDocs » リファレンス » REPLを参照してください。


コードプロファイリング

コードプロファイリングは、REPLが提供するprofon profoff組込みコマンドで簡単に有効化、無効化できます。
コマンドの詳細はDocs » リファレンス » REPLを参照してください。


コードカバレッジの計測

コードカバレッジ測定モードでテストを実行することにより、プラグインのコード走行率をリアルタイムで確認することができます。テストカバレッジの参考にしてください。カバレッジ計測モードの有効化、無効化は、Frontal画面上で行うか、coverage組込みコマンドを使用する必要があります。
コマンドの詳細はDocs » リファレンス » REPLを参照してください。

Note

ランタイムトレースモード、ランタイムプロファイリングモード、およびランタイムカバレッジモードは、排他的にのみ使用できます。


ドライランソリューション

外部サービスや他のプラグインへの通信や永続化などの入出力を伴うScenarioATOMなどのプラグイン開発では、接続先のサービスのインタフェース動作を模擬するモックが必要になります。テスト駆動開発では、様々な入出力の応答パターンをテストしながら実装を進めていかなければなりません。このようなプラグイン開発者が意図した入出力を模擬するため、FakerIllusionサービスが提供されています。

入出力の模擬パターンをFakerサービスで定義し、プラグインのランタイムに適用する模擬パターンの組み合わせ(テストシーン)をIllusionサービスで定義できます。Illusionを指定してプラグインを実行することでプラグインが受信する外部サービスの応答を疑似させ、意図した動作が実装できているかを検査することが可能になります。


Faker

Fakerは疑似動作の定義セットです。以下の例に示す通り、タグ付けされたフェイク動作関数のリストを保有します。タグは後述するIllusionにフェイク動作を割り当てる際のキーとなるため、同一Fakerの保持するフェイク関数の中でユニークでなければなりません。

category: example
name: exampleFaker
fakes:
  normal:
    script: |-
      async def Success(*args, **kwargs):
          return FakeHttpResponse(body={"status": True})
  abnormal:
    script: |-
      async def BadRequest(*args, **kwargs):
          return FakeHttpResponse(400, body={"status": False})


Tip

組込みオブジェクトFakeHttpResponseは、HTTP疑似応答を作成するのに役立ちます。使用方法についてはDocs » リファレンス » ビルトインオブジェクトを参照してください。
また、Fakerのフェイク関数は、Faker組込みオブジェクトを使用してREPL上で動作確認することができます。
複雑な処理を行うフェイク関数をデバッグする場合、REPLの組込みコマンドであるtraceonでコードトレーシングを有効にすることで解析効率を向上させることができます。

>>> f = await Faker.load("exampleFaker")↵
... r = await f.Success()↵
... print(r)↵
... ↵
{
    "code": 200,
    "headers": {
        "Content-Type": "application/json"
    },
    "error": null,
    "body": "{\"status\":true}"
}
↵
>>> r = await f.BadRequest()↵
... print(r)↵
... ↵
{
    "code": 400,
    "headers": {
        "Content-Type": "application/json"
    },
    "error": true,
    "body": "{\"status\":false}"
}
↵
>>>


Illusion

Illusionは、フェイク関数の集合概念です。ScenarioATOMなどのプラグインが動作するテストシーンにおいてプラグインの周辺サービスがどのような応答を返すかを定義しているイメージです。
ScenarioIllusion空間で実行するには、拡張ヘッダーを指定する必要があります。X-Xaas-Illusion拡張ヘッダーでIllusion名を指定します。
尚、REPLでは、REPLの実行空間にIllusionを適用できます。Docs » リファレンス » REPLillusionコマンドを参照してください。

category: example
name: normalScene
fakers:
  exampleFaker: Success
category: example
name: subnormalScene
fakers:
  exampleFaker: BadRequest


Fakerの使用方法

Fakerを使用するには、ScenarioATOMのカスタムスクリプトにfaker宣言を記述する必要があります。
faker宣言は、引数としてFakerの名前を与える必要があります。faker宣言を埋め込まれたコードは、指定されたFakerのフェイク関数を集約しているIllusion適用空間においてのみフェイク関数に置換されます。置換されるコードは、faker宣言の直後に記述されている関数呼び出しコードです。

Note

プラグインコードに記述されているfaker宣言によるコードスワップは、Illusion適用時にしか行われません。


例えば、次のHTTP通信を実行するスクリプトがあります。

r = await callout(url="https://www.google.com/")
if r.error:
    raise Error(r.code, reason="Unable to access")


faker宣言は、フェイク動作を適用したい入出力処理の直前に記述します。fakerは単一の引数としてFakerの名前を指定する必要があります。

faker("exampleFaker")
r = await callout(url="https://www.google.com/")
if r.error:
    raise Error(r.code, reason="Unable to access")


Fakerは、X-Xaas-Illusion拡張ヘッダーで Illusion名を指定することで動作します。

>>> r = await callout(path="/example1", headers={"X-Xaas-Illusion": "normalScene"})↵
... print(r.body)↵
... ↵
b'{"status":true}'
↵
>>> r = await callout(path="/example1", headers={"X-Xaas-Illusion": "subnormalScene"})↵
... print(r.body)↵
... ↵
b'{"errorCode":400,"errorMessage":"Unable to access","moreInfo":null}'
↵
>>>


テストフレームワーク

Qmonus SDKのテストフレームワークは、次の図に示す構造になっています。

上記のFakerは、入出力のバリエーションを表現したモデルであり、テストの観点に相当します。Illusionは、テスト実行時の外部環境の動作をモデル化したもので、テストシーンに相当します。 Testcaseは、テスト本体であり、1つのテストシーンに関連しています。Testsuiteは、テストの集合概念です。


Warning

現在、Testsuiteは、他のTestsuiteをコンポジットできるモデルで実装されていますが、管理が複雑化するため、Testsuiteのコンポジット定義は非推奨です。


Testcaseの実行ワークフローは以下の通りです。


サンプルテストケース

テストケースの名前空間には、ResponseTransaction、およびTestcaseオブジェクトが含まれており、これらは各assert関数によるチェックに使用できます。

category: example
name: exampleAbnormalDeleteFailed
target: example
illusion: exampleDeleteFailed
input:
  body: |-
    def randomBody():
        import rstr
        return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
  headers:
    Content-Type: application/json
  method: POST
  path: /examples
assertion:
  output: |-
    async def assertion():
        assert Response.code==202, "Invalid response code %r" % Response.code
        assert Response.body, "Empty body %r" % Response.body
        assert "hogeID" in json.loads(Response.body), "Invalid response body %s" % Response.body
  progress:
  - index: 1
    script: |-
      async def assertion():
          assert Transaction.xglobals.name and Transaction.xglobals.region, "name or region is None"
          assert Transaction.xglobals.hogeID is not None, "hogeID is None"
  end: |-
    async def assertion():
        assert Transaction.status=="Aborted", "Transaction is in an unexpected state %r" % Transaction.status
cleanup: |-
  async def cleanup():
      await Transaction.cancel(force=True)
      assert Transaction.status=="ForceCancelled", "Force cancel failed %r" % Transaction.status


Learn More

Qmonus SDKでのTDDを理解するには、以下のムービーが参考になります。

TDD demonstration movie


さらにTDDについて詳しく知りたい場合は、Docs » チュートリアル » Practical Test-Driven Developmentで実際に開発を体験することができます。