Practical Test-Driven Development
本チュートリアルでは、Qmonus SDKでのテスト駆動型開発を学習します。チュートリアルを実践する前にDocs » Scenario » テスト駆動型開発を読んでおいてください。
Step1. アプリケーションの仕様を決定する
最初にチュートリアルで作成するアプリケーションの仕様を決めます。以下のシーケンスのようにクライアントからの指示でオーダーデータを作成し、外部のHogeサービス側にもリソースを生成するアプリケーションを作成しましょう。外部のHogeサービスとのインタラクションは非同期でリソースが生成されるまで状態をポーリングする必要があります。

HogeサービスのAPI仕様
外部のHogeサービスのAPI仕様は、以下のように仮定します。
| method | path | request body | response body | success code | error code | description | 
|---|---|---|---|---|---|---|
| POST | /hoges | name, region | hogeID | Accepted | Conflict, InternalError | リソース作成 | 
| DELETE | /hoges/{hogeID} | hogeID | Accepted | NotFound, InternalError | リソース削除 | |
| GET | /hoges/{hogeID} | hogeID, status | Success | NotFound, InternalError | リソース取得 | 
Hogeリソースの状態マシン
Hogeリソースは以下の状態マシンで駆動すると仮定します。

開発するアプリケーションのAPI仕様
開発するアプリケーションは、1つのAPIです。API仕様は以下の通りとします。
POST /examplesで待ち受け、name、regionを含んだボディーを元に非同期でHogeリソースを外部サービスに生成します。応答は、hogeIDです。
| method | path | request body | response body | success code | error code | 
|---|---|---|---|---|---|
| POST | /examples | name, region | hogeID | Accepted | BadRequest, Conflict, InternalError | 
Step2. テスト観点を実装する
テスト観点は、外部入出力のバリエーションです。これらは、Fakerによって実装することができます。
外部入出力のI/F毎に正常な応答、異常な応答が存在し、それらを網羅的に実装してIllusionで動作を切り替えることで入出力テストを網羅することができます。
まずは、POST /hogesのFakerを定義します。注意点としてHogeリソースの生成は非同期APIであるため、202応答後、一定時間は、Pending状態を維持する必要があります。ここでは、Cache組込みオブジェクトを使用して、15秒間リソースのPending状態を保持しています。
name: postHoge
category: example
fakes:
  Accepted:
    script: |-
      async def Accepted(*args, **kwargs):
          hogeID = uuid.uuid1().hex
          await Cache.put(hogeID, "Pending", 15)
          return FakeHttpResponse(202, body=dict(hogeID=hogeID))
  Conflict:
    script: |-
      async def Conflict(*args, **kwargs):
          return FakeHttpResponse(409)
  InternalError:
    script: |-
      async def InternalError(*args, **kwargs):
          return FakeHttpResponse(500)
次に、アプリケーションが、Active状態への遷移を監視するための、GET /hoges/{hogeID}に対してFakerを定義します。リクエストパスからhogeIDを取り出してPOST時にキャッシュしたPending状態の存在有無によって返却する状態値を切り替えています。
category: example
name: getHogeWaitForActive
fakes:
  InternalError:
    script: |-
      async def InternalError(*args, **kwargs):
          return FakeHttpResponse(500)
  WaitForActive:
    script: |-
      async def Success(*args, **kwargs):
          hogeID = kwargs.get("path").split("/")[-1]
          value = await Cache.get(hogeID)
          return FakeHttpResponse(body=dict(hogeID=hogeID, status="Active" if value is None else "Processing"))
  WaitForError:
    script: |-
      async def Error(*args, **kwargs):
          hogeID = kwargs.get("path").split("/")[-1]
          value = await Cache.get(hogeID)
          return FakeHttpResponse(body=dict(hogeID=hogeID, status="Error" if value is None else "Processing"))
ここまでの試験観点は、上記のシーケンスに従って、すべてが正常に処理されたときのものです。何か問題が発生した場合(Hogeリソースの状態がErrorに移行した場合を含む)、アプリケーションは、ロールバックしなければなりません。ロールバックは、Hogeリソースを完全に削除することです。受信したオーダ情報は履歴として残します。

アプリケーションは、ロールバックを開始すると最初にGET /hoges/{hogeID}を送信してリソースの存在有無をチェックします。この時の応答バリエーションは、存在する、しない、わからないのいづれかです。
category: example
name: getHoge
fakes:
  InternalError:
    script: |-
      async def InternalError(*args, **kwargs):
          return FakeHttpResponse(500)
  NotFound:
    script: |-
      async def NotFound(*args, **kwargs):
          return FakeHttpResponse(404)
  Success:
    script: |-
      async def Success(*args, **kwargs):
          hogeID = kwargs.get("path").split("/")[-1]
          return FakeHttpResponse(body=dict(hogeID=hogeID, status="Active"))
削除対象が存在する場合、アプリケーションは、DELETE /hoges/{hogeID}を送信してリソースの削除を試行します。削除も非同期APIであることからPending状態をキャッシュに15秒間維持します。
category: example
name: deleteHoge
fakes:
  Accepted:
    script: |-
      async def Accepted(*args, **kwargs):
          hogeID = kwargs.get("path").split("/")[-1]
          await Cache.put(hogeID, "Pending", 15)
          return FakeHttpResponse(202, body=dict(hogeID=hogeID))
  InternalError:
    script: |-
      async def InternalError(*args, **kwargs):
          return FakeHttpResponse(500)
  NotFound:
    script: |-
      async def NotFound(*args, **kwargs):
          return FakeHttpResponse(404)
最後に、アプリケーションは、リソースが完全に削除されるのをGET /hoges/{hogeID}を送信して監視します。
category: example
name: getHogeWaitForNotFound
fakes:
  InternalError:
    script: |-
      async def InternalError(*args, **kwargs):
          return FakeHttpResponse(500)
  WaitForNotFound:
    script: |-
      async def WaitForNotFound(*args, **kwargs):
          hogeID = kwargs.get("path").split("/")[-1]
          value = await Cache.get(hogeID)
          if value:
              return FakeHttpResponse(body=dict(hogeID=hogeID, status="Active"))
          else:
              return FakeHttpResponse(404)
以上が、Fakerによる外部入手力の疑似実装です。
Step3. Illusionを定義する
Illusionとは、Step2で作成したFakerのfake動作の集合体です。
全て期待する正常動作となるIllusion
category: example
name: exampleDryrun
fakers:
  postHoge: Accepted
  getHogeWaitForActive: WaitForActive
POSTが失敗するIllusion
category: example
name: examplePostFailed
fakers:
  postHoge: InternalError
Active状態の待機中にエラーが発生し、ロールバックするIllusion
category: example
name: exampleWaitforActiveFailed
fakers:
  postHoge: Accepted
  getHogeWaitForActive: InternalError
  getHoge: Success
  deleteHoge: Accepted
  getHogeWaitForNotFound: WaitForNotFound
Active状態の待機中にエラーが発生し、ロールバックを試みたが、存在チェックのGETでさらに失敗するIllusion
category: example
name: exampleGetFailed
fakers:
  postHoge: Accepted
  getHogeWaitForActive: WaitForError
  getHoge: InternalError
  deleteHoge: Accepted
  getHogeWaitForNotFound: WaitForNotFound
Active状態で待機中にエラーが発生し、ロールバックを試みたが、DELETEで失敗するIllusion
category: example
name: exampleDeleteFailed
fakers:
  postHoge: Accepted
  getHogeWaitForActive: WaitForError
  getHoge: Success
  deleteHoge: InternalError
  getHogeWaitForNotFound: WaitForNotFound
Active状態で待機中にエラーが発生し、ロールバックを試みたが、削除完了の監視ポーリングで失敗するIllusion
category: example
name: exampleWaitforNotFoundFailed
fakers:
  postHoge: Accepted
  getHogeWaitForActive: WaitForError
  getHoge: Success
  deleteHoge: Accepted
  getHogeWaitForNotFound: InternalError
Active状態の待機中にError状態を検出し、ロールバックするIllusion
category: example
name: exampleDetectedErrorHogeState
fakers:
  postHoge: Accepted
  getHogeWaitForActive: WaitForError
  getHoge: Success
  deleteHoge: Accepted
  getHogeWaitForNotFound: WaitForNotFound
Step4. テストケースを実装する
いよいよテストケースを作成していきます。Step3でIllusionを定義しましたが、原則としてIllusionとテストケースは1:1の関係になります。
| テスト分類 | Illusion | テストケース名 | 
|---|---|---|
| Input validation test | Empty body | exampleValidationBodyEmpty | 
| Empty headers | exampleValidationHeaderEmpty | |
| Bad Content-Type | exampleValidationHeaderContentType | |
| Unspecified name | exampleValidationBodyNameRequired | |
| Unspecified region | exampleValidationBodyRegionRequired | |
| Normal test | exampleDryrun | exampleNormalDryrun | 
| Subnormal test | examplePostFailed | examplePostFailed | 
| exampleWaitforActiveFailed | exampleSubnormalWaitforActiveFailed | |
| exampleDetectedErrorHogeState | exampleSubnormalDryrun | |
| Abnormal test | exampleGetFailed | exampleAbnormalGetFailed | 
| exampleDeleteFailed | exampleAbnormalDeleteFailed | |
| exampleWaitforNotFoundFailed | exampleAbnormalWaitforNotFoundFailed | 
exampleValidationBodyEmpty
空bodyを受信したら、400応答することを確認します。
category: example
name: exampleValidationBodyEmpty
target: example
input:
  method: POST
  path: /examples
  headers:
    Content-Type: application/json
  body: |-
    def randomBody():
        return dict()
assertion:
  output: |-
    async def assertion():
        assert Response.code==400, "Invalid validation schema %r" % Response.code
exampleValidationHeaderEmpty
空headerを受信したら、400応答することを確認します。rstrモジュールを利用してランダムなbody値を生成すると便利です。
category: example
name: exampleValidationHeaderEmpty
target: example
input:
  method: POST
  path: /examples
  headers: {}
  body: |-
    def randomBody():
        import rstr
        return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
assertion:
  output: |-
    async def assertion():
        assert Response.code==400, "Invalid validation schema %r" % Response.code
exampleValidationHeaderContentType
application/json以外のContent-Typeを受信したら、400応答することを確認します。
category: example
name: exampleValidationHeaderContentType
target: example
input:
  method: POST
  path: /examples
  headers:
    Content-Type: application/xml
  body: |-
    def randomBody():
        import rstr
        return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
assertion:
  output: |-
    async def assertion():
        assert Response.code==400, "Invalid validation schema %r" % Response.code
exampleValidationBodyNameRequired
nameキーが含まれていないbodyを受信したら、400応答することを確認しています。
category: example
name: exampleValidationBodyNameRequired
target: example
input:
  method: POST
  path: /examples
  headers:
    Content-Type: application/json
  body: |-
    def randomBody():
        import rstr
        return dict(region=rstr.rstr(["jp1", "jp2"], 1))
assertion:
  output: |-
    async def assertion():
        assert Response.code==400, "Invalid validation schema %r" % Response.code
exampleValidationBodyRegionRequired
regionキーが含まれていないbodyを受信したら、400応答することを確認しています。
category: example
name: exampleValidationBodyRegionRequired
target: example
input:
  method: POST
  path: /examples
  headers:
    Content-Type: application/json
  body: |-
    def randomBody():
        import rstr
        return dict(name=rstr.word())
assertion:
  output: |-
    async def assertion():
        assert Response.code==400, "Invalid validation schema %r" % Response.code
exampleNormalDryrun
正しい要求であれば、202応答を返却し、トランザクションがCompleteすることを確認しています。
category: example
name: exampleNormalDryrun
target: example
illusion: exampleDryrun
input:
  method: POST
  path: /examples
  headers:
    Content-Type: application/json
  body: |-
    def randomBody():
        import rstr
        return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
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=="Complete", "Transaction is in an unexpected state %r" % Transaction.status
examplePostFailed
HogeリソースのPOSTに失敗すると、500応答を返却することを確認しています。
category: example
name: examplePostFailed
target: example
illusion: examplePostFailed
input:
  method: POST
  path: /examples
  headers:
    Content-Type: application/json
  body: |-
    def randomBody():
        import rstr
        return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
assertion:
  output: |-
    async def assertion():
        assert Response.code==500, "Invalid response code %r" % Response.code
exampleSubnormalWaitforActiveFailed
HogeリソースのPOSTに成功すると、202応答を返却することを確認しています。その後、Hogeリソースが正常に遷移しなかった場合は、トランザクションがロールバックされていることを確認しています。
category: example
name: exampleSubnormalWaitforActiveFailed
target: example
illusion: exampleWaitforActiveFailed
input:
  method: POST
  path: /examples
  headers:
    Content-Type: application/json
  body: |-
    def randomBody():
        import rstr
        return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
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=="Cancelled", "Transaction is in an unexpected state %r" % Transaction.status
exampleSubnormalDryrun
HogeリソースのPOSTに成功すると、202応答を返却することを確認しています。その後、HogeリソースがErrorに遷移した場合は、トランザクションがロールバックされていることを確認しています。
category: example
name: exampleSubnormalDryrun
target: example
illusion: exampleDetectedErrorHogeState
input:
  method: POST
  path: /examples
  headers:
    Content-Type: application/json
  body: |-
    def randomBody():
        import rstr
        return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
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=="Cancelled", "Transaction is in an unexpected state %r" % Transaction.status
exampleAbnormalGetFailed
HogeリソースのPOSTに成功すると、202応答を返却することを確認しています。その後、トランザクションロールバック時にHogeリソース情報の取得が失敗し、Abortedに遷移していることを確認後、強制ロールバックを実行してForceCancelledに遷移することを確認しています。
category: example
name: exampleAbnormalGetFailed
target: example
illusion: exampleGetFailed
input:
  method: POST
  path: /examples
  headers:
    Content-Type: application/json
  body: |-
    def randomBody():
        import rstr
        return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
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
exampleAbnormalDeleteFailed
HogeリソースのPOSTに成功すると、202応答を返却することを確認しています。その後、トランザクションロールバック時にHogeリソースの削除が失敗し、Abortedに遷移していることを確認後、強制ロールバックを実行してForceCancelledに遷移することを確認しています。
category: example
name: exampleAbnormalDeleteFailed
target: example
illusion: exampleDeleteFailed
input:
  method: POST
  path: /examples
  headers:
    Content-Type: application/json
  body: |-
    def randomBody():
        import rstr
        return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
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
exampleAbnormalWaitforNotFoundFailed
HogeリソースのPOSTに成功すると、202応答を返却することを確認しています。その後、トランザクションロールバック時にHogeリソースの削除ポーリングが失敗し、Abortedに遷移していることを確認後、強制ロールバックを実行してForceCancelledに遷移することを確認しています。
category: example
name: exampleAbnormalWaitforNotFoundFailed
target: example
illusion: exampleWaitforNotFoundFailed
input:
  method: POST
  path: /examples
  headers:
    Content-Type: application/json
  body: |-
    def randomBody():
        import rstr
        return dict(name=rstr.xeger("^[A-Z][a-zA-Z_0-9-]+$"), region=rstr.rstr(["jp1", "jp2"], 1))
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
Step5. テストスイートを定義する
テストケースが作成されたので、それらをテストスイートに統合しましょう。テストをまとめて実行するのに便利です。
category: example
name: exampleSuite
suites:
  testcases:
  - exampleValidationHeaderEmpty
  - exampleValidationHeaderContentType
  - exampleValidationBodyEmpty
  - exampleValidationBodyNameRequired
  - exampleValidationBodyRegionRequired
  - examplePostFailed
  - exampleNormalDryrun
  - exampleSubnormalDryrun
  - exampleAbnormalGetFailed
  - exampleAbnormalDeleteFailed
  - exampleAbnormalWaitforNotFoundFailed
  - exampleSubnormalWaitforActiveFailed
Step6. アプリケーションを実装する
最後にアプリケーションを作成しましょう。ここまででテスト環境は作成済ですのでFrontalでの開発時にTDDをオンにしてテストを駆動しながら実装するのも良いでしょう。
また、以下のATOMやシナリオは実装例です。これまでに定義したテストをクリアするアプリケーションを自由に作成してみてください。
ATOM
作成するAPIは、要求された情報および注文情報としてデータベースに保持します。ATOMで注文情報のCRUDモデルを作成します。
category: example
name: Example
persistence: true
abstract: false
api_generation: false
attributes:
  identifier:
    field_immutable: true
    field_name: name
    field_persistence: true
    field_type: string
  local_fields:
  - field_immutable: false
    field_name: region
    field_nullable: true
    field_persistence: true
    field_type: string
    field_unique: false
  - field_immutable: false
    field_name: description
    field_nullable: true
    field_persistence: true
    field_type: string
    field_unique: false
  - field_immutable: false
    field_name: hogeID
    field_nullable: true
    field_persistence: true
    field_type: string
    field_unique: false
  - field_immutable: false
    field_name: createdAt
    field_nullable: true
    field_persistence: true
    field_type: DateTime
    field_unique: false
  ref_fields: []
methods:
  class_methods: []
  instance_methods: []
注文情報には識別子としてnameがあり、属性としてregion、description、createdAtが定義されています。また、hogeリソースに割り当てられたhogeIDを保持します。
Scenario
最後に、シナリオを作成します。 TDD自動運転モードを有効にして、シナリオを実装してください。すべてのテストに合格することを願っています!
category: Tutorial
name: example
method: POST
uri: /examples
routing_auto_generation_mode: true
global_variables:
  example:
    initial: null
  hoge:
    initial: null
  hogeID:
    initial: null
  name:
    description: resource name
    initial: null
  r:
    initial: null
  region:
    description: region name
    initial: null
transaction:
  async: true
  auto_response: false
  enable: true
commands:
- command: request_validation
  label: Order Validation
  kwargs:
    aspect_options:
      post:
        process: |-
          (name, region) = (context.request.body.name, context.request.body.region)
    body:
      properties:
        description:
          type: string
        name:
          pattern: ^[A-Z][a-zA-Z_0-9-]+$
          type: string
        region:
          enum:
          - jp1
          - jp2
          type: string
      required:
      - name
      - region
      type: object
    headers:
      properties:
        Content-Type:
          enum:
          - application/json
          type: string
      required:
      - Content-Type
      type: object
- command: script
  label: Create Hoge
  kwargs:
    code: |-
      faker("ExampleFaker")
      await atom.Example(**context.request.body.dictionary).save()
      faker("postHoge")
      r = await callout(path="/hoges", method="POST", body=dict(name=name, region=region))
      if r.error:
          raise Error(r.code, reason="POST failed")
      hogeID = MU(json.loads(r.body)).hogeID
      context.session.set_status(202)
      context.session.finish(dict(hogeID=hogeID))
    cancellation:
      cancellable: true
      actions:
      - action_type: script
        code: |-
          if hogeID:
              faker("getHoge")
              r = await callout(path="/hoges/{}".format(hogeID))
              if r.error and r.code != 404:
                  raise Error(r.code, reason="Unable to get hoge %r" % hogeID)
              if r.code!=404:
                  faker("deleteHoge")
                  r = await callout(path="/hoges/{}".format(hogeID), method="DELETE")
                  if r.error and r.code != 404:
                      raise Error(r.code, reason="Unable to delete hoge %r" % hogeID)
              for i in range(30):
                  faker("getHogeWaitForNotFound")
                  r = await callout(path="/hoges/{}".format(hogeID))
                  if r.error and r.code==404:
                      faker("ExampleFaker")
                      example = await atom.Example.load(name)
                      if example is not None:
                          faker("ExampleFaker")
                          await example.destroy()
                          return
                  await asyncio.sleep(1)
              raise Error(500, reason="Status poll retry over")
- command: script
  label: Wait for Active State
  kwargs:
    code: |-
      for i in range(30):
          faker("getHogeWaitForActive")
          r = await callout(path="/hoges/{}".format(hogeID))
          if r.error:
              continue
          hoge = MU(json.loads(r.body))
          if hoge.status == "Active":
              return
          elif hoge.status == "Error":
              raise Error(500, reason="Transited to Error state")
          await asyncio.sleep(1)
      raise Error(500, reason="Status poll retry over")
- command: script
  label: Update Order
  kwargs:
    code: |-
      faker("ExampleFaker")
      example = await atom.Example.load(name)
      (example.hogeID, example.createdAt) = (hogeID, clock.now())
      faker("ExampleFaker")
      await example.save()
Tip
上述のサンプルでは、illusionプラグインを全て事前に定義していますが、illusionプラグインを事前作成せずにtestcaseに直接fakerのfakeアクションを紐付けて定義することができます。この場合、対応するillusionが必要に応じて自動生成されます。testcaseに対するillusionとfakerの指定と適用条件は以下の通りです。
1. faker未指定且つillusion未指定: なし
2. faker未指定且つillusion指定あり: illusion採用
3. faker指定あり且つillusion未指定: illusion自動生成
4. faker指定あり且つillusion指定あり:
- 指定illusionとfake動作が一致している: 指定illusionを採用
- 指定illusionとfake動作が一致していない: illusion自動生成
Tip
testcaseの各フェーズで変数を共有したい場合(例えばpreparationでテストデータを生成し、cleanupで削除する際にIDを持ち回りたいなど)、フェーズのテスト関数から共有したい変数をreturnします。returnしたデータは、次以降のフェーズの関数内で Resultsという辞書変数で参照できます。キーはフェーズ名となります。preparationフェーズでreturnしたデータは、Results["preparation"]で参照できます。尚、プラグインモジュールやファンクションが格納される context変数に埋め込んで共有する方法もありますが、名前競合の観点から推奨しません。 Results共有変数は、v21.2LTS-patch20210910以降のversionで利用できます。